Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46fc74e90f | ||
|
|
2c4f4092d8 | ||
|
|
aac7268dc8 | ||
|
|
fd162a72ff | ||
|
|
e20936709c | ||
|
|
2fda2c8c15 | ||
|
|
1f09757c49 | ||
|
|
7e49babff7 | ||
|
|
b35653ade1 | ||
|
|
57b53013d1 | ||
|
|
bafd3f3c05 | ||
|
|
29db0a50b3 | ||
|
|
9566fb888e | ||
|
|
13c8903e8f | ||
|
|
1d06b47e8d | ||
|
|
0089c2ee43 | ||
|
|
9869a9fcb7 | ||
|
|
631f29d66d | ||
|
|
97deebad37 | ||
|
|
e667615449 | ||
|
|
4ab61faeae | ||
|
|
0004db3d4a | ||
|
|
c133c472be | ||
|
|
0019ccdf51 | ||
|
|
5a1a469f35 | ||
|
|
4c28f34803 | ||
|
|
104be830fc | ||
|
|
24c70791cd | ||
|
|
216fdea290 | ||
|
|
40e4ce2ed3 | ||
|
|
b2508fc6f2 | ||
|
|
f73b37287f | ||
|
|
c09eb738c3 | ||
|
|
6de3a6ec28 | ||
|
|
c1d35cc8b3 | ||
|
|
f279b2bea2 | ||
|
|
6ffbf756f8 | ||
|
|
10837d7ba1 | ||
|
|
2d6eddf9d4 | ||
|
|
aadf43efc3 | ||
|
|
227f729ecd | ||
|
|
a5b15e37f6 | ||
|
|
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 |
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"
|
||||
|
||||
81
.github/workflows/release.yaml
vendored
81
.github/workflows/release.yaml
vendored
@@ -1,36 +1,50 @@
|
||||
name: Release
|
||||
# every morning
|
||||
on:
|
||||
# every morning
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
- cron: '0 12 * * 1-4'
|
||||
|
||||
# releases are triggered on changes to this file
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
paths:
|
||||
- '.github/workflows/release.yaml'
|
||||
- '.env.production'
|
||||
|
||||
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 +53,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,26 +80,25 @@ 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 }})
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
||||
|
||||
The IPFS hash of the bundle is `${{ steps.upload.outputs.hash }}`
|
||||
|
||||
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.
|
||||
The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on the Uniswap interface without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway. 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 }}/)
|
||||
- https://dweb.link/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 }}/
|
||||
|
||||
${{ 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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TEST_ADDRESS_NEVER_USE } from '../support/commands'
|
||||
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
|
||||
|
||||
describe('Landing Page', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
@@ -22,6 +22,6 @@ describe('Landing Page', () => {
|
||||
|
||||
it('is connected', () => {
|
||||
cy.get('#web3-status-connected').click()
|
||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE)
|
||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ describe('Pool', () => {
|
||||
beforeEach(() => cy.visit('/pool'))
|
||||
it('can search for a pool', () => {
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.get('#token-search-input').type('DAI')
|
||||
cy.get('#token-search-input').type('DAI', { delay: 200 })
|
||||
})
|
||||
|
||||
it('can import a pool', () => {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ describe('Send', () => {
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#sending-no-swap-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,31 +2,31 @@ describe('Swap', () => {
|
||||
beforeEach(() => cy.visit('/swap'))
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.0')
|
||||
.type('0.0', { delay: 200 })
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('\\')
|
||||
.type('\\', { delay: 200 })
|
||||
.should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero output amount', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.0')
|
||||
.type('0.0', { delay: 200 })
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
@@ -35,9 +35,8 @@ describe('Swap', () => {
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true, delay: 200 })
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
cy.get('#show-advanced').click()
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
||||
})
|
||||
|
||||
2
cypress/support/commands.d.ts
vendored
2
cypress/support/commands.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
export const TEST_ADDRESS_NEVER_USE: string
|
||||
|
||||
export const TEST_ADDRESS_NEVER_USE_SHORTENED: string
|
||||
|
||||
// declare namespace Cypress {
|
||||
// // eslint-disable-next-line @typescript-eslint/class-name-casing
|
||||
// interface cy {
|
||||
|
||||
@@ -14,6 +14,8 @@ const PRIVATE_KEY_TEST_NEVER_USE = '0xad20c82497421e9784f18460ad2fe84f73569068e9
|
||||
// address of the above key
|
||||
export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
|
||||
|
||||
export const TEST_ADDRESS_NEVER_USE_SHORTENED = '0x0fF2...F4a5'
|
||||
|
||||
class CustomizedBridge extends _Eip1193Bridge {
|
||||
async sendAsync(...args) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
@@ -71,10 +73,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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
|
||||
headers = {Link="<https://uniswap.exchange>"}
|
||||
|
||||
# forward migrate
|
||||
[[redirects]]
|
||||
from = "https://migrate.uniswap.exchange/*"
|
||||
to = "https://uniswap.exchange/migrate/v1"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
# forward v2 subdomain to apex
|
||||
[[redirects]]
|
||||
from = "https://v2.uniswap.exchange/*"
|
||||
|
||||
14
package.json
14
package.json
@@ -13,12 +13,12 @@
|
||||
"@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/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",
|
||||
@@ -33,11 +33,10 @@
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"@uniswap/sdk": "^2.0.5",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "1.0.0-beta.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
"@web3-react/fortmatic-connector": "^6.0.9",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/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",
|
||||
@@ -76,13 +75,12 @@
|
||||
"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"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
|
||||
"build": "react-scripts build",
|
||||
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
|
||||
75
public/locales/he.json
Normal file
75
public/locales/he.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"noWallet": "לא נמצא ארנק",
|
||||
"wrongNetwork": "נבחרה רשת לא נכונה",
|
||||
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
|
||||
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
|
||||
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
|
||||
"disconnected": "מנותק",
|
||||
"swap": "המרה",
|
||||
"send": "שליחה",
|
||||
"pool": "להפקיד",
|
||||
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
|
||||
"input": "מוכר",
|
||||
"output": "אקבל",
|
||||
"estimated": "הערכה",
|
||||
"balance": "בארנק שלי {{ balanceInput }}",
|
||||
"unlock": "שחרור נעילת ארנק",
|
||||
"pending": "ממתין לאישור",
|
||||
"selectToken": "בחרו את הטוקן להמרה",
|
||||
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
|
||||
"noExchange": "לא מתאפשרת המרה",
|
||||
"exchangeRate": "שער המרה",
|
||||
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
|
||||
"selectTokenCont": "בחרו טוקן כדי להמשיך",
|
||||
"noLiquidity": "אין נזילות",
|
||||
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
|
||||
"transactionDetails": "פרטי הטרנזקציה",
|
||||
"hideDetails": "הסתר פרטים נוספים",
|
||||
"youAreSelling": "למכירה",
|
||||
"orTransFail": "או שהטרנזקציה תיכשל",
|
||||
"youWillReceive": "תוצר המרה מינימלי",
|
||||
"youAreBuying": "קונה",
|
||||
"itWillCost": "זה יעלה",
|
||||
"insufficientBalance": "אין בחשבון מספיק מטבעות",
|
||||
"inputNotValid": "קלט לא תקין",
|
||||
"differentToken": "יש צורך בטוקנים שונים",
|
||||
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
|
||||
"invalidRecipient": "לא הוכנסה כתובת תקינה",
|
||||
"recipientAddress": "כתובת יעד",
|
||||
"youAreSending": "כמות לשליחה",
|
||||
"willReceive": "יתקבל לכל הפחות",
|
||||
"to": "אל",
|
||||
"addLiquidity": "להוספת נזילות למאגר",
|
||||
"deposit": "הפקדה",
|
||||
"currentPoolSize": "גודל מאגר הנזילות הכולל",
|
||||
"yourPoolShare": "חלקך במאגר הנזילות",
|
||||
"noZero": "אפס אינו ערך תקין",
|
||||
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
|
||||
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
|
||||
"youAreAdding": "מתווספים למאגר",
|
||||
"and": "וגם",
|
||||
"intoPool": "לתוך הנזילות",
|
||||
"outPool": "מתוך",
|
||||
"youWillMint": "יונפקו לכם",
|
||||
"liquidityTokens": "טוקנים של נזילות",
|
||||
"totalSupplyIs": "חלקך במאגר הנזילות",
|
||||
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
|
||||
"totalSupplyIs0": "אין לך טוקנים של נזילות",
|
||||
"tokenWorth": "שווי כל טוקן נזילות הינו",
|
||||
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר",
|
||||
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
|
||||
"removeLiquidity": "הוצאה של נזילות",
|
||||
"poolTokens": "טוקנים של מאגר הנזילות",
|
||||
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
|
||||
"youAreRemoving": "יוסרו",
|
||||
"youWillRemove": "יוסרו",
|
||||
"createExchange": "ליצירת זוג מסחר",
|
||||
"invalidTokenAddress": "כתובת טוקן לא נכונה",
|
||||
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
|
||||
"invalidSymbol": "תו שגוי",
|
||||
"invalidDecimals": "ספרות עשרוניות שגויות",
|
||||
"tokenAddress": "כתובת הטוקן",
|
||||
"label": "שם",
|
||||
"decimals": "ספרות עשרויות",
|
||||
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
|
||||
}
|
||||
75
public/locales/iw.json
Normal file
75
public/locales/iw.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"noWallet": "לא נמצא ארנק",
|
||||
"wrongNetwork": "נבחרה רשת לא נכונה",
|
||||
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
|
||||
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
|
||||
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
|
||||
"disconnected": "מנותק",
|
||||
"swap": "המרה",
|
||||
"send": "שליחה",
|
||||
"pool": "להפקיד",
|
||||
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
|
||||
"input": "מוכר",
|
||||
"output": "אקבל",
|
||||
"estimated": "הערכה",
|
||||
"balance": "בארנק שלי {{ balanceInput }}",
|
||||
"unlock": "שחרור נעילת ארנק",
|
||||
"pending": "ממתין לאישור",
|
||||
"selectToken": "בחרו את הטוקן להמרה",
|
||||
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
|
||||
"noExchange": "לא מתאפשרת המרה",
|
||||
"exchangeRate": "שער המרה",
|
||||
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
|
||||
"selectTokenCont": "בחרו טוקן כדי להמשיך",
|
||||
"noLiquidity": "אין נזילות",
|
||||
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
|
||||
"transactionDetails": "פרטי הטרנזקציה",
|
||||
"hideDetails": "הסתר פרטים נוספים",
|
||||
"youAreSelling": "למכירה",
|
||||
"orTransFail": "או שהטרנזקציה תיכשל",
|
||||
"youWillReceive": "תוצר המרה מינימלי",
|
||||
"youAreBuying": "קונה",
|
||||
"itWillCost": "זה יעלה",
|
||||
"insufficientBalance": "אין בחשבון מספיק מטבעות",
|
||||
"inputNotValid": "קלט לא תקין",
|
||||
"differentToken": "יש צורך בטוקנים שונים",
|
||||
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
|
||||
"invalidRecipient": "לא הוכנסה כתובת תקינה",
|
||||
"recipientAddress": "כתובת יעד",
|
||||
"youAreSending": "כמות לשליחה",
|
||||
"willReceive": "יתקבל לכל הפחות",
|
||||
"to": "אל",
|
||||
"addLiquidity": "להוספת נזילות למאגר",
|
||||
"deposit": "הפקדה",
|
||||
"currentPoolSize": "גודל מאגר הנזילות הכולל",
|
||||
"yourPoolShare": "חלקך במאגר הנזילות",
|
||||
"noZero": "אפס אינו ערך תקין",
|
||||
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
|
||||
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
|
||||
"youAreAdding": "מתווספים למאגר",
|
||||
"and": "וגם",
|
||||
"intoPool": "לתוך הנזילות",
|
||||
"outPool": "מתוך",
|
||||
"youWillMint": "יונפקו לכם",
|
||||
"liquidityTokens": "טוקנים של נזילות",
|
||||
"totalSupplyIs": "חלקך במאגר הנזילות",
|
||||
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
|
||||
"totalSupplyIs0": "אין לך טוקנים של נזילות",
|
||||
"tokenWorth": "שווי כל טוקן נזילות הינו",
|
||||
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר",
|
||||
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
|
||||
"removeLiquidity": "הוצאה של נזילות",
|
||||
"poolTokens": "טוקנים של מאגר הנזילות",
|
||||
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
|
||||
"youAreRemoving": "יוסרו",
|
||||
"youWillRemove": "יוסרו",
|
||||
"createExchange": "ליצירת זוג מסחר",
|
||||
"invalidTokenAddress": "כתובת טוקן לא נכונה",
|
||||
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
|
||||
"invalidSymbol": "תו שגוי",
|
||||
"invalidDecimals": "ספרות עשרוניות שגויות",
|
||||
"tokenAddress": "כתובת הטוקן",
|
||||
"label": "שם",
|
||||
"decimals": "ספרות עשרויות",
|
||||
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
|
||||
}
|
||||
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,26 +1,26 @@
|
||||
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)`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
const CopyIcon = styled(LinkStyledButton)`
|
||||
color: ${({ theme }) => theme.text3};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
margin-right: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: none;
|
||||
font-size: 0.825rem;
|
||||
:hover,
|
||||
:active,
|
||||
:focus {
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
}
|
||||
`
|
||||
const TransactionStatusText = styled.span`
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.825rem;
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
`
|
||||
@@ -30,7 +30,6 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
|
||||
|
||||
return (
|
||||
<CopyIcon onClick={() => setCopied(props.toCopy)}>
|
||||
{props.children}
|
||||
{isCopied ? (
|
||||
<TransactionStatusText>
|
||||
<CheckCircle size={'16'} />
|
||||
@@ -41,6 +40,7 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
|
||||
<Copy size={'16'} />
|
||||
</TransactionStatusText>
|
||||
)}
|
||||
{isCopied ? '' : props.children}
|
||||
</CopyIcon>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,55 +1,39 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Check, Triangle } from 'react-feather'
|
||||
import { CheckCircle, Triangle, ExternalLink as LinkIcon } from 'react-feather'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { Link, Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
|
||||
import { transparentize } from 'polished'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { RowFixed } from '../Row'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const TransactionWrapper = styled.div`
|
||||
margin-top: 0.75rem;
|
||||
`
|
||||
const TransactionWrapper = styled.div``
|
||||
|
||||
const TransactionStatusText = styled.div`
|
||||
margin-right: 0.5rem;
|
||||
`
|
||||
|
||||
const TransactionState = styled(Link)<{ pending: boolean; success?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
text-decoration: none !important;
|
||||
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid;
|
||||
|
||||
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
|
||||
|
||||
border-color: ${({ pending, success, theme }) =>
|
||||
pending
|
||||
? transparentize(0.75, theme.primary1)
|
||||
: success
|
||||
? transparentize(0.75, theme.green1)
|
||||
: transparentize(0.75, theme.red1)};
|
||||
|
||||
align-items: center;
|
||||
:hover {
|
||||
border-color: ${({ pending, success, theme }) =>
|
||||
pending
|
||||
? transparentize(0, theme.primary1)
|
||||
: success
|
||||
? transparentize(0, theme.green1)
|
||||
: transparentize(0, theme.red1)};
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
flex-shrink: 0;
|
||||
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-decoration: none !important;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.825rem;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>`
|
||||
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
|
||||
`
|
||||
|
||||
export default function Transaction({ hash }: { hash: string }) {
|
||||
@@ -65,9 +49,12 @@ export default function Transaction({ hash }: { hash: string }) {
|
||||
return (
|
||||
<TransactionWrapper>
|
||||
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
|
||||
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
|
||||
<IconWrapper>
|
||||
{pending ? <Spinner src={Circle} /> : success ? <Check size="16" /> : <Triangle size="16" />}
|
||||
<RowFixed>
|
||||
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
|
||||
<LinkIcon size={16} />
|
||||
</RowFixed>
|
||||
<IconWrapper pending={pending} success={success}>
|
||||
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
|
||||
</IconWrapper>
|
||||
</TransactionState>
|
||||
</TransactionWrapper>
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { useCallback, useContext } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { AppDispatch } from '../../state'
|
||||
import { clearAllTransactions } from '../../state/transactions/actions'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { AutoRow } from '../Row'
|
||||
import Copy from './Copy'
|
||||
import Transaction from './Transaction'
|
||||
@@ -18,10 +18,9 @@ import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||
import Identicon from '../Identicon'
|
||||
|
||||
import { ButtonEmpty } from '../Button'
|
||||
|
||||
import { Link, TYPE } from '../../theme'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
@@ -55,31 +54,31 @@ const UpperSection = styled.div`
|
||||
|
||||
const InfoCard = styled.div`
|
||||
padding: 1rem;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-row-gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const AccountGroupingRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
|
||||
div {
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const AccountSection = styled.div`
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
padding: 0rem 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1rem 1rem;`};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
|
||||
`
|
||||
|
||||
const YourAccount = styled.div`
|
||||
@@ -94,28 +93,6 @@ const YourAccount = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const GreenCircle = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:first-child {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
margin-left: 12px;
|
||||
margin-right: 2px;
|
||||
background-color: ${({ theme }) => theme.green1};
|
||||
border-radius: 50%;
|
||||
}
|
||||
`
|
||||
|
||||
const CircleWrapper = styled.div`
|
||||
color: ${({ theme }) => theme.green1};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const LowerSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
padding: 1.5rem;
|
||||
@@ -132,13 +109,14 @@ const LowerSection = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
const AccountControl = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? 500 : 400) : 500)};
|
||||
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? '1rem' : '0.8rem') : '1rem')};
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
@@ -146,22 +124,22 @@ const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
|
||||
|
||||
p {
|
||||
min-width: 0;
|
||||
margin: 0.5rem 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
|
||||
const ConnectButtonRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 10px 0;
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)<{ hasENS: boolean; isENS: boolean }>`
|
||||
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.primary1 : theme.text3) : theme.primary1)};
|
||||
const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
|
||||
font-size: 0.825rem;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
margin-left: 1rem;
|
||||
font-size: 0.825rem;
|
||||
display: flex;
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.text2};
|
||||
}
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
@@ -181,14 +159,17 @@ const CloseColor = styled(Close)`
|
||||
`
|
||||
|
||||
const WalletName = styled.div`
|
||||
padding-left: 0.5rem;
|
||||
width: initial;
|
||||
font-size: 0.825rem;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
& > img,
|
||||
span {
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
@@ -203,10 +184,12 @@ const TransactionListWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
`
|
||||
|
||||
const WalletAction = styled.div`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
margin-left: 16px;
|
||||
const WalletAction = styled(ButtonSecondary)`
|
||||
width: fit-content;
|
||||
font-weight: 400;
|
||||
margin-left: 8px;
|
||||
font-size: 0.825rem;
|
||||
padding: 4px 6px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
@@ -255,39 +238,39 @@ export default function AccountDetails({
|
||||
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
|
||||
)
|
||||
.map(k => SUPPORTED_WALLETS[k].name)[0]
|
||||
return <WalletName>{name}</WalletName>
|
||||
return <WalletName>Connected with {name}</WalletName>
|
||||
}
|
||||
|
||||
function getStatusIcon() {
|
||||
if (connector === injected) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<Identicon /> {formatConnectorName()}
|
||||
<Identicon />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} /> {formatConnectorName()}
|
||||
<img src={WalletConnectIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()}
|
||||
<img src={CoinbaseWalletIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === fortmatic) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={FortmaticIcon} alt={''} /> {formatConnectorName()}
|
||||
<img src={FortmaticIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === portis) {
|
||||
return (
|
||||
<>
|
||||
<IconWrapper size={16}>
|
||||
<img src={PortisIcon} alt={''} /> {formatConnectorName()}
|
||||
<img src={PortisIcon} alt={''} />
|
||||
<MainWalletAction
|
||||
onClick={() => {
|
||||
portis.portis.showPortis()
|
||||
@@ -320,10 +303,11 @@ export default function AccountDetails({
|
||||
<YourAccount>
|
||||
<InfoCard>
|
||||
<AccountGroupingRow>
|
||||
{getStatusIcon()}
|
||||
{formatConnectorName()}
|
||||
<div>
|
||||
{connector !== injected && connector !== walletlink && (
|
||||
<WalletAction
|
||||
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
|
||||
onClick={() => {
|
||||
;(connector as any).close()
|
||||
}}
|
||||
@@ -331,71 +315,84 @@ export default function AccountDetails({
|
||||
Disconnect
|
||||
</WalletAction>
|
||||
)}
|
||||
<CircleWrapper>
|
||||
<GreenCircle>
|
||||
<div />
|
||||
</GreenCircle>
|
||||
</CircleWrapper>
|
||||
<WalletAction
|
||||
style={{ fontSize: '.825rem', fontWeight: 400 }}
|
||||
onClick={() => {
|
||||
openOptions()
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</WalletAction>
|
||||
</div>
|
||||
</AccountGroupingRow>
|
||||
<AccountGroupingRow id="web3-account-identifier-row">
|
||||
{ENSName ? (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={true}>
|
||||
<p>{ENSName}</p> <Copy toCopy={account} />
|
||||
</AccountControl>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<p>{account}</p> <Copy toCopy={account} />
|
||||
</AccountControl>
|
||||
</>
|
||||
)}
|
||||
<AccountControl>
|
||||
{ENSName ? (
|
||||
<>
|
||||
<div>
|
||||
{getStatusIcon()}
|
||||
<p> {ENSName}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
{getStatusIcon()}
|
||||
<p> {shortenAddress(account)}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AccountControl>
|
||||
</AccountGroupingRow>
|
||||
<AccountGroupingRow>
|
||||
{ENSName ? (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
View on Etherscan ↗
|
||||
</StyledLink>
|
||||
<AccountControl>
|
||||
<div>
|
||||
<Copy toCopy={account}>
|
||||
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||
</Copy>
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={true}
|
||||
href={getEtherscanLink(chainId, ENSName, 'address')}
|
||||
>
|
||||
<LinkIcon size={16} />
|
||||
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||
</AddressLink>
|
||||
</div>
|
||||
</AccountControl>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(chainId, account, 'address')}>
|
||||
View on Etherscan ↗
|
||||
</StyledLink>
|
||||
<AccountControl>
|
||||
<div>
|
||||
<Copy toCopy={account}>
|
||||
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||
</Copy>
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={false}
|
||||
href={getEtherscanLink(chainId, account, 'address')}
|
||||
>
|
||||
<LinkIcon size={16} />
|
||||
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||
</AddressLink>
|
||||
</div>
|
||||
</AccountControl>
|
||||
</>
|
||||
)}
|
||||
{/* {formatConnectorName()} */}
|
||||
</AccountGroupingRow>
|
||||
</InfoCard>
|
||||
</YourAccount>
|
||||
|
||||
{!(isMobile && (window.web3 || window.ethereum)) && (
|
||||
<ConnectButtonRow>
|
||||
<ButtonEmpty
|
||||
style={{ fontWeight: 400 }}
|
||||
padding={'12px'}
|
||||
width={'260px'}
|
||||
onClick={() => {
|
||||
openOptions()
|
||||
}}
|
||||
>
|
||||
Connect to a different wallet
|
||||
</ButtonEmpty>
|
||||
</ConnectButtonRow>
|
||||
)}
|
||||
</AccountSection>
|
||||
</UpperSection>
|
||||
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
||||
<LowerSection>
|
||||
<AutoRow style={{ justifyContent: 'space-between' }}>
|
||||
<AutoRow mb={'1rem'} style={{ justifyContent: 'space-between' }}>
|
||||
<TYPE.body>Recent Transactions</TYPE.body>
|
||||
<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
|
||||
|
||||
@@ -6,7 +6,12 @@ import { RowBetween } from '../Row'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Button as RebassButton, ButtonProps } from 'rebass/styled-components'
|
||||
|
||||
const Base = styled(RebassButton)<{ padding?: string; width?: string; borderRadius?: string }>`
|
||||
const Base = styled(RebassButton)<{
|
||||
padding?: string
|
||||
width?: string
|
||||
borderRadius?: string
|
||||
altDisbaledStyle?: boolean
|
||||
}>`
|
||||
padding: ${({ padding }) => (padding ? padding : '18px')};
|
||||
width: ${({ width }) => (width ? width : '100%')};
|
||||
font-weight: 500;
|
||||
@@ -45,10 +50,12 @@ export const ButtonPrimary = styled(Base)`
|
||||
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.text3}
|
||||
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
|
||||
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -68,6 +75,16 @@ export const ButtonLight = styled(Base)`
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
||||
}
|
||||
:disabled {
|
||||
opacity: 0.4;
|
||||
:hover {
|
||||
cursor: auto;
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonGray = styled(Base)`
|
||||
@@ -180,7 +197,6 @@ export const ButtonEmpty = styled(Base)`
|
||||
export const ButtonWhite = styled(Base)`
|
||||
border: 1px solid #edeef2;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
};
|
||||
color: black;
|
||||
|
||||
&:focus {
|
||||
@@ -228,6 +244,9 @@ const ButtonErrorStyle = styled(Base)`
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
background-color: ${({ theme }) => theme.red1};
|
||||
border: 1px solid ${({ theme }) => theme.red1};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -247,7 +266,7 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProp
|
||||
}
|
||||
}
|
||||
|
||||
export function ButtonDropwdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
export function ButtonDropdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonPrimary {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
@@ -258,7 +277,7 @@ export function ButtonDropwdown({ disabled = false, children, ...rest }: { disab
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropwdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonOutlined {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
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 { CloseIcon, Spinner } from '../../theme/components'
|
||||
import { RowBetween } from '../Row'
|
||||
import { ArrowUpCircle } from 'react-feather'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
@@ -32,56 +30,41 @@ const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
interface ConfirmationModalProps extends RouteComponentProps<{}> {
|
||||
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
`
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
hash: string
|
||||
topContent: () => React.ReactChild
|
||||
bottomContent: () => React.ReactChild
|
||||
attemptingTxn: boolean
|
||||
pendingConfirmation: boolean
|
||||
pendingText: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
function ConfirmationModal({
|
||||
history,
|
||||
export default function ConfirmationModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
hash,
|
||||
topContent,
|
||||
bottomContent,
|
||||
attemptingTxn,
|
||||
pendingConfirmation,
|
||||
hash,
|
||||
pendingText,
|
||||
title = ''
|
||||
}: ConfirmationModalProps) {
|
||||
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])
|
||||
const transactionBroadcast = !!hash
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
{!attemptingTxn ? (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{title}
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
{topContent()}
|
||||
</Section>
|
||||
<BottomSection gap="12px">{bottomContent()}</BottomSection>
|
||||
</Wrapper>
|
||||
) : (
|
||||
// waiting for user to confirm/reject tx _or_ showing info on a tx that has been broadcast
|
||||
if (attemptingTxn || transactionBroadcast) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
@@ -89,37 +72,36 @@ function ConfirmationModal({
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
{pendingConfirmation ? (
|
||||
<Loader size="90px" />
|
||||
) : (
|
||||
{transactionBroadcast ? (
|
||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||
) : (
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
|
||||
)}
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'}
|
||||
{transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'}
|
||||
</Text>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
|
||||
{pendingText}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
{!pendingConfirmation && (
|
||||
|
||||
{transactionBroadcast ? (
|
||||
<>
|
||||
<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>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)}
|
||||
|
||||
{pendingConfirmation && (
|
||||
) : (
|
||||
<Text fontSize={12} color="#565A69" textAlign="center">
|
||||
Confirm this transaction in your wallet
|
||||
</Text>
|
||||
@@ -127,9 +109,25 @@ function ConfirmationModal({
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</Wrapper>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
// confirmation screen
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{title}
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
{topContent()}
|
||||
</Section>
|
||||
<BottomSection gap="12px">{bottomContent()}</BottomSection>
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(ConfirmationModal)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import TokenSearchModal from '../SearchModal/TokenSearchModal'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import SearchModal from '../SearchModal'
|
||||
import { RowBetween } from '../Row'
|
||||
import { TYPE, CursorPointer } from '../../theme'
|
||||
import { Input as NumericalInput } from '../NumericalInput'
|
||||
@@ -49,7 +49,6 @@ const LabelRow = styled.div`
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
padding: 0.75rem 1rem 0 1rem;
|
||||
height: 20px;
|
||||
span:hover {
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => darken(0.2, theme.text2)};
|
||||
@@ -160,6 +159,10 @@ export default function CurrencyInputPanel({
|
||||
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
}, [setModalOpen])
|
||||
|
||||
return (
|
||||
<InputPanel id={id}>
|
||||
<Container hideInput={hideInput}>
|
||||
@@ -236,12 +239,9 @@ export default function CurrencyInputPanel({
|
||||
</InputRow>
|
||||
</Container>
|
||||
{!disableTokenSelect && (
|
||||
<SearchModal
|
||||
<TokenSearchModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={() => {
|
||||
setModalOpen(false)
|
||||
}}
|
||||
filterType="tokens"
|
||||
onDismiss={handleDismissSearch}
|
||||
onTokenSelect={onTokenSelection}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hiddenToken={token?.address}
|
||||
|
||||
@@ -13,7 +13,7 @@ interface DoubleTokenLogoProps {
|
||||
margin?: boolean
|
||||
size?: number
|
||||
a0: string
|
||||
a1: string
|
||||
a1?: string
|
||||
}
|
||||
|
||||
const HigherLogo = styled(TokenLogo)`
|
||||
@@ -28,7 +28,7 @@ export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: D
|
||||
return (
|
||||
<TokenWrapper sizeraw={size} margin={margin}>
|
||||
<HigherLogo address={a0} size={size.toString() + 'px'} />
|
||||
<CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />
|
||||
{a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
|
||||
</TokenWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
70
src/components/Header/VersionSwitch.tsx
Normal file
70
src/components/Header/VersionSwitch.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
const VersionLabel = styled.span<{ enabled: boolean }>`
|
||||
padding: ${({ enabled }) => (enabled ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary3)};
|
||||
}
|
||||
`
|
||||
|
||||
interface VersionToggleProps extends React.ComponentProps<typeof Link> {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
|
||||
border-radius: 16px;
|
||||
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
|
||||
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
|
||||
background: ${({ theme }) => theme.primary5};
|
||||
border: 1px solid ${({ theme }) => theme.primary4};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export function VersionSwitch() {
|
||||
const version = useToggledVersion()
|
||||
const location = useLocation()
|
||||
const query = useParsedQueryString()
|
||||
const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send'
|
||||
|
||||
const toggleDest = useMemo(() => {
|
||||
return versionSwitchAvailable
|
||||
? {
|
||||
...location,
|
||||
search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}`
|
||||
}
|
||||
: location
|
||||
}, [location, query, version, versionSwitchAvailable])
|
||||
|
||||
const handleClick = useCallback(
|
||||
e => {
|
||||
if (!versionSwitchAvailable) e.preventDefault()
|
||||
},
|
||||
[versionSwitchAvailable]
|
||||
)
|
||||
|
||||
return (
|
||||
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
|
||||
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
|
||||
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { Link as HistoryLink } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import Row from '../Row'
|
||||
import Menu from '../Menu'
|
||||
import Web3Status from '../Web3Status'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { WETH, ChainId } from '@uniswap/sdk'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { YellowCard } from '../Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import Logo from '../../assets/svg/logo.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import LogoDark from '../../assets/svg/logo_white.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import { ExternalLink, StyledInternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
import Settings from '../Settings'
|
||||
import Menu from '../Menu'
|
||||
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import Web3Status from '../Web3Status'
|
||||
import { VersionSwitch } from './VersionSwitch'
|
||||
|
||||
const HeaderFrame = styled.div`
|
||||
display: flex;
|
||||
@@ -31,15 +32,12 @@ const HeaderFrame = styled.div`
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
z-index: 2;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
padding: 12px 0 0 0;
|
||||
width: calc(100%);
|
||||
position: relative;
|
||||
`};
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const HeaderElement = styled.div`
|
||||
@@ -122,31 +120,18 @@ const MigrateBanner = styled(AutoColumn)`
|
||||
`};
|
||||
`
|
||||
|
||||
const VersionLabel = styled.span<{ isV2?: boolean }>`
|
||||
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
|
||||
}
|
||||
`
|
||||
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
|
||||
[ChainId.MAINNET]: null,
|
||||
[ChainId.RINKEBY]: 'Rinkeby',
|
||||
[ChainId.ROPSTEN]: 'Ropsten',
|
||||
[ChainId.GÖRLI]: 'Görli',
|
||||
[ChainId.KOVAN]: 'Kovan'
|
||||
}
|
||||
|
||||
const VersionToggle = styled.a`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
const BalanceWrapper = styled.div`
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Header() {
|
||||
@@ -159,13 +144,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/">
|
||||
<StyledInternalLink to="/migrate/v1">
|
||||
<b>migrate your liquidity ↗</b>
|
||||
</Link>
|
||||
</StyledInternalLink>
|
||||
.
|
||||
</MigrateBanner>
|
||||
<RowBetween padding="1rem">
|
||||
@@ -186,33 +171,24 @@ export default function Header() {
|
||||
</TitleText>
|
||||
)}
|
||||
</Title>
|
||||
<TestnetWrapper style={{ pointerEvents: 'auto' }}>
|
||||
{!isMobile && (
|
||||
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
|
||||
<VersionLabel isV2={true}>V2</VersionLabel>
|
||||
<VersionLabel isV2={false}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)}
|
||||
</TestnetWrapper>
|
||||
<TestnetWrapper style={{ pointerEvents: 'auto' }}>{!isMobile && <VersionSwitch />}</TestnetWrapper>
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
|
||||
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
<BalanceWrapper>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
</BalanceWrapper>
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
<div style={{ pointerEvents: 'auto' }}>
|
||||
<Menu />
|
||||
</div>
|
||||
<Settings />
|
||||
<Menu />
|
||||
</HeaderElement>
|
||||
</RowBetween>
|
||||
</HeaderFrame>
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
import React from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
const rotate = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`
|
||||
|
||||
export default function Loader({ size }: { size: string }) {
|
||||
return <SpinnerWrapper src={Circle} alt="loader" size={size} />
|
||||
const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
|
||||
animation: 2s ${rotate} linear infinite;
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
path {
|
||||
stroke: ${({ stroke, theme }) => stroke ?? theme.primary1};
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Takes in custom size and stroke for circle color, default to primary color as fill,
|
||||
* need ...rest for layered styles on top
|
||||
*/
|
||||
export default function Loader({ size = '16px', stroke = null, ...rest }: { size?: string; stroke?: string }) {
|
||||
return (
|
||||
<StyledSVG viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" size={size} stroke={stroke} {...rest}>
|
||||
<path
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</StyledSVG>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ 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,7 +63,7 @@ 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};
|
||||
@@ -77,9 +77,7 @@ const MenuItem = styled(Link)`
|
||||
}
|
||||
`
|
||||
|
||||
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
|
||||
: 'https://github.com/Uniswap/uniswap-frontend'
|
||||
const CODE_LINK = 'https://github.com/Uniswap/uniswap-frontend'
|
||||
|
||||
export default function Menu() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
|
||||
@@ -9,15 +9,16 @@ 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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
@@ -41,7 +42,11 @@ 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 {...rest} />
|
||||
)).attrs({
|
||||
'aria-label': 'dialog'
|
||||
})`
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
border: 1px solid ${({ theme }) => theme.bg1};
|
||||
@@ -95,7 +100,7 @@ interface ModalProps {
|
||||
onDismiss: () => void
|
||||
minHeight?: number | false
|
||||
maxHeight?: number
|
||||
initialFocusRef?: React.Ref<any>
|
||||
initialFocusRef?: React.RefObject<any>
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -145,7 +150,7 @@ export default function Modal({
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile}
|
||||
mobile={true}
|
||||
>
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
@@ -163,11 +168,12 @@ export default function Modal({
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
ariaLabel="test"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
mobile={isMobile ?? undefined}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
@@ -186,20 +192,8 @@ export default function Modal({
|
||||
{transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile ? isMobile : undefined}
|
||||
>
|
||||
<StyledDialogContent
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
mobile={isMobile ? isMobile : undefined}
|
||||
>
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
|
||||
@@ -3,14 +3,13 @@ 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 '../QuestionHelper'
|
||||
|
||||
import { useBodyKeyDown } from '../../hooks'
|
||||
|
||||
const tabOrder = [
|
||||
{
|
||||
path: '/swap',
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Placement } from '@popperjs/core'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
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 {
|
||||
@@ -26,7 +27,6 @@ const fadeOut = keyframes`
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
|
||||
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
|
||||
@@ -45,7 +45,6 @@ const ReferenceElement = styled.div`
|
||||
`
|
||||
|
||||
const Arrow = styled.div`
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 9998;
|
||||
@@ -98,42 +97,39 @@ const Arrow = styled.div`
|
||||
|
||||
export interface PopoverProps {
|
||||
content: React.ReactNode
|
||||
showPopup: boolean
|
||||
show: boolean
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
export default function Popover({ content, showPopup, children }: PopoverProps) {
|
||||
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: 'auto',
|
||||
placement,
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset: [8, 8] } },
|
||||
{ name: 'arrow', options: { element: arrowElement } }
|
||||
]
|
||||
})
|
||||
|
||||
const portal = createPortal(
|
||||
<PopoverContainer show={showPopup} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
{content}
|
||||
<Arrow
|
||||
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||
ref={setArrowElement}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
</PopoverContainer>,
|
||||
document.getElementById('popover-container')
|
||||
)
|
||||
|
||||
useInterval(update, showPopup ? 100 : null)
|
||||
useInterval(update, show ? 100 : null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
|
||||
{portal}
|
||||
<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()
|
||||
|
||||
75
src/components/PositionCard/V1.tsx
Normal file
75
src/components/PositionCard/V1.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
|
||||
import { Text } from 'rebass'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { FixedHeightRow, HoverCard } from './index'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
interface PositionCardProps extends RouteComponentProps<{}> {
|
||||
token: Token
|
||||
V1LiquidityBalance: TokenAmount
|
||||
}
|
||||
|
||||
function V1PositionCard({ token, V1LiquidityBalance, history }: PositionCardProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<DoubleTokenLogo a0={token.address} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
|
||||
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={12}
|
||||
fontWeight={500}
|
||||
ml="0.5rem"
|
||||
px="0.75rem"
|
||||
py="0.25rem"
|
||||
style={{ borderRadius: '1rem' }}
|
||||
backgroundColor={theme.yellow1}
|
||||
color={'black'}
|
||||
>
|
||||
V1
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonSecondary
|
||||
width="68%"
|
||||
onClick={() => {
|
||||
history.push(`/migrate/v1/${V1LiquidityBalance.token.address}`)
|
||||
}}
|
||||
>
|
||||
Migrate
|
||||
</ButtonSecondary>
|
||||
|
||||
<ButtonSecondary
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
width="28%"
|
||||
onClick={() => {
|
||||
history.push(`/remove/v1/${V1LiquidityBalance.token.address}`)
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</ButtonSecondary>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(V1PositionCard)
|
||||
@@ -12,17 +12,18 @@ 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'
|
||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||
import { Dots } from '../swap/styleds'
|
||||
|
||||
const FixedHeightRow = styled(RowBetween)`
|
||||
export const FixedHeightRow = styled(RowBetween)`
|
||||
height: 24px;
|
||||
`
|
||||
|
||||
const HoverCard = styled(Card)`
|
||||
export const HoverCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
|
||||
@@ -72,7 +73,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Your current position
|
||||
Your position
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
@@ -96,7 +97,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
</Text>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token0?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
@@ -111,7 +111,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
</Text>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token1?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
@@ -134,7 +133,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
{!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
@@ -158,7 +157,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
@@ -176,35 +175,33 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
|
||||
<AutoRow justify="center" marginTop={'10px'}>
|
||||
<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,4 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { HelpCircle as Question } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import Tooltip from '../Tooltip'
|
||||
@@ -23,22 +23,15 @@ const QuestionWrapper = styled.div`
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
|
||||
const [showPopup, setShowPopup] = useState<boolean>(false)
|
||||
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} showPopup={showPopup && !disabled}>
|
||||
<QuestionWrapper
|
||||
onClick={() => {
|
||||
setShowPopup(true)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setShowPopup(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowPopup(false)
|
||||
}}
|
||||
>
|
||||
<Tooltip text={text} show={show && !disabled}>
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Question size={16} />
|
||||
</QuestionWrapper>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { COMMON_BASES } from '../../constants'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
|
||||
import { SUGGESTED_BASES } from '../../constants'
|
||||
import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow } from '../Row'
|
||||
@@ -25,7 +27,7 @@ export default function CommonBases({
|
||||
<QuestionHelper text="These tokens are commonly used in pairs." />
|
||||
</AutoRow>
|
||||
<AutoRow gap="10px">
|
||||
{COMMON_BASES[chainId]?.map(token => {
|
||||
{(SUGGESTED_BASES[chainId] ?? []).map((token: Token) => {
|
||||
return (
|
||||
<BaseWrapper
|
||||
gap="6px"
|
||||
|
||||
@@ -25,13 +25,7 @@ export default function PairList({
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
itemSize={54}
|
||||
height={500}
|
||||
itemCount={pairs.length}
|
||||
width="100%"
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
>
|
||||
<FixedSizeList itemSize={56} height={500} itemCount={pairs.length} width="100%" style={{ flex: '1' }}>
|
||||
{({ index, style }) => {
|
||||
const pair = pairs[index]
|
||||
|
||||
|
||||
135
src/components/SearchModal/PairSearchModal.tsx
Normal file
135
src/components/SearchModal/PairSearchModal.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Pair } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs } from '../../state/user/hooks'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, StyledInternalLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { filterPairs } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { pairComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
|
||||
interface PairSearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
}
|
||||
|
||||
function PairSearchModal({ history, isOpen, onDismiss }: PairSearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
return allPairs.sort((a, b): number => {
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
return pairComparator(a, b, balanceA, balanceB)
|
||||
})
|
||||
}, [allPairs, allPairBalances])
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
return filterPairs(sortedPairList, searchQuery)
|
||||
}, [searchQuery, sortedPairList])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
minHeight={70}
|
||||
>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a pool
|
||||
<QuestionHelper text="Find a pair by searching for its name below." />
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={onInput}
|
||||
/>
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Pool Name
|
||||
</Text>
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<PairList
|
||||
pairs={filteredPairs}
|
||||
focusTokenAddress={focusedToken?.address}
|
||||
onAddLiquidity={selectPair}
|
||||
onSelectPair={selectPair}
|
||||
pairBalances={allPairBalances}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
||||
</Text>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(PairSearchModal)
|
||||
@@ -18,19 +18,14 @@ export const FilterWrapper = styled(RowFixed)`
|
||||
`
|
||||
|
||||
export default function SortButton({
|
||||
title,
|
||||
toggleSortOrder,
|
||||
ascending
|
||||
}: {
|
||||
title: string
|
||||
toggleSortOrder: () => void
|
||||
ascending: boolean
|
||||
}) {
|
||||
return (
|
||||
<FilterWrapper onClick={toggleSortOrder}>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{ascending ? '↑' : '↓'}
|
||||
</Text>
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { Link as StyledLink, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { FadedSpan, GreySpan, MenuItem, SpinnerWrapper, ModalInfo } from './styleds'
|
||||
|
||||
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
||||
const address = isAddress(tokenAddress)
|
||||
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
||||
}
|
||||
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
import { isDefaultToken, isCustomAddedToken } from '../../utils'
|
||||
|
||||
export default function TokenList({
|
||||
tokens,
|
||||
@@ -27,14 +23,12 @@ export default function TokenList({
|
||||
onTokenSelect,
|
||||
otherToken,
|
||||
showSendWithSwap,
|
||||
onRemoveAddedToken,
|
||||
otherSelectedText
|
||||
}: {
|
||||
tokens: Token[]
|
||||
selectedToken: string
|
||||
allTokenBalances: { [tokenAddress: string]: TokenAmount }
|
||||
onTokenSelect: (tokenAddress: string) => void
|
||||
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
|
||||
otherToken: string
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedText: string
|
||||
@@ -42,22 +36,29 @@ export default function TokenList({
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const allTokens = useAllTokens()
|
||||
const addToken = useAddUserToken()
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return <ModalInfo>{t('noToken')}</ModalInfo>
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={500}
|
||||
itemCount={tokens.length}
|
||||
itemSize={50}
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
itemSize={56}
|
||||
style={{ flex: '1' }}
|
||||
itemKey={index => tokens[index].address}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const { address, symbol } = tokens[index]
|
||||
const token = tokens[index]
|
||||
const { address, symbol } = token
|
||||
|
||||
const customAdded = !isDefaultToken(address, chainId)
|
||||
const isDefault = isDefaultToken(token)
|
||||
const customAdded = isCustomAddedToken(allTokens, token)
|
||||
const balance = allTokenBalances[address]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
@@ -79,21 +80,36 @@ export default function TokenList({
|
||||
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||
{customAdded && (
|
||||
<div
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
onRemoveAddedToken(chainId, address)
|
||||
}}
|
||||
>
|
||||
<StyledLink style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</StyledLink>
|
||||
</div>
|
||||
)}
|
||||
{customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Added by user
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
removeToken(chainId, address)
|
||||
}}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
{!isDefault && !customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Found by address
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
addToken(token)
|
||||
}}
|
||||
>
|
||||
(Add)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
</FadedSpan>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
<AutoColumn gap="4px" justify="end">
|
||||
<AutoColumn>
|
||||
{balance ? (
|
||||
<Text>
|
||||
{zeroBalance && showSendWithSwap ? (
|
||||
@@ -109,7 +125,7 @@ export default function TokenList({
|
||||
)}
|
||||
</Text>
|
||||
) : account ? (
|
||||
<SpinnerWrapper src={Circle} alt="loader" />
|
||||
<Loader />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
|
||||
185
src/components/SearchModal/TokenSearchModal.tsx
Normal file
185
src/components/SearchModal/TokenSearchModal.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterTokens } from './filtering'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface TokenSearchModalProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
export default function TokenSearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: TokenSearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
const searchToken = useToken(searchQuery)
|
||||
const searchTokenBalance = useTokenBalanceTreatingWETHasETH(account, searchToken)
|
||||
const allTokenBalances_ = useAllTokenBalancesTreatingWETHasETH()
|
||||
const allTokenBalances = searchToken
|
||||
? {
|
||||
[searchToken.address]: searchTokenBalance
|
||||
}
|
||||
: allTokenBalances_ ?? {}
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [searchToken, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
...(searchToken ? [searchToken] : []),
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
|
||||
|
||||
const handleTokenSelect = useCallback(
|
||||
(address: string) => {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
},
|
||||
[onDismiss, onTokenSelect]
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const handleInput = useCallback(event => {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
setTooltipOpen(false)
|
||||
}, [])
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
minHeight={70}
|
||||
>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text="Find a token by searching for its name or symbol or by pasting its address below."
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={handleInput}
|
||||
onBlur={closeTooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<TokenList
|
||||
tokens={filteredSortedTokens}
|
||||
allTokenBalances={allTokenBalances}
|
||||
onTokenSelect={handleTokenSelect}
|
||||
otherSelectedText={otherSelectedText}
|
||||
otherToken={otherSelectedTokenAddress}
|
||||
selectedToken={hiddenToken}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -10,12 +10,22 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
|
||||
return tokens.filter(token => token.address === searchingAddress)
|
||||
}
|
||||
|
||||
const lowerSearchParts = searchingAddress ? [] : search.toLowerCase().split(/\s+/)
|
||||
const lowerSearchParts = search
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
|
||||
if (lowerSearchParts.length === 0) {
|
||||
return tokens
|
||||
}
|
||||
|
||||
const matchesSearch = (s: string): boolean => {
|
||||
const sParts = s.toLowerCase().split(/\s+/)
|
||||
const sParts = s
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
|
||||
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p)))
|
||||
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p) || sp.endsWith(p)))
|
||||
}
|
||||
|
||||
return tokens.filter(token => {
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, Link as StyledLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterPairs, filterTokens } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { balanceComparator, useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface SearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
filterType?: 'tokens'
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
function SearchModal({
|
||||
history,
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
filterType,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: SearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const isTokenView = filterType === 'tokens'
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH()[account] ?? {}
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
|
||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const sortedTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return Object.values(allTokens).sort(tokenComparator)
|
||||
}, [allTokens, isTokenView, tokenComparator])
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return filterTokens(sortedTokens, searchQuery)
|
||||
}, [isTokenView, sortedTokens, searchQuery])
|
||||
|
||||
function _onTokenSelect(address: string) {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return allPairs.sort((a, b): number => {
|
||||
// sort by balance
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
|
||||
return balanceComparator(balanceA, balanceB)
|
||||
})
|
||||
}, [isTokenView, allPairs, allPairBalances])
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return filterPairs(sortedPairList, searchQuery)
|
||||
}, [isTokenView, searchQuery, sortedPairList])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{isTokenView ? 'Select a token' : 'Select a pool'}
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text={
|
||||
isTokenView
|
||||
? 'Find a token by searching for its name or symbol or by pasting its address below.'
|
||||
: 'Find a pair by searching for its name below.'
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
showPopup={tooltipOpen}
|
||||
>
|
||||
<SearchInput
|
||||
type={'text'}
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={onInput}
|
||||
onBlur={closeTooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{isTokenView ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
{isTokenView && (
|
||||
<SortButton
|
||||
ascending={invertSearchOrder}
|
||||
toggleSortOrder={() => setInvertSearchOrder(iso => !iso)}
|
||||
title="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 ? (
|
||||
<Text fontWeight={500} color={theme.text2} fontSize={14}>
|
||||
<StyledLink onClick={openTooltip}>Having trouble importing a token?</StyledLink>
|
||||
</Text>
|
||||
) : (
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledLink
|
||||
onClick={() => {
|
||||
history.push('/find')
|
||||
}}
|
||||
>
|
||||
{!isMobile ? 'Import it.' : 'Import pool.'}
|
||||
</StyledLink>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(SearchModal)
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
|
||||
|
||||
// compare two token amounts with highest one coming first
|
||||
export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
if (balanceA && balanceB) {
|
||||
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
|
||||
} else if (balanceA && balanceA.greaterThan('0')) {
|
||||
@@ -15,6 +16,26 @@ export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount
|
||||
return 0
|
||||
}
|
||||
|
||||
// compare two pairs, favoring "pinned" pairs, and falling back to balances
|
||||
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
const aShouldBePinned =
|
||||
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
|
||||
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
|
||||
) ?? false
|
||||
const bShouldBePinned =
|
||||
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
|
||||
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
|
||||
) ?? false
|
||||
|
||||
if (aShouldBePinned && !bShouldBePinned) {
|
||||
return -1
|
||||
} else if (!aShouldBePinned && bShouldBePinned) {
|
||||
return 1
|
||||
} else {
|
||||
return balanceComparator(balanceA, balanceB)
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenComparator(
|
||||
weth: Token | undefined,
|
||||
balances: { [tokenAddress: string]: TokenAmount }
|
||||
@@ -42,10 +63,10 @@ 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()
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances[account] ?? {}), [account, balances, weth])
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
|
||||
return useMemo(() => {
|
||||
if (inverted) {
|
||||
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import styled from 'styled-components'
|
||||
import { Spinner } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
|
||||
@@ -9,8 +8,8 @@ export const ModalInfo = styled.div`
|
||||
padding: 1rem 1rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
min-height: 200px;
|
||||
`
|
||||
|
||||
export const FadedSpan = styled(RowFixed)`
|
||||
@@ -23,12 +22,6 @@ export const GreySpan = styled.span`
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
export const SpinnerWrapper = styled(Spinner)`
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
color: ${({ theme }) => theme.text4};
|
||||
opacity: 0.6;
|
||||
`
|
||||
|
||||
export const Input = styled.input`
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -57,12 +50,9 @@ export const PaddedColumn = styled(AutoColumn)`
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
const PaddedItem = styled(RowBetween)`
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(PaddedItem)`
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
|
||||
253
src/components/Settings/index.tsx
Normal file
253
src/components/Settings/index.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React, { useRef, useEffect, useContext, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import {
|
||||
useUserSlippageTolerance,
|
||||
useExpertModeManager,
|
||||
useUserDeadline,
|
||||
useDarkModeManager
|
||||
} from '../../state/user/hooks'
|
||||
import SlippageTabs from '../SlippageTabs'
|
||||
import { RowFixed, RowBetween } from '../Row'
|
||||
import { TYPE } from '../../theme'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Toggle from '../Toggle'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ButtonError } from '../Button'
|
||||
import { useSettingsMenuOpen, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { Text } from 'rebass'
|
||||
import Modal from '../Modal'
|
||||
|
||||
const StyledMenuIcon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledCloseIcon = styled(X)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
const EmojiWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
right: 0px;
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const MenuFlyout = styled.span`
|
||||
min-width: 20.125rem;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
right: 0rem;
|
||||
z-index: 100;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
min-width: 18.125rem;
|
||||
right: -46px;
|
||||
`};
|
||||
`
|
||||
|
||||
const Break = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
const ModalContentWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
export default function SettingsTab() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const open = useSettingsMenuOpen()
|
||||
const toggle = useToggleSettingsMenu()
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
const [userSlippageTolerance, setUserslippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
const [deadline, setDeadline] = useUserDeadline()
|
||||
|
||||
const [expertMode, toggleExpertMode] = useExpertModeManager()
|
||||
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = e => {
|
||||
if (node.current?.contains(e.target) ?? false) {
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [open, toggle])
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)}>
|
||||
<ModalContentWrapper>
|
||||
<AutoColumn gap="lg">
|
||||
<RowBetween style={{ padding: '0 2rem' }}>
|
||||
<div />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Are you sure?
|
||||
</Text>
|
||||
<StyledCloseIcon onClick={() => setShowConfirmation(false)} />
|
||||
</RowBetween>
|
||||
<Break />
|
||||
<AutoColumn gap="lg" style={{ padding: '0 2rem' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Expert mode turns off the confirm transaction prompt and allows high slippage trades that often result
|
||||
in bad rates and lost funds.
|
||||
</Text>
|
||||
<Text fontWeight={600} fontSize={20}>
|
||||
ONLY USE THIS MODE IF YOU KNOW WHAT YOU ARE DOING.
|
||||
</Text>
|
||||
<ButtonError
|
||||
error={true}
|
||||
padding={'12px'}
|
||||
onClick={() => {
|
||||
if (window.prompt(`Please type the word "confirm" to enable expert mode.`) === 'confirm') {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
Turn On Expert Mode
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</ModalContentWrapper>
|
||||
</Modal>
|
||||
<StyledMenuButton onClick={toggle}>
|
||||
<StyledMenuIcon />
|
||||
{expertMode && (
|
||||
<EmojiWrapper>
|
||||
<span role="img" aria-label="wizard-icon">
|
||||
🧙
|
||||
</span>
|
||||
</EmojiWrapper>
|
||||
)}
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<MenuFlyout>
|
||||
<AutoColumn gap="md" style={{ padding: '1rem' }}>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Transaction Settings
|
||||
</Text>
|
||||
<SlippageTabs
|
||||
rawSlippage={userSlippageTolerance}
|
||||
setRawSlippage={setUserslippageTolerance}
|
||||
deadline={deadline}
|
||||
setDeadline={setDeadline}
|
||||
/>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Interface Settings
|
||||
</Text>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Expert Mode
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Bypasses confirmation modals and allows high slippage trades. Use at your own risk." />
|
||||
</RowFixed>
|
||||
<Toggle
|
||||
isActive={expertMode}
|
||||
toggle={
|
||||
expertMode
|
||||
? () => {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
: () => setShowConfirmation(true)
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Dark Mode
|
||||
</TYPE.black>
|
||||
</RowFixed>
|
||||
<Toggle isActive={darkMode} toggle={toggleDarkMode} />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</MenuFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
)
|
||||
}
|
||||
@@ -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 '../QuestionHelper'
|
||||
import { Text } from 'rebass'
|
||||
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 }>`
|
||||
@@ -85,16 +78,6 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
|
||||
}
|
||||
`
|
||||
|
||||
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 +85,153 @@ 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 (
|
||||
<>
|
||||
<SlippageSelector>
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
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>
|
||||
</SlippageSelector>
|
||||
{slippageError === SlippageError.InvalidInput
|
||||
? 'Enter a valid slippage percentage'
|
||||
: slippageError === SlippageError.RiskyLow
|
||||
? 'Your transaction may fail'
|
||||
: 'Your transaction may be frontrun'}
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontSize={14} color={theme.text2}>
|
||||
Deadline
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Transaction deadline
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="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' }}>
|
||||
<RowFixed>
|
||||
<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}
|
||||
/>
|
||||
@@ -352,6 +241,6 @@ export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, se
|
||||
</TYPE.body>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
</>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
41
src/components/Toggle/index.tsx
Normal file
41
src/components/Toggle/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')};
|
||||
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
const StyledToggle = styled.a<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Toggle({ isActive, toggle }: ToggleProps) {
|
||||
return (
|
||||
<StyledToggle isActive={isActive} target="_self" onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} isOnSwitch={true}>
|
||||
On
|
||||
</ToggleElement>
|
||||
<ToggleElement isActive={!isActive} isOnSwitch={false}>
|
||||
Off
|
||||
</ToggleElement>
|
||||
</StyledToggle>
|
||||
)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,13 +3,12 @@ import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { Link, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink, isDefaultToken } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
@@ -18,11 +17,11 @@ const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)};
|
||||
/* border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)}; */
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-rows: 14px auto auto;
|
||||
grid-row-gap: 14px;
|
||||
`
|
||||
|
||||
@@ -42,15 +41,15 @@ const CloseColor = styled(Close)`
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 14px;
|
||||
top: 12px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
& > * {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -68,9 +67,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'
|
||||
|
||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const isDefaultToken = Boolean(
|
||||
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address]
|
||||
)
|
||||
|
||||
const isDefault = isDefaultToken(token)
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
@@ -80,7 +78,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (isDefaultToken || !token || !chainId) return false
|
||||
if (isDefault || !token || !chainId) return false
|
||||
|
||||
return Object.keys(allTokens).some(tokenAddress => {
|
||||
const userToken = allTokens[tokenAddress]
|
||||
@@ -89,9 +87,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
}
|
||||
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
|
||||
})
|
||||
}, [isDefaultToken, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (isDefaultToken || !token || dismissed) return null
|
||||
if (isDefault || !token || dismissed) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||
@@ -111,9 +109,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>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
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'
|
||||
@@ -51,18 +51,21 @@ export default function TxnPopup({
|
||||
isRunning ? delay : null
|
||||
)
|
||||
|
||||
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
|
||||
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
|
||||
{success ? (
|
||||
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} />
|
||||
) : (
|
||||
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
|
||||
)}
|
||||
<AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||
</div>
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>
|
||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
||||
</TYPE.body>
|
||||
<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
|
||||
|
||||
@@ -5,9 +5,8 @@ import Option from './Option'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import WalletConnectData from './WalletConnectData'
|
||||
import { walletconnect, injected } from '../../connectors'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { darken } from 'polished'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const PendingSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -19,14 +18,8 @@ const PendingSection = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)`
|
||||
font-size: 4rem;
|
||||
const StyledLoader = styled(Loader)`
|
||||
margin-right: 1rem;
|
||||
svg {
|
||||
path {
|
||||
color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div<{ error?: boolean }>`
|
||||
@@ -93,7 +86,7 @@ export default function PendingView({
|
||||
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
|
||||
<LoadingMessage error={error}>
|
||||
<LoadingWrapper>
|
||||
{!error && <SpinnerWrapper src={Circle} />}
|
||||
{!error && <StyledLoader />}
|
||||
{error ? (
|
||||
<ErrorGroup>
|
||||
<div>Error connecting.</div>
|
||||
|
||||
@@ -10,11 +10,15 @@ const QRCodeWrapper = styled.div`
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const StyledQRCode = styled(QRCode)`
|
||||
border: 3px solid white;
|
||||
`
|
||||
|
||||
interface WalletConnectDataProps {
|
||||
uri?: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
|
||||
return <QRCodeWrapper>{uri && <QRCode size={size} value={uri} />}</QRCodeWrapper>
|
||||
return <QRCodeWrapper>{uri && <StyledQRCode size={size} value={uri} />}</QRCodeWrapper>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { network } from '../../connectors'
|
||||
import { useEagerConnect, useInactiveListener } from '../../hooks'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const MessageWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -20,16 +19,6 @@ const Message = styled.h2`
|
||||
color: ${({ theme }) => theme.secondary1};
|
||||
`
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)`
|
||||
font-size: 4rem;
|
||||
|
||||
svg {
|
||||
path {
|
||||
color: ${({ theme }) => theme.secondary1};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Web3ReactManager({ children }) {
|
||||
const { t } = useTranslation()
|
||||
const { active } = useWeb3React()
|
||||
@@ -45,20 +34,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)
|
||||
|
||||
@@ -92,7 +67,7 @@ export default function Web3ReactManager({ children }) {
|
||||
if (!active && !networkActive) {
|
||||
return showLoader ? (
|
||||
<MessageWrapper>
|
||||
<SpinnerWrapper src={Circle} />
|
||||
<Loader />
|
||||
</MessageWrapper>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -15,19 +16,12 @@ import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
|
||||
import { Spinner } from '../../theme'
|
||||
import LightCircle from '../../assets/svg/lightcircle.svg'
|
||||
|
||||
import { RowBetween } from '../Row'
|
||||
import { useENSName } from '../../hooks'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)`
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
`
|
||||
import Loader from '../Loader'
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -189,7 +183,7 @@ export default function Web3Status() {
|
||||
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>{pending?.length} Pending</Text> <SpinnerWrapper src={LightCircle} alt="loader" />
|
||||
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
|
||||
</RowBetween>
|
||||
) : (
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
|
||||
@@ -1,54 +1,35 @@
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronUp, ChevronRight } from 'react-feather'
|
||||
import { Text, Flex } from 'rebass'
|
||||
import { ChevronRight } from 'react-feather'
|
||||
import { Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { SectionBreak } from './styleds'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
|
||||
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}>
|
||||
@@ -80,19 +61,27 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<SectionBreak />
|
||||
export interface AdvancedSwapDetailsProps {
|
||||
trade?: Trade
|
||||
}
|
||||
|
||||
<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} />
|
||||
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
{trade.route.path.length > 2 && (
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const showRoute = trade?.route?.path?.length > 2
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
|
||||
{showRoute && <SectionBreak />}
|
||||
{showRoute && (
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
@@ -109,27 +98,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,47 +1,30 @@
|
||||
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 React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||
import { PriceSlippageWarningCard } from './PriceSlippageWarningCard'
|
||||
import { AdvancedDropwdown, FixedBottom } 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)
|
||||
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
|
||||
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
|
||||
transition: transform 300ms ease-in-out;
|
||||
`
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
|
||||
const lastTrade = useLast(trade)
|
||||
|
||||
return (
|
||||
<AdvancedDropwdown>
|
||||
{showAdvanced ? (
|
||||
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
|
||||
) : (
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={() => setShowAdvanced(true)} padding={'8px 20px'} id="show-advanced">
|
||||
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Show Advanced
|
||||
</Text>
|
||||
<ChevronDown color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
)}
|
||||
<FixedBottom>
|
||||
<AutoColumn gap="lg">
|
||||
{severity > 2 && <PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />}
|
||||
</AutoColumn>
|
||||
</FixedBottom>
|
||||
</AdvancedDropwdown>
|
||||
<AdvancedDetailsFooter show={Boolean(trade)}>
|
||||
<AdvancedSwapDetails {...rest} trade={lastTrade} />
|
||||
</AdvancedDetailsFooter>
|
||||
)
|
||||
}
|
||||
|
||||
40
src/components/swap/BetterTradeLink.tsx
Normal file
40
src/components/swap/BetterTradeLink.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { useLocation } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
import { StyledInternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function BetterTradeLink({ version }: { version: Version }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const location = useLocation()
|
||||
const search = useParsedQueryString()
|
||||
|
||||
const linkDestination = useMemo(() => {
|
||||
return {
|
||||
...location,
|
||||
search: `?${stringify({
|
||||
...search,
|
||||
use: version !== DEFAULT_VERSION ? version : undefined
|
||||
})}`
|
||||
}
|
||||
}, [location, search, version])
|
||||
|
||||
return (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on{' '}
|
||||
<StyledInternalLink to={linkDestination}>
|
||||
<b>Uniswap {version.toUpperCase()} ↗</b>
|
||||
</StyledInternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -1,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>
|
||||
)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
export function PriceSlippageWarningCard({ priceSlippage }: { priceSlippage: Percent }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<YellowCard style={{ padding: '20px', paddingTop: '10px' }}>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<RowFixed style={{ paddingTop: '8px' }}>
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>{' '}
|
||||
<Text fontWeight={500} marginLeft="4px" color={theme.text1}>
|
||||
Price Warning
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<Text lineHeight="145.23%;" fontSize={16} fontWeight={400} color={theme.text1}>
|
||||
This trade will move the price by ~{priceSlippage.toFixed(2)}%.
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -37,6 +37,11 @@ export default function SwapModalFooter({
|
||||
confirmText: string
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
if (!trade) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoColumn gap="0px">
|
||||
@@ -66,9 +71,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}>
|
||||
|
||||
@@ -11,29 +11,30 @@ import TokenLogo from '../TokenLogo'
|
||||
import { TruncatedText } from './styleds'
|
||||
|
||||
export default function SwapModalHeader({
|
||||
formattedAmounts,
|
||||
tokens,
|
||||
formattedAmounts,
|
||||
slippageAdjustedAmounts,
|
||||
priceImpactSeverity,
|
||||
independentField
|
||||
}: {
|
||||
formattedAmounts?: { [field in Field]?: string }
|
||||
tokens?: { [field in Field]?: Token }
|
||||
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
|
||||
tokens: { [field in Field]?: Token }
|
||||
formattedAmounts: { [field in Field]?: string }
|
||||
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
|
||||
priceImpactSeverity: number
|
||||
independentField: Field
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
|
||||
<RowBetween align="flex-end">
|
||||
<TruncatedText fontSize={24} fontWeight={500}>
|
||||
{!!formattedAmounts[Field.INPUT] && formattedAmounts[Field.INPUT]}
|
||||
{formattedAmounts[Field.INPUT]}
|
||||
</TruncatedText>
|
||||
<RowFixed gap="4px">
|
||||
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{tokens[Field.INPUT]?.symbol || ''}
|
||||
{tokens[Field.INPUT]?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
@@ -42,12 +43,12 @@ export default function SwapModalHeader({
|
||||
</RowFixed>
|
||||
<RowBetween align="flex-end">
|
||||
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
|
||||
{!!formattedAmounts[Field.OUTPUT] && formattedAmounts[Field.OUTPUT]}
|
||||
{formattedAmounts[Field.OUTPUT]}
|
||||
</TruncatedText>
|
||||
<RowFixed gap="4px">
|
||||
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{tokens[Field.OUTPUT]?.symbol || ''}
|
||||
{tokens[Field.OUTPUT]?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
@@ -56,7 +57,7 @@ export default function SwapModalHeader({
|
||||
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
|
||||
{`Output is estimated. You will receive at least `}
|
||||
<b>
|
||||
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}{' '}
|
||||
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}
|
||||
</b>
|
||||
{' or the transaction will revert.'}
|
||||
</TYPE.italic>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Trade } from '@uniswap/sdk'
|
||||
import { Price, Token } from '@uniswap/sdk'
|
||||
import { useContext } from 'react'
|
||||
import { Repeat } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -7,20 +7,19 @@ import { ThemeContext } from 'styled-components'
|
||||
import { StyledBalanceMaxMini } from './styleds'
|
||||
|
||||
interface TradePriceProps {
|
||||
trade?: Trade
|
||||
price?: Price
|
||||
inputToken?: Token
|
||||
outputToken?: Token
|
||||
showInverted: boolean
|
||||
setShowInverted: (showInverted: boolean) => void
|
||||
}
|
||||
|
||||
export default function TradePrice({ trade, showInverted, setShowInverted }: TradePriceProps) {
|
||||
export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const inputToken = trade?.inputAmount?.token
|
||||
const outputToken = trade?.outputAmount?.token
|
||||
|
||||
const price = showInverted
|
||||
? trade?.executionPrice?.toSignificant(6)
|
||||
: trade?.executionPrice?.invert()?.toSignificant(6)
|
||||
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
|
||||
|
||||
const show = Boolean(inputToken && outputToken)
|
||||
const label = showInverted
|
||||
? `${outputToken?.symbol} per ${inputToken?.symbol}`
|
||||
: `${inputToken?.symbol} per ${outputToken?.symbol}`
|
||||
@@ -32,10 +31,16 @@ export default function TradePrice({ trade, showInverted, setShowInverted }: Tra
|
||||
color={theme.text2}
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
>
|
||||
{price && `${price} ${label}`}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
{show ? (
|
||||
<>
|
||||
{formattedPrice ?? '-'} {label}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBetter: string }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return v1TradeLinkIfBetter ? (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on
|
||||
<Link href={v1TradeLinkIfBetter}>
|
||||
<b> Uniswap V1 ↗</b>
|
||||
</Link>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null
|
||||
}
|
||||
@@ -21,29 +21,6 @@ export const ArrowWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export const FixedBottom = styled.div`
|
||||
position: absolute;
|
||||
margin-top: 1.5rem;
|
||||
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;
|
||||
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;
|
||||
`
|
||||
|
||||
export const SectionBreak = styled.div`
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
@@ -55,9 +32,15 @@ export const BottomGrouping = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
|
||||
color: ${({ theme, severity }) =>
|
||||
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.green1 : theme.text1};
|
||||
severity === 3 || severity === 4
|
||||
? theme.red1
|
||||
: severity === 2
|
||||
? theme.yellow2
|
||||
: severity === 1
|
||||
? theme.text1
|
||||
: theme.green1};
|
||||
`
|
||||
|
||||
export const InputGroup = styled(AutoColumn)`
|
||||
@@ -75,7 +58,7 @@ export const StyledNumerical = styled(NumericalInput)`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
export const StyledBalanceMaxMini = styled.button<{ active?: boolean }>`
|
||||
export const StyledBalanceMaxMini = styled.button`
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
|
||||
@@ -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": ["**/*"]
|
||||
}
|
||||
10
src/constants/abis/erc20.ts
Normal file
10
src/constants/abis/erc20.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import ERC20_ABI from './erc20.json'
|
||||
import ERC20_BYTES32_ABI from './erc20_bytes32.json'
|
||||
|
||||
const ERC20_INTERFACE = new Interface(ERC20_ABI)
|
||||
|
||||
const ERC20_BYTES32_INTERFACE = new Interface(ERC20_BYTES32_ABI)
|
||||
|
||||
export default ERC20_INTERFACE
|
||||
export { ERC20_ABI, ERC20_BYTES32_INTERFACE, ERC20_BYTES32_ABI }
|
||||
@@ -3,56 +3,12 @@
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [{ "name": "", "type": "bytes32" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
|
||||
"name": "approve",
|
||||
"outputs": [{ "name": "", "type": "bool" }],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{ "name": "_from", "type": "address" },
|
||||
{ "name": "_to", "type": "address" },
|
||||
{ "name": "_value", "type": "uint256" }
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [{ "name": "", "type": "bool" }],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [{ "name": "", "type": "uint8" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{ "name": "_owner", "type": "address" }],
|
||||
"name": "balanceOf",
|
||||
"outputs": [{ "name": "balance", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
@@ -61,48 +17,14 @@
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [{ "name": "", "type": "bytes32" }],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
|
||||
"name": "transfer",
|
||||
"outputs": [{ "name": "", "type": "bool" }],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
|
||||
"name": "allowance",
|
||||
"outputs": [{ "name": "", "type": "uint256" }],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "owner", "type": "address" },
|
||||
{ "indexed": true, "name": "spender", "type": "address" },
|
||||
{ "indexed": false, "name": "value", "type": "uint256" }
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{ "indexed": true, "name": "from", "type": "address" },
|
||||
{ "indexed": true, "name": "to", "type": "address" },
|
||||
{ "indexed": false, "name": "value", "type": "uint256" }
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
||||
|
||||
55
src/constants/abis/migrator.json
Normal file
55
src/constants/abis/migrator.json
Normal file
@@ -0,0 +1,55 @@
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_factoryV1",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_router",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountTokenMin",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amountETHMin",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "migrate",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"stateMutability": "payable",
|
||||
"type": "receive"
|
||||
}
|
||||
]
|
||||
5
src/constants/abis/migrator.ts
Normal file
5
src/constants/abis/migrator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import MIGRATOR_ABI from './migrator.json'
|
||||
|
||||
const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b'
|
||||
|
||||
export { MIGRATOR_ADDRESS, MIGRATOR_ABI }
|
||||
@@ -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 = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
|
||||
|
||||
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,
|
||||
@@ -114,12 +151,13 @@ export const ONE_BIPS = new Percent(JSBI.BigInt(1), JSBI.BigInt(10000))
|
||||
export const BIPS_BASE = JSBI.BigInt(10000)
|
||||
// used for warning states
|
||||
export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1%
|
||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||
|
||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(300), BIPS_BASE) // 3%
|
||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||
// if the price slippage exceeds this number, force the user to type 'confirm' to execute
|
||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(2500), BIPS_BASE) // 25%
|
||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||
// for non expert mode disable swaps above this
|
||||
export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BIPS_BASE) // 15%
|
||||
|
||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||
export const V1_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
|
||||
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'),
|
||||
@@ -76,6 +79,9 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
|
||||
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
|
||||
new Token(ChainId.MAINNET, '0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf', 8, 'renBCH', 'renBCH'),
|
||||
new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC'),
|
||||
new Token(ChainId.MAINNET, '0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2', 8, 'renZEC', 'renZEC'),
|
||||
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REP', 'Reputation'),
|
||||
new Token(ChainId.MAINNET, '0x9469D013805bFfB7D3DEBe5E7839237e535ec483', 18, 'RING', 'Darwinia Network Native Token'),
|
||||
new Token(ChainId.MAINNET, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),
|
||||
@@ -90,6 +96,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'),
|
||||
|
||||
17
src/constants/v1/index.ts
Normal file
17
src/constants/v1/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import V1_EXCHANGE_ABI from './v1_exchange.json'
|
||||
import V1_FACTORY_ABI from './v1_factory.json'
|
||||
|
||||
const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {
|
||||
[ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
|
||||
[ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
|
||||
[ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
|
||||
[ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',
|
||||
[ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
|
||||
}
|
||||
|
||||
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
|
||||
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
|
||||
|
||||
export { V1_FACTORY_ADDRESSES, 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,38 +1,43 @@
|
||||
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'
|
||||
import { useMultipleContractSingleData } from '../state/multicall/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
|
||||
})
|
||||
}
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
|
||||
/*
|
||||
* if loading, return undefined
|
||||
* if no pair created yet, return null
|
||||
* if pair already created (even if 0 reserves), return pair
|
||||
*/
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
const pairAddress = !!tokenA && !!tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const contract = usePairContract(pairAddress, false)
|
||||
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 shouldFetch = !!contract
|
||||
const key = shouldFetch ? [pairAddress, tokenA.chainId, SWRKeys.Reserves] : null
|
||||
const { data, mutate } = useSWR(key, getReserves(contract, tokenA, tokenB))
|
||||
useKeepSWRDataLiveAsBlocksArrive(mutate)
|
||||
const results = useMultipleContractSingleData(pairAddresses, PAIR_INTERFACE, 'getReserves')
|
||||
|
||||
return data
|
||||
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])
|
||||
}
|
||||
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
return usePairs([[tokenA, tokenB]])[0]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
187
src/data/V1.ts
187
src/data/V1.ts
@@ -1,66 +1,99 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Token, TokenAmount, Pair, Trade, ChainId, WETH, Route, TradeType, Percent } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { 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 { Version } from '../hooks/useToggledVersion'
|
||||
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
|
||||
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
|
||||
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
|
||||
const contract = useV1FactoryContract()
|
||||
|
||||
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) {
|
||||
const isWETH = token?.equals(WETH[token?.chainId])
|
||||
class MockV1Pair extends Pair {}
|
||||
|
||||
// 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]
|
||||
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
||||
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
|
||||
|
||||
return tokenBalance && ETHBalance
|
||||
? new Pair(tokenBalance, new TokenAmount(WETH[token?.chainId], ETHBalance.toString()))
|
||||
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
|
||||
const tokenBalance = useTokenBalance(v1PairAddress, token)
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
||||
|
||||
return tokenBalance && ETHBalance && token
|
||||
? new MockV1Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
|
||||
: undefined
|
||||
}
|
||||
|
||||
export function useV1TradeLinkIfBetter(
|
||||
// returns all v1 exchange addresses in the user's token list
|
||||
export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
|
||||
const allTokens = useAllTokens()
|
||||
const factory = useV1FactoryContract()
|
||||
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
|
||||
|
||||
const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
|
||||
if (result?.[0] && result[0] !== AddressZero) {
|
||||
const token = allTokens[args[ix][0]]
|
||||
memo[result[0]] = token
|
||||
}
|
||||
return memo
|
||||
}, {}) ?? {},
|
||||
[allTokens, args, data]
|
||||
)
|
||||
}
|
||||
|
||||
// returns whether any of the tokens in the user's token list have liquidity on v1
|
||||
export function useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const fakeLiquidityTokens = useMemo(
|
||||
() =>
|
||||
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
|
||||
[chainId, exchanges]
|
||||
)
|
||||
|
||||
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.keys(balances).some(tokenAddress => {
|
||||
const b = balances[tokenAddress]?.raw
|
||||
return b && JSBI.greaterThan(b, JSBI.BigInt(0))
|
||||
}),
|
||||
[balances]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the trade to execute on V1 to go between input and output token
|
||||
*/
|
||||
export function useV1Trade(
|
||||
isExactIn?: boolean,
|
||||
inputToken?: Token,
|
||||
outputToken?: Token,
|
||||
exactAmount?: TokenAmount,
|
||||
v2Trade?: Trade,
|
||||
minimumDelta: Percent = new Percent('0')
|
||||
): string {
|
||||
exactAmount?: TokenAmount
|
||||
): Trade | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const input = inputToken
|
||||
const output = outputToken
|
||||
const mainnet = chainId === ChainId.MAINNET
|
||||
|
||||
// get the mock v1 pairs
|
||||
const inputPair = useMockV1Pair(input)
|
||||
const outputPair = useMockV1Pair(output)
|
||||
const inputPair = useMockV1Pair(inputToken)
|
||||
const outputPair = useMockV1Pair(outputToken)
|
||||
|
||||
const inputIsWETH = mainnet && input?.equals(WETH[ChainId.MAINNET])
|
||||
const outputIsWETH = mainnet && output?.equals(WETH[ChainId.MAINNET])
|
||||
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
|
||||
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
|
||||
|
||||
// construct a direct or through ETH v1 route
|
||||
let pairs: Pair[]
|
||||
let pairs: Pair[] = []
|
||||
if (inputIsWETH && outputPair) {
|
||||
pairs = [outputPair]
|
||||
} else if (outputIsWETH && inputPair) {
|
||||
@@ -71,33 +104,61 @@ export function useV1TradeLinkIfBetter(
|
||||
pairs = [inputPair, outputPair]
|
||||
}
|
||||
|
||||
const route = pairs && new Route(pairs, input)
|
||||
let v1Trade: Trade
|
||||
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
|
||||
let v1Trade: Trade | undefined
|
||||
try {
|
||||
v1Trade =
|
||||
route && exactAmount
|
||||
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
|
||||
: undefined
|
||||
} catch {}
|
||||
return v1Trade
|
||||
}
|
||||
|
||||
let v1HasBetterTrade = false
|
||||
if (v1Trade) {
|
||||
if (isExactIn) {
|
||||
// discount the v1 output amount by minimumDelta
|
||||
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
|
||||
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !!!v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
||||
} else {
|
||||
// inflate the v1 amount by minimumDelta
|
||||
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
|
||||
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !!!v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
||||
}
|
||||
export function getTradeVersion(trade?: Trade): Version | undefined {
|
||||
const isV1 = trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair)
|
||||
if (isV1) return Version.v1
|
||||
if (isV1 === false) return Version.v2
|
||||
return undefined
|
||||
}
|
||||
|
||||
// returns the v1 exchange against which a trade should be executed
|
||||
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
|
||||
const tokenAddress: string | undefined = useMemo(() => {
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
const isV1 = tradeVersion === Version.v1
|
||||
return isV1
|
||||
? trade &&
|
||||
WETH[trade.inputAmount.token.chainId] &&
|
||||
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
|
||||
? trade.outputAmount.token.address
|
||||
: trade?.inputAmount?.token?.address
|
||||
: undefined
|
||||
}, [trade])
|
||||
return useV1ExchangeAddress(tokenAddress)
|
||||
}
|
||||
|
||||
const ZERO_PERCENT = new Percent('0')
|
||||
const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||
// returns whether tradeB is better than tradeA by at least a threshold
|
||||
export function isTradeBetter(
|
||||
tradeA: Trade | undefined,
|
||||
tradeB: Trade | undefined,
|
||||
minimumDelta: Percent = ZERO_PERCENT
|
||||
): boolean | undefined {
|
||||
if (!tradeA || !tradeB) return undefined
|
||||
|
||||
if (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
|
||||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
|
||||
) {
|
||||
throw new Error('Trades are not comparable')
|
||||
}
|
||||
|
||||
return v1HasBetterTrade
|
||||
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
|
||||
outputIsWETH ? 'ETH' : output.address
|
||||
}`
|
||||
: undefined
|
||||
if (minimumDelta.equalTo(ZERO_PERCENT)) {
|
||||
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
|
||||
} else {
|
||||
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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,10 +1,13 @@
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { ChainId, Token, WETH } from '@uniswap/sdk'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { ALL_TOKENS } from '../constants/tokens'
|
||||
import { useAddUserToken, useFetchTokenByAddress, useUserAddedTokens } from '../state/user/hooks'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
@@ -35,36 +38,65 @@ export function useAllTokens(): { [address: string]: Token } {
|
||||
}, [userAddedTokens, chainId])
|
||||
}
|
||||
|
||||
export function useToken(tokenAddress?: string): Token | undefined {
|
||||
const tokens = useAllTokens()
|
||||
return useMemo(() => {
|
||||
const validatedAddress = isAddress(tokenAddress)
|
||||
if (!validatedAddress) return
|
||||
return tokens[validatedAddress]
|
||||
}, [tokens, tokenAddress])
|
||||
// parse a name or symbol from a token response
|
||||
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
|
||||
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
|
||||
return str && str.length > 0
|
||||
? str
|
||||
: bytes32 && BYTES32_REGEX.test(bytes32)
|
||||
? parseBytes32String(bytes32)
|
||||
: defaultValue
|
||||
}
|
||||
|
||||
// gets token information by address (typically user input) and
|
||||
// automatically adds it for the user if the token address is valid
|
||||
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined {
|
||||
const fetchTokenByAddress = useFetchTokenByAddress()
|
||||
const addToken = useAddUserToken()
|
||||
const token = useToken(tokenAddress)
|
||||
// undefined if invalid or does not exist
|
||||
// null if loading
|
||||
// otherwise returns the token
|
||||
export function useToken(tokenAddress?: string): Token | undefined | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const tokens = useAllTokens()
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainId || !isAddress(tokenAddress)) return
|
||||
const weth = WETH[chainId as ChainId]
|
||||
if (weth && weth.address === isAddress(tokenAddress)) return
|
||||
const address = isAddress(tokenAddress)
|
||||
|
||||
if (tokenAddress && !token) {
|
||||
fetchTokenByAddress(tokenAddress).then(token => {
|
||||
if (token !== null) {
|
||||
addToken(token)
|
||||
}
|
||||
})
|
||||
const tokenContract = useTokenContract(address ? address : undefined, false)
|
||||
const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
|
||||
const token: Token | undefined = address ? tokens[address] : undefined
|
||||
|
||||
const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)
|
||||
const tokenNameBytes32 = useSingleCallResult(
|
||||
token ? undefined : tokenContractBytes32,
|
||||
'name',
|
||||
undefined,
|
||||
NEVER_RELOAD
|
||||
)
|
||||
const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)
|
||||
const symbolBytes32 = useSingleCallResult(token ? undefined : tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
|
||||
const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)
|
||||
|
||||
return useMemo(() => {
|
||||
if (token) return token
|
||||
if (!chainId || !address) return undefined
|
||||
if (decimals.loading || symbol.loading || tokenName.loading) return null
|
||||
if (decimals.result) {
|
||||
return new Token(
|
||||
chainId,
|
||||
address,
|
||||
decimals.result[0],
|
||||
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
|
||||
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
|
||||
)
|
||||
}
|
||||
}, [tokenAddress, token, fetchTokenByAddress, addToken, chainId])
|
||||
|
||||
return token
|
||||
return undefined
|
||||
}, [
|
||||
address,
|
||||
chainId,
|
||||
decimals.loading,
|
||||
decimals.result,
|
||||
symbol.loading,
|
||||
symbol.result,
|
||||
symbolBytes32.result,
|
||||
token,
|
||||
tokenName.loading,
|
||||
tokenName.result,
|
||||
tokenNameBytes32.result
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
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 allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
|
||||
() => [
|
||||
// the direct pair
|
||||
[tokenA, tokenB],
|
||||
// token A against all bases
|
||||
...bases.map((base): [Token | undefined, Token | undefined] => [tokenA, base]),
|
||||
// token B against all bases
|
||||
...bases.map((base): [Token | undefined, Token | undefined] => [tokenB, base]),
|
||||
// each base against all bases
|
||||
...flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase]))
|
||||
],
|
||||
[tokenA, tokenB, bases]
|
||||
)
|
||||
|
||||
const allPairs = usePairs(allPairCombinations)
|
||||
|
||||
// only pass along valid pairs, non-duplicated pairs
|
||||
return useMemo(
|
||||
() =>
|
||||
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
|
||||
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,18 +1,13 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
|
||||
import { Web3ReactContextInterface } from '@web3-react/core/dist/types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import 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() {
|
||||
export function useActiveWeb3React(): Web3ReactContextInterface<Web3Provider> & { chainId?: ChainId } {
|
||||
const context = useWeb3ReactCore<Web3Provider>()
|
||||
const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName)
|
||||
return context.active ? context : contextNetwork
|
||||
@@ -64,7 +59,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 +67,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 +75,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 +94,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]
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { ROUTER_ADDRESS } from '../constants'
|
||||
import { useTokenAllowance } from '../data/Allowances'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin } from '../utils'
|
||||
import { useTokenContract, useActiveWeb3React } from './index'
|
||||
import { useTokenContract } from './useContract'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
export enum ApprovalState {
|
||||
UNKNOWN,
|
||||
@@ -20,16 +23,16 @@ export enum ApprovalState {
|
||||
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
|
||||
export function useApproveCallback(
|
||||
amountToApprove?: TokenAmount,
|
||||
addressToApprove?: string
|
||||
spender?: string
|
||||
): [ApprovalState, () => Promise<void>] {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, addressToApprove)
|
||||
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address)
|
||||
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
|
||||
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
|
||||
|
||||
// check the current approval status
|
||||
const approval = useMemo(() => {
|
||||
if (!amountToApprove) return ApprovalState.UNKNOWN
|
||||
const approvalState: ApprovalState = useMemo(() => {
|
||||
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
|
||||
// we treat WETH as ETH which requires no approvals
|
||||
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
|
||||
// we might not have enough data to know whether or not we need to approve
|
||||
@@ -37,13 +40,13 @@ export function useApproveCallback(
|
||||
if (pendingApproval) return ApprovalState.PENDING
|
||||
// amountToApprove will be defined if currentAllowance is
|
||||
return currentAllowance.lessThan(amountToApprove) ? ApprovalState.NOT_APPROVED : ApprovalState.APPROVED
|
||||
}, [amountToApprove, currentAllowance, pendingApproval])
|
||||
}, [amountToApprove, currentAllowance, pendingApproval, spender])
|
||||
|
||||
const tokenContract = useTokenContract(amountToApprove?.token?.address)
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const approve = useCallback(async (): Promise<void> => {
|
||||
if (approval !== ApprovalState.NOT_APPROVED) {
|
||||
if (approvalState !== ApprovalState.NOT_APPROVED) {
|
||||
console.error('approve was called unnecessarily')
|
||||
return
|
||||
}
|
||||
@@ -58,30 +61,35 @@ export function useApproveCallback(
|
||||
return
|
||||
}
|
||||
|
||||
if (!spender) {
|
||||
console.error('no spender')
|
||||
return
|
||||
}
|
||||
|
||||
let useExact = false
|
||||
const estimatedGas = await tokenContract.estimateGas.approve(addressToApprove, MaxUint256).catch(() => {
|
||||
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
|
||||
// general fallback for tokens who restrict approval amounts
|
||||
useExact = true
|
||||
return tokenContract.estimateGas.approve(addressToApprove, amountToApprove.raw.toString())
|
||||
return tokenContract.estimateGas.approve(spender, amountToApprove.raw.toString())
|
||||
})
|
||||
|
||||
return tokenContract
|
||||
.approve(addressToApprove, useExact ? amountToApprove.raw.toString() : MaxUint256, {
|
||||
.approve(spender, useExact ? amountToApprove.raw.toString() : MaxUint256, {
|
||||
gasLimit: calculateGasMargin(estimatedGas)
|
||||
})
|
||||
.then((response: TransactionResponse) => {
|
||||
addTransaction(response, {
|
||||
summary: 'Approve ' + amountToApprove?.token?.symbol,
|
||||
approvalOfToken: amountToApprove?.token?.address
|
||||
summary: 'Approve ' + amountToApprove.token.symbol,
|
||||
approval: { tokenAddress: amountToApprove.token.address, spender: spender }
|
||||
})
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.debug('Failed to approve token', error)
|
||||
throw error
|
||||
})
|
||||
}, [approval, tokenContract, addressToApprove, amountToApprove, addTransaction])
|
||||
}, [approvalState, tokenContract, spender, amountToApprove, addTransaction])
|
||||
|
||||
return [approval, approve]
|
||||
return [approvalState, approve]
|
||||
}
|
||||
|
||||
// wraps useApproveCallback in the context of a swap
|
||||
@@ -90,5 +98,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0)
|
||||
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined),
|
||||
[trade, allowedSlippage]
|
||||
)
|
||||
return useApproveCallback(amountToApprove, ROUTER_ADDRESS)
|
||||
const tradeIsV1 = getTradeVersion(trade) === Version.v1
|
||||
const v1ExchangeAddress = useV1TradeExchangeAddress(trade)
|
||||
return useApproveCallback(amountToApprove, tradeIsV1 ? v1ExchangeAddress : ROUTER_ADDRESS)
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user