Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91a8202737 | ||
|
|
0b4819d165 | ||
|
|
e7d3289754 | ||
|
|
0698e0f82a | ||
|
|
0350cc4701 | ||
|
|
997052869d | ||
|
|
9ec16c2ba8 | ||
|
|
e2cf8f1642 | ||
|
|
ed6952d1f7 | ||
|
|
3277d70e93 | ||
|
|
d1a31fe763 | ||
|
|
f88af029ae | ||
|
|
9f3e49b4d8 | ||
|
|
d4911d1054 | ||
|
|
90df9c4ced | ||
|
|
14f15d1fd6 | ||
|
|
69818ace1f | ||
|
|
42906d6709 | ||
|
|
2f8936a980 | ||
|
|
f5c4468c3c | ||
|
|
852e8f749f | ||
|
|
6694e5e398 | ||
|
|
2c9a50a372 | ||
|
|
0fc0cba6de | ||
|
|
041c86c04d | ||
|
|
123373e671 | ||
|
|
eb1732deee | ||
|
|
3c13321a71 | ||
|
|
58703f31a0 | ||
|
|
58721fb191 | ||
|
|
678cd1a06f | ||
|
|
a5ff3beb92 | ||
|
|
35ccf425f6 | ||
|
|
fe030412cd | ||
|
|
4d5a43351f | ||
|
|
ac1bc3b3a6 | ||
|
|
d1063d50ed | ||
|
|
46fc74e90f | ||
|
|
2c4f4092d8 | ||
|
|
aac7268dc8 | ||
|
|
fd162a72ff | ||
|
|
e20936709c | ||
|
|
2fda2c8c15 | ||
|
|
1f09757c49 | ||
|
|
7e49babff7 | ||
|
|
b35653ade1 | ||
|
|
57b53013d1 | ||
|
|
bafd3f3c05 | ||
|
|
29db0a50b3 | ||
|
|
9566fb888e | ||
|
|
13c8903e8f | ||
|
|
1d06b47e8d | ||
|
|
0089c2ee43 | ||
|
|
9869a9fcb7 | ||
|
|
631f29d66d | ||
|
|
97deebad37 | ||
|
|
e667615449 | ||
|
|
4ab61faeae | ||
|
|
0004db3d4a | ||
|
|
c133c472be | ||
|
|
0019ccdf51 | ||
|
|
5a1a469f35 | ||
|
|
4c28f34803 | ||
|
|
104be830fc | ||
|
|
24c70791cd | ||
|
|
216fdea290 | ||
|
|
40e4ce2ed3 | ||
|
|
b2508fc6f2 | ||
|
|
f73b37287f | ||
|
|
c09eb738c3 | ||
|
|
6de3a6ec28 | ||
|
|
c1d35cc8b3 | ||
|
|
f279b2bea2 | ||
|
|
6ffbf756f8 | ||
|
|
10837d7ba1 | ||
|
|
2d6eddf9d4 | ||
|
|
aadf43efc3 | ||
|
|
227f729ecd | ||
|
|
a5b15e37f6 |
35
.github/workflows/release.yaml
vendored
35
.github/workflows/release.yaml
vendored
@@ -2,14 +2,15 @@ name: Release
|
||||
on:
|
||||
# every morning
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
- cron: '0 12 * * 1-4'
|
||||
|
||||
# releases are triggered on changes to this file
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/release.yaml'
|
||||
- '.env.production'
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
@@ -46,7 +47,7 @@ jobs:
|
||||
run: yarn install --ignore-scripts --frozen-lockfile
|
||||
|
||||
- name: Build the IPFS bundle
|
||||
run: yarn ipfs-build
|
||||
run: yarn build
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: upload
|
||||
@@ -64,14 +65,14 @@ jobs:
|
||||
cidv0: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Update DNS with new IPFS hash
|
||||
uses: uniswap/replace-vercel-dns-records@v1.0.0
|
||||
env:
|
||||
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
|
||||
RECORD_DOMAIN: 'uniswap.org'
|
||||
RECORD_NAME: '_dnslink.app'
|
||||
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||
uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
|
||||
with:
|
||||
domain: 'uniswap.org'
|
||||
subdomain: '_dnslink.app'
|
||||
record-type: 'TXT'
|
||||
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
|
||||
token: ${{ secrets.VERCEL_TOKEN }}
|
||||
team-name: 'uniswap'
|
||||
cid: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
@@ -82,15 +83,17 @@ jobs:
|
||||
tag_name: ${{ needs.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
|
||||
body: |
|
||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
||||
|
||||
The IPFS hash of the bundle is:
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
||||
|
||||
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
|
||||
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface directly from an IPFS gateway.
|
||||
The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on the Uniswap interface without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||
The preferred URLs below are safe to use to access this specific release.
|
||||
|
||||
Preferred URLs:
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
|
||||
4
.github/workflows/tests.yaml
vendored
4
.github/workflows/tests.yaml
vendored
@@ -2,10 +2,10 @@ name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
- master
|
||||
jobs:
|
||||
integration-tests:
|
||||
name: Integration tests
|
||||
|
||||
41
README.md
41
README.md
@@ -1,11 +1,12 @@
|
||||
# Uniswap Frontend
|
||||
# Uniswap Interface
|
||||
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](https://github.com/Uniswap/uniswap-interface/actions?query=workflow%3ATests)
|
||||
[](https://prettier.io/)
|
||||
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||
- Docs: [uniswap.org/docs/](https://uniswap.org/docs/)
|
||||
- Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
@@ -13,11 +14,11 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
|
||||
- Discord: [Uniswap](https://discord.gg/Y7TF6QA)
|
||||
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
|
||||
## Accessing the frontend
|
||||
## Accessing the Uniswap Interface
|
||||
|
||||
To access the front end, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
|
||||
or visit [uniswap.exchange](https://uniswap.exchange).
|
||||
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
## Development
|
||||
|
||||
@@ -27,26 +28,32 @@ or visit [uniswap.exchange](https://uniswap.exchange).
|
||||
yarn
|
||||
```
|
||||
|
||||
### Configure Environment (optional)
|
||||
|
||||
Copy `.env` to `.env.local` and change the appropriate variables.
|
||||
|
||||
### Run
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
To have the frontend default to a different network, make a copy of `.env` named `.env.local`,
|
||||
change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETWORK_URL` to e.g.
|
||||
`"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
|
||||
### Configuring the environment (optional)
|
||||
|
||||
Note that the front end only works properly on testnets where both
|
||||
To have the interface default to a different network when a wallet is not connected:
|
||||
|
||||
1. Make a copy of `.env` named `.env.local`
|
||||
2. Change `REACT_APP_NETWORK_ID` to `"{YOUR_NETWORK_ID}"`
|
||||
3. Change `REACT_APP_NETWORK_URL` to e.g. `"https://{YOUR_NETWORK_ID}.infura.io/v3/{YOUR_INFURA_KEY}"`
|
||||
|
||||
Note that the interface only works on testnets where both
|
||||
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
|
||||
[multicall](https://github.com/makerdao/multicall) are deployed.
|
||||
The frontend will not work on other networks.
|
||||
The interface will not work on other networks.
|
||||
|
||||
## Contributions
|
||||
|
||||
**Please open all pull requests against the `v2` branch.**
|
||||
CI checks will run against all PRs.
|
||||
**Please open all pull requests against the `master` branch.**
|
||||
CI checks will run against all PRs.
|
||||
|
||||
## Accessing Uniswap Interface V1
|
||||
|
||||
The Uniswap Interface supports swapping against, and migrating or removing liquidity from Uniswap V1. However,
|
||||
if you would like to use Uniswap V1, the Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
|
||||
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
@@ -67,13 +69,13 @@ class CustomizedBridge extends _Eip1193Bridge {
|
||||
|
||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||
Cypress.Commands.overwrite('visit', (original, url, options) => {
|
||||
return original(url, {
|
||||
return original(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url, {
|
||||
...options,
|
||||
onBeforeLoad(win) {
|
||||
options && options.onBeforeLoad && options.onBeforeLoad(win)
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
|
||||
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
|
||||
win.ethereum = new CustomizedBridge(signer, provider)
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
20
netlify.toml
20
netlify.toml
@@ -1,20 +0,0 @@
|
||||
# block some countries
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/451.html"
|
||||
status = 451
|
||||
force = true
|
||||
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
|
||||
headers = {Link="<https://uniswap.exchange>"}
|
||||
|
||||
# forward v2 subdomain to apex
|
||||
[[redirects]]
|
||||
from = "https://v2.uniswap.exchange/*"
|
||||
to = "https://uniswap.exchange/:splat"
|
||||
status = 301
|
||||
|
||||
# support SPA setup
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
10
package.json
10
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@uniswap/interface",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": "https://uniswap.exchange",
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@ethersproject/address": "^5.0.0-beta.134",
|
||||
@@ -33,12 +33,12 @@
|
||||
"@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/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.1.1",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
@@ -51,6 +51,7 @@
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"inter-ui": "^3.13.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"lodash.flatmap": "^4.5.0",
|
||||
"polished": "^3.3.2",
|
||||
@@ -80,8 +81,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
|
||||
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
|
||||
75
public/locales/iw.json
Normal file
75
public/locales/iw.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"noWallet": "לא נמצא ארנק",
|
||||
"wrongNetwork": "נבחרה רשת לא נכונה",
|
||||
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
|
||||
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
|
||||
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
|
||||
"disconnected": "מנותק",
|
||||
"swap": "המרה",
|
||||
"send": "שליחה",
|
||||
"pool": "להפקיד",
|
||||
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
|
||||
"input": "מוכר",
|
||||
"output": "אקבל",
|
||||
"estimated": "הערכה",
|
||||
"balance": "בארנק שלי {{ balanceInput }}",
|
||||
"unlock": "שחרור נעילת ארנק",
|
||||
"pending": "ממתין לאישור",
|
||||
"selectToken": "בחרו את הטוקן להמרה",
|
||||
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
|
||||
"noExchange": "לא מתאפשרת המרה",
|
||||
"exchangeRate": "שער המרה",
|
||||
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
|
||||
"selectTokenCont": "בחרו טוקן כדי להמשיך",
|
||||
"noLiquidity": "אין נזילות",
|
||||
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
|
||||
"transactionDetails": "פרטי הטרנזקציה",
|
||||
"hideDetails": "הסתר פרטים נוספים",
|
||||
"youAreSelling": "למכירה",
|
||||
"orTransFail": "או שהטרנזקציה תיכשל",
|
||||
"youWillReceive": "תוצר המרה מינימלי",
|
||||
"youAreBuying": "קונה",
|
||||
"itWillCost": "זה יעלה",
|
||||
"insufficientBalance": "אין בחשבון מספיק מטבעות",
|
||||
"inputNotValid": "קלט לא תקין",
|
||||
"differentToken": "יש צורך בטוקנים שונים",
|
||||
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
|
||||
"invalidRecipient": "לא הוכנסה כתובת תקינה",
|
||||
"recipientAddress": "כתובת יעד",
|
||||
"youAreSending": "כמות לשליחה",
|
||||
"willReceive": "יתקבל לכל הפחות",
|
||||
"to": "אל",
|
||||
"addLiquidity": "להוספת נזילות למאגר",
|
||||
"deposit": "הפקדה",
|
||||
"currentPoolSize": "גודל מאגר הנזילות הכולל",
|
||||
"yourPoolShare": "חלקך במאגר הנזילות",
|
||||
"noZero": "אפס אינו ערך תקין",
|
||||
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
|
||||
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
|
||||
"youAreAdding": "מתווספים למאגר",
|
||||
"and": "וגם",
|
||||
"intoPool": "לתוך הנזילות",
|
||||
"outPool": "מתוך",
|
||||
"youWillMint": "יונפקו לכם",
|
||||
"liquidityTokens": "טוקנים של נזילות",
|
||||
"totalSupplyIs": "חלקך במאגר הנזילות",
|
||||
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
|
||||
"totalSupplyIs0": "אין לך טוקנים של נזילות",
|
||||
"tokenWorth": "שווי כל טוקן נזילות הינו",
|
||||
"firstLiquidity": "אתה הראשוןה שמזרים נזילות למאגר",
|
||||
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
|
||||
"removeLiquidity": "הוצאה של נזילות",
|
||||
"poolTokens": "טוקנים של מאגר הנזילות",
|
||||
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
|
||||
"youAreRemoving": "יוסרו",
|
||||
"youWillRemove": "יוסרו",
|
||||
"createExchange": "ליצירת זוג מסחר",
|
||||
"invalidTokenAddress": "כתובת טוקן לא נכונה",
|
||||
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
|
||||
"invalidSymbol": "תו שגוי",
|
||||
"invalidDecimals": "ספרות עשרוניות שגויות",
|
||||
"tokenAddress": "כתובת הטוקן",
|
||||
"label": "שם",
|
||||
"decimals": "ספרות עשרויות",
|
||||
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
|
||||
}
|
||||
@@ -6,21 +6,21 @@ import { LinkStyledButton } from '../../theme'
|
||||
import { CheckCircle, Copy } from 'react-feather'
|
||||
|
||||
const CopyIcon = styled(LinkStyledButton)`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
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 { ExternalLink, 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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
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.5rem 0.75rem;
|
||||
padding: 0.25rem 0rem;
|
||||
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)};
|
||||
|
||||
:hover {
|
||||
border-color: ${({ pending, success, theme }) =>
|
||||
pending
|
||||
? transparentize(0, theme.primary1)
|
||||
: success
|
||||
? transparentize(0, theme.green1)
|
||||
: transparentize(0, theme.red1)};
|
||||
}
|
||||
font-size: 0.825rem;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
flex-shrink: 0;
|
||||
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,9 +18,8 @@ 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 { ButtonSecondary } from '../Button'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
@@ -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 AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
|
||||
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.primary1 : theme.text3) : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
margin-left: 1rem;
|
||||
font-size: 0.825rem;
|
||||
display: flex;
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.text2};
|
||||
}
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
@@ -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,73 +315,82 @@ 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}>
|
||||
<AddressLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
View on Etherscan ↗
|
||||
</AddressLink>
|
||||
<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}>
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={false}
|
||||
href={getEtherscanLink(chainId, account, 'address')}
|
||||
>
|
||||
View on Etherscan ↗
|
||||
</AddressLink>
|
||||
<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>
|
||||
<LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton>
|
||||
</AutoRow>
|
||||
|
||||
@@ -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,17 +1,17 @@
|
||||
import React, { useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import Modal from '../Modal'
|
||||
import Loader from '../Loader'
|
||||
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%;
|
||||
@@ -30,6 +30,11 @@ const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
`
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
@@ -37,7 +42,6 @@ interface ConfirmationModalProps {
|
||||
topContent: () => React.ReactChild
|
||||
bottomContent: () => React.ReactChild
|
||||
attemptingTxn: boolean
|
||||
pendingConfirmation: boolean
|
||||
pendingText: string
|
||||
title?: string
|
||||
}
|
||||
@@ -45,33 +49,22 @@ interface ConfirmationModalProps {
|
||||
export default function ConfirmationModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
hash,
|
||||
topContent,
|
||||
bottomContent,
|
||||
attemptingTxn,
|
||||
pendingConfirmation,
|
||||
hash,
|
||||
pendingText,
|
||||
title = ''
|
||||
}: ConfirmationModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
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>
|
||||
) : (
|
||||
const transactionBroadcast = !!hash
|
||||
|
||||
// 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>
|
||||
@@ -79,22 +72,23 @@ export default 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 ? (
|
||||
<>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
@@ -107,9 +101,7 @@ export default function ConfirmationModal({
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)}
|
||||
|
||||
{pendingConfirmation && (
|
||||
) : (
|
||||
<Text fontSize={12} color="#565A69" textAlign="center">
|
||||
Confirm this transaction in your wallet
|
||||
</Text>
|
||||
@@ -117,7 +109,25 @@ export default 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Send, Sun, Moon } from 'react-feather'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import { ButtonSecondary } from '../Button'
|
||||
|
||||
const FooterFrame = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Footer() {
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<FooterFrame>
|
||||
<form action="https://forms.gle/DaLuqvJsVhVaAM3J9" target="_blank">
|
||||
<ButtonSecondary p="8px 12px">
|
||||
<Send size={16} style={{ marginRight: '8px' }} /> Feedback
|
||||
</ButtonSecondary>
|
||||
</form>
|
||||
<ButtonSecondary onClick={toggleDarkMode} p="8px 12px" ml="0.5rem" width="min-content">
|
||||
{darkMode ? <Sun size={16} /> : <Moon size={16} />}
|
||||
</ButtonSecondary>
|
||||
</FooterFrame>
|
||||
)
|
||||
}
|
||||
70
src/components/Header/VersionSwitch.tsx
Normal file
70
src/components/Header/VersionSwitch.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
const VersionLabel = styled.span<{ enabled: boolean }>`
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
|
||||
font-size: 1rem;
|
||||
font-weight: ${({ enabled }) => (enabled ? '500' : '400')};
|
||||
:hover {
|
||||
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
|
||||
}
|
||||
`
|
||||
|
||||
interface VersionToggleProps extends React.ComponentProps<typeof Link> {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
|
||||
border-radius: 12px;
|
||||
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
|
||||
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
|
||||
background: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin-left: 0.5rem;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function VersionSwitch() {
|
||||
const version = useToggledVersion()
|
||||
const location = useLocation()
|
||||
const query = useParsedQueryString()
|
||||
const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send'
|
||||
|
||||
const toggleDest = useMemo(() => {
|
||||
return versionSwitchAvailable
|
||||
? {
|
||||
...location,
|
||||
search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}`
|
||||
}
|
||||
: location
|
||||
}, [location, query, version, versionSwitchAvailable])
|
||||
|
||||
const handleClick = useCallback(
|
||||
e => {
|
||||
if (!versionSwitchAvailable) e.preventDefault()
|
||||
},
|
||||
[versionSwitchAvailable]
|
||||
)
|
||||
|
||||
return (
|
||||
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
|
||||
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
|
||||
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,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 { ExternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { WETH, ChainId } from '@uniswap/sdk'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { YellowCard } from '../Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import Logo from '../../assets/svg/logo.svg'
|
||||
import 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`
|
||||
@@ -47,6 +45,15 @@ const HeaderElement = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const HeaderElementWrap = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
margin-top: 0.5rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -72,6 +79,7 @@ const AccountElement = styled.div<{ active: boolean }>`
|
||||
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
|
||||
:focus {
|
||||
border: 1px solid blue;
|
||||
@@ -82,10 +90,7 @@ const TestnetWrapper = styled.div`
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
margin-left: 10px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
pointer-events: auto;
|
||||
`
|
||||
|
||||
const NetworkCard = styled(YellowCard)`
|
||||
@@ -122,32 +127,23 @@ 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 HeaderControls = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
flex-direction: column;
|
||||
`};
|
||||
`
|
||||
|
||||
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 NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
|
||||
[ChainId.MAINNET]: null,
|
||||
[ChainId.RINKEBY]: 'Rinkeby',
|
||||
[ChainId.ROPSTEN]: 'Ropsten',
|
||||
[ChainId.GÖRLI]: 'Görli',
|
||||
[ChainId.KOVAN]: 'Kovan'
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
@@ -163,12 +159,12 @@ export default function Header() {
|
||||
<b>blog post ↗</b>
|
||||
</ExternalLink>
|
||||
or
|
||||
<ExternalLink href="https://migrate.uniswap.exchange/">
|
||||
<StyledInternalLink to="/migrate/v1">
|
||||
<b>migrate your liquidity ↗</b>
|
||||
</ExternalLink>
|
||||
</StyledInternalLink>
|
||||
.
|
||||
</MigrateBanner>
|
||||
<RowBetween padding="1rem">
|
||||
<RowBetween style={{ alignItems: 'flex-start' }} padding="1rem 1rem 0 1rem">
|
||||
<HeaderElement>
|
||||
<Title>
|
||||
<UniIcon id="link" to="/">
|
||||
@@ -186,34 +182,27 @@ 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>
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
<div style={{ pointerEvents: 'auto' }}>
|
||||
<HeaderControls>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
</HeaderElement>
|
||||
<HeaderElementWrap>
|
||||
<VersionSwitch />
|
||||
<Settings />
|
||||
<Menu />
|
||||
</div>
|
||||
</HeaderElement>
|
||||
</HeaderElementWrap>
|
||||
</HeaderControls>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,9 +77,7 @@ const MenuItem = styled(ExternalLink)`
|
||||
}
|
||||
`
|
||||
|
||||
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
|
||||
: 'https://github.com/Uniswap/uniswap-frontend'
|
||||
const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
|
||||
|
||||
export default function Menu() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
|
||||
@@ -18,6 +18,7 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
@@ -42,8 +43,10 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
|
||||
// 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 aria-label="content" {...rest} />
|
||||
))`
|
||||
<DialogContent {...rest} />
|
||||
)).attrs({
|
||||
'aria-label': 'dialog'
|
||||
})`
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
border: 1px solid ${({ theme }) => theme.bg1};
|
||||
@@ -67,12 +70,10 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
border-radius: 20px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
width: 65vw;
|
||||
max-height: 65vh;
|
||||
margin: 0;
|
||||
`}
|
||||
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
|
||||
width: 85vw;
|
||||
max-height: 66vh;
|
||||
${mobile &&
|
||||
css`
|
||||
width: 100vw;
|
||||
@@ -84,14 +85,6 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
}
|
||||
`
|
||||
|
||||
const HiddenCloseButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
`
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
@@ -116,21 +109,13 @@ export default function Modal({
|
||||
leave: { opacity: 0 }
|
||||
})
|
||||
|
||||
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] }))
|
||||
const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
|
||||
const bind = useGesture({
|
||||
onDrag: state => {
|
||||
let velocity = state.velocity
|
||||
if (velocity < 1) {
|
||||
velocity = 1
|
||||
}
|
||||
if (velocity > 8) {
|
||||
velocity = 8
|
||||
}
|
||||
set({
|
||||
xy: state.down ? state.movement : [0, 0],
|
||||
config: { mass: 1, tension: 210, friction: 20 }
|
||||
y: state.down ? state.movement[1] : 0
|
||||
})
|
||||
if (velocity > 3 && state.direction[1] > 0) {
|
||||
if (state.velocity > 3 && state.direction[1] > 0) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
@@ -149,6 +134,8 @@ export default function Modal({
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={true}
|
||||
>
|
||||
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||
{initialFocusRef ? null : <div tabIndex={1} />}
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
|
||||
@@ -161,18 +148,17 @@ export default function Modal({
|
||||
<animated.div
|
||||
{...bind()}
|
||||
style={{
|
||||
transform: (xy as any).interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`)
|
||||
transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`)
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
ariaLabel="test"
|
||||
aria-label="dialog content"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</animated.div>
|
||||
@@ -189,15 +175,14 @@ export default function Modal({
|
||||
{transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={false}
|
||||
>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent
|
||||
aria-label="dialog content"
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
|
||||
@@ -2,36 +2,16 @@ import { Placement } from '@popperjs/core'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import styled from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import Portal from '@reach/portal'
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity : 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 1;
|
||||
}
|
||||
`
|
||||
|
||||
const fadeOut = keyframes`
|
||||
from {
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 0;
|
||||
}
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 9999;
|
||||
|
||||
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
|
||||
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear;
|
||||
transition: visibility 150ms linear;
|
||||
visibility: ${props => (props.show ? 'visible' : 'hidden')};
|
||||
opacity: ${props => (props.show ? 1 : 0)};
|
||||
transition: visibility 150ms linear, opacity 150ms linear;
|
||||
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
|
||||
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)
|
||||
@@ -17,12 +17,13 @@ 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,32 +175,28 @@ 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'}>
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
141
src/components/SearchModal/PairSearchModal.tsx
Normal file
141
src/components/SearchModal/PairSearchModal.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Pair } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs } from '../../state/user/hooks'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, StyledInternalLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { filterPairs } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { pairComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
|
||||
interface PairSearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
}
|
||||
|
||||
function PairSearchModal({ history, isOpen, onDismiss }: PairSearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
return filterPairs(allPairs, searchQuery)
|
||||
}, [allPairs, searchQuery])
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
const query = searchQuery.toLowerCase()
|
||||
const queryMatches = (pair: Pair): boolean =>
|
||||
pair.token0.symbol.toLowerCase() === query || pair.token1.symbol.toLowerCase() === query
|
||||
return filteredPairs.sort((a, b): number => {
|
||||
const [aMatches, bMatches] = [queryMatches(a), queryMatches(b)]
|
||||
if (aMatches && !bMatches) return -1
|
||||
if (bMatches && !aMatches) return 1
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
return pairComparator(a, b, balanceA, balanceB)
|
||||
})
|
||||
}, [searchQuery, filteredPairs, allPairBalances])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
minHeight={70}
|
||||
>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a pool
|
||||
<QuestionHelper text="Find a pair by searching for its name below." />
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={onInput}
|
||||
/>
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Pool Name
|
||||
</Text>
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<PairList
|
||||
pairs={sortedPairList}
|
||||
focusTokenAddress={focusedToken?.address}
|
||||
onAddLiquidity={selectPair}
|
||||
onSelectPair={selectPair}
|
||||
pairBalances={allPairBalances}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
||||
</Text>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(PairSearchModal)
|
||||
@@ -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 { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { FadedSpan, GreySpan, MenuItem, 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)
|
||||
}}
|
||||
>
|
||||
<LinkStyledButton style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</LinkStyledButton>
|
||||
</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,226 +0,0 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton, StyledInternalLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterPairs, filterTokens } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { balanceComparator, useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface SearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
filterType?: 'tokens'
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
function SearchModal({
|
||||
history,
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
filterType,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: SearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const isTokenView = filterType === 'tokens'
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
|
||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const sortedTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return Object.values(allTokens).sort(tokenComparator)
|
||||
}, [allTokens, isTokenView, tokenComparator])
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return filterTokens(sortedTokens, searchQuery)
|
||||
}, [isTokenView, sortedTokens, searchQuery])
|
||||
|
||||
function _onTokenSelect(address: string) {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return allPairs.sort((a, b): number => {
|
||||
// sort by balance
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
|
||||
return balanceComparator(balanceA, balanceB)
|
||||
})
|
||||
}, [isTokenView, allPairs, allPairBalances])
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return filterPairs(sortedPairList, searchQuery)
|
||||
}, [isTokenView, searchQuery, sortedPairList])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{isTokenView ? 'Select a token' : 'Select a pool'}
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text={
|
||||
isTokenView
|
||||
? 'Find a token by searching for its name or symbol or by pasting its address below.'
|
||||
: 'Find a pair by searching for its name below.'
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type={'text'}
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={onInput}
|
||||
onBlur={closeTooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{isTokenView ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
{isTokenView && (
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
)}
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
{isTokenView ? (
|
||||
<TokenList
|
||||
tokens={filteredTokens}
|
||||
allTokenBalances={allTokenBalances}
|
||||
onRemoveAddedToken={removeTokenByAddress}
|
||||
onTokenSelect={_onTokenSelect}
|
||||
otherSelectedText={otherSelectedText}
|
||||
otherToken={otherSelectedTokenAddress}
|
||||
selectedToken={hiddenToken}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
/>
|
||||
) : (
|
||||
<PairList
|
||||
pairs={filteredPairs}
|
||||
focusTokenAddress={focusedToken?.address}
|
||||
onAddLiquidity={selectPair}
|
||||
onSelectPair={selectPair}
|
||||
pairBalances={allPairBalances}
|
||||
/>
|
||||
)}
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
{isTokenView ? (
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
) : (
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(SearchModal)
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { 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 }
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -78,10 +78,6 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
|
||||
}
|
||||
`
|
||||
|
||||
const SlippageSelector = styled.div`
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
export interface SlippageTabsProps {
|
||||
rawSlippage: number
|
||||
setRawSlippage: (rawSlippage: number) => void
|
||||
@@ -146,15 +142,14 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Set slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
|
||||
<SlippageSelector>
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
@@ -220,16 +215,16 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
: 'Your transaction may be frontrun'}
|
||||
</RowBetween>
|
||||
)}
|
||||
</SlippageSelector>
|
||||
</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="Your transaction will revert if it is pending for more than this long." />
|
||||
</RowFixed>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<RowFixed>
|
||||
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
||||
<Input
|
||||
color={!!deadlineError ? 'red' : undefined}
|
||||
@@ -246,6 +241,6 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
</TYPE.body>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
</>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
41
src/components/Toggle/index.tsx
Normal file
41
src/components/Toggle/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')};
|
||||
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
const StyledToggle = styled.a<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Toggle({ isActive, toggle }: ToggleProps) {
|
||||
return (
|
||||
<StyledToggle isActive={isActive} target="_self" onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} isOnSwitch={true}>
|
||||
On
|
||||
</ToggleElement>
|
||||
<ToggleElement isActive={!isActive} isOnSwitch={false}>
|
||||
Off
|
||||
</ToggleElement>
|
||||
</StyledToggle>
|
||||
)
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import { WETH } from '@uniswap/sdk'
|
||||
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
|
||||
const TOKEN_ICON_API = address =>
|
||||
const getTokenLogoURL = address =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
const BAD_IMAGES = {}
|
||||
const NO_LOGO_ADDRESSES: { [tokenAddress: string]: true } = {}
|
||||
|
||||
const Image = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
@@ -44,20 +44,16 @@ export default function TokenLogo({
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const [error, setError] = useState(false)
|
||||
const [, refresh] = useState<number>(0)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
// mock rinkeby DAI
|
||||
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
|
||||
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
|
||||
}
|
||||
|
||||
let path = ''
|
||||
const validated = isAddress(address)
|
||||
// hard code to show ETH instead of WETH in UI
|
||||
if (address === WETH[chainId].address) {
|
||||
if (validated === WETH[chainId].address) {
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
|
||||
path = TOKEN_ICON_API(address)
|
||||
} else if (!NO_LOGO_ADDRESSES[address] && validated) {
|
||||
path = getTokenLogoURL(validated)
|
||||
} else {
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
@@ -75,8 +71,8 @@ export default function TokenLogo({
|
||||
src={path}
|
||||
size={size}
|
||||
onError={() => {
|
||||
BAD_IMAGES[address] = true
|
||||
setError(true)
|
||||
NO_LOGO_ADDRESSES[address] = true
|
||||
refresh(i => i + 1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -3,13 +3,12 @@ import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { getEtherscanLink, isDefaultToken } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
@@ -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,7 +109,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}
|
||||
</div>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
@@ -51,13 +51,16 @@ 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)}
|
||||
|
||||
@@ -15,7 +15,7 @@ const InfoCard = styled.button<{ active?: boolean }>`
|
||||
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)};
|
||||
`
|
||||
|
||||
const OptionCard = styled(InfoCard)`
|
||||
const OptionCard = styled(InfoCard as any)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -30,7 +30,7 @@ const OptionCardLeft = styled.div`
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const OptionCardClickable = styled(OptionCard)<{ clickable?: boolean }>`
|
||||
const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>`
|
||||
margin-top: 0;
|
||||
&:hover {
|
||||
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
|
||||
@@ -114,7 +114,6 @@ export default function Option({
|
||||
<OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color}>
|
||||
{' '}
|
||||
{active ? (
|
||||
<CircleWrapper>
|
||||
<GreenCircle>
|
||||
|
||||
@@ -3,11 +3,9 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Option from './Option'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import WalletConnectData from './WalletConnectData'
|
||||
import { walletconnect, injected } from '../../connectors'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { injected } from '../../connectors'
|
||||
import { darken } from 'polished'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const PendingSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -19,14 +17,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 }>`
|
||||
@@ -72,28 +64,22 @@ const LoadingWrapper = styled.div`
|
||||
`
|
||||
|
||||
export default function PendingView({
|
||||
uri = '',
|
||||
size,
|
||||
connector,
|
||||
error = false,
|
||||
setPendingError,
|
||||
tryActivation
|
||||
}: {
|
||||
uri?: string
|
||||
size?: number
|
||||
connector?: AbstractConnector
|
||||
error?: boolean
|
||||
setPendingError: (error: boolean) => void
|
||||
tryActivation: (connector: AbstractConnector) => void
|
||||
}) {
|
||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
||||
const isMetamask = window?.ethereum?.isMetaMask
|
||||
|
||||
return (
|
||||
<PendingSection>
|
||||
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
|
||||
<LoadingMessage error={error}>
|
||||
<LoadingWrapper>
|
||||
{!error && <SpinnerWrapper src={Circle} />}
|
||||
{error ? (
|
||||
<ErrorGroup>
|
||||
<div>Error connecting.</div>
|
||||
@@ -106,10 +92,11 @@ export default function PendingView({
|
||||
Try Again
|
||||
</ErrorButton>
|
||||
</ErrorGroup>
|
||||
) : connector === walletconnect ? (
|
||||
'Scan QR code with a compatible wallet...'
|
||||
) : (
|
||||
'Initializing...'
|
||||
<>
|
||||
<StyledLoader />
|
||||
Initializing...
|
||||
</>
|
||||
)}
|
||||
</LoadingWrapper>
|
||||
</LoadingMessage>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import QRCode from 'qrcode.react'
|
||||
|
||||
const QRCodeWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
interface WalletConnectDataProps {
|
||||
uri?: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
|
||||
return <QRCodeWrapper>{uri && <QRCode size={size} value={uri} />}</QRCodeWrapper>
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import ReactGA from 'react-ga'
|
||||
import styled from 'styled-components'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
||||
|
||||
@@ -15,8 +14,9 @@ import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, walletconnect, fortmatic, portis } from '../../connectors'
|
||||
import { injected, fortmatic, portis } from '../../connectors'
|
||||
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
@@ -152,19 +152,6 @@ export default function WalletModal({
|
||||
}
|
||||
}, [walletModalOpen])
|
||||
|
||||
// set up uri listener for walletconnect
|
||||
const [uri, setUri] = useState()
|
||||
useEffect(() => {
|
||||
const activateWC = uri => {
|
||||
setUri(uri)
|
||||
// setWalletView(WALLET_VIEWS.PENDING)
|
||||
}
|
||||
walletconnect.on(URI_AVAILABLE, activateWC)
|
||||
return () => {
|
||||
walletconnect.off(URI_AVAILABLE, activateWC)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// close modal when a connection is successful
|
||||
const activePrevious = usePrevious(active)
|
||||
const connectorPrevious = usePrevious(connector)
|
||||
@@ -190,6 +177,12 @@ export default function WalletModal({
|
||||
})
|
||||
setPendingWallet(connector) // set wallet for pending view
|
||||
setWalletView(WALLET_VIEWS.PENDING)
|
||||
|
||||
// if the connector is walletconnect and the user has already tried to connect, manually reset the connector
|
||||
if (connector instanceof WalletConnectConnector && connector.walletConnectProvider?.wc?.uri) {
|
||||
connector.walletConnectProvider = undefined
|
||||
}
|
||||
|
||||
activate(connector, undefined, true).catch(error => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
@@ -345,8 +338,6 @@ export default function WalletModal({
|
||||
<ContentWrapper>
|
||||
{walletView === WALLET_VIEWS.PENDING ? (
|
||||
<PendingView
|
||||
uri={uri}
|
||||
size={220}
|
||||
connector={pendingWallet}
|
||||
error={pendingError}
|
||||
setPendingError={setPendingError}
|
||||
|
||||
@@ -5,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()
|
||||
@@ -78,7 +67,7 @@ export default function Web3ReactManager({ children }) {
|
||||
if (!active && !networkActive) {
|
||||
return showLoader ? (
|
||||
<MessageWrapper>
|
||||
<SpinnerWrapper src={Circle} />
|
||||
<Loader />
|
||||
</MessageWrapper>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -16,18 +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 { 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,19 +1,16 @@
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronUp, ChevronRight } from 'react-feather'
|
||||
import { Text, Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { SectionBreak } from './styleds'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { SectionBreak } from './styleds'
|
||||
import SwapRoute from './SwapRoute'
|
||||
|
||||
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
@@ -61,79 +58,37 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
|
||||
<SectionBreak />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
||||
export interface AdvancedSwapDetailsProps {
|
||||
trade?: Trade
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
||||
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const showRoute = trade?.route?.path?.length > 2
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Hide Advanced
|
||||
</Text>
|
||||
<ChevronUp color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
|
||||
<SectionBreak />
|
||||
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
|
||||
|
||||
<SlippageTabs {...slippageTabProps} />
|
||||
|
||||
{trade?.route?.path?.length > 2 && (
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||
</RowFixed>
|
||||
<Flex
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
my="0.5rem"
|
||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
||||
flexWrap="wrap"
|
||||
width="100%"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
{flatMap(
|
||||
trade.route.path,
|
||||
// add a null in-between each item
|
||||
(token, i, array) => {
|
||||
const lastItem = i === array.length - 1
|
||||
return lastItem ? [token] : [token, null]
|
||||
}
|
||||
).map((token, i) => {
|
||||
// use null as an indicator to insert chevrons
|
||||
if (token === null) {
|
||||
return <ChevronRight key={i} color={theme.text2} />
|
||||
} else {
|
||||
return (
|
||||
<Flex my="0.5rem" alignItems="center" key={token.address} style={{ flexShrink: 0 }}>
|
||||
<TokenLogo address={token.address} size="1.5rem" />
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
||||
{token.symbol}
|
||||
</TYPE.black>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</Flex>
|
||||
</AutoColumn>
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
|
||||
{showRoute && (
|
||||
<>
|
||||
<SectionBreak />
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
|
||||
</RowFixed>
|
||||
<SwapRoute trade={trade} />
|
||||
</AutoColumn>
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
)
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { RowBetween } from '../Row'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||
import { AdvancedDropdown } from './styleds'
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({
|
||||
showAdvanced,
|
||||
setShowAdvanced,
|
||||
...rest
|
||||
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
|
||||
showAdvanced: boolean
|
||||
setShowAdvanced: (showAdvanced: boolean) => void
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
|
||||
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
|
||||
transition: transform 300ms ease-in-out;
|
||||
`
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
|
||||
const lastTrade = useLast(trade)
|
||||
|
||||
return (
|
||||
<AdvancedDropdown>
|
||||
{showAdvanced ? (
|
||||
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
|
||||
) : (
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={() => setShowAdvanced(true)} padding={'8px 20px'} id="show-advanced">
|
||||
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Show Advanced
|
||||
</Text>
|
||||
<ChevronDown color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
)}
|
||||
</AdvancedDropdown>
|
||||
<AdvancedDetailsFooter show={Boolean(trade)}>
|
||||
<AdvancedSwapDetails {...rest} trade={lastTrade} />
|
||||
</AdvancedDetailsFooter>
|
||||
)
|
||||
}
|
||||
|
||||
40
src/components/swap/BetterTradeLink.tsx
Normal file
40
src/components/swap/BetterTradeLink.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { useLocation } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
import { StyledInternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function BetterTradeLink({ version }: { version: Version }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const location = useLocation()
|
||||
const search = useParsedQueryString()
|
||||
|
||||
const linkDestination = useMemo(() => {
|
||||
return {
|
||||
...location,
|
||||
search: `?${stringify({
|
||||
...search,
|
||||
use: version !== DEFAULT_VERSION ? version : undefined
|
||||
})}`
|
||||
}
|
||||
}, [location, search, version])
|
||||
|
||||
return (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on{' '}
|
||||
<StyledInternalLink to={linkDestination}>
|
||||
<b>Uniswap {version.toUpperCase()} ↗</b>
|
||||
</StyledInternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
export function PriceSlippageWarningCard({ priceSlippage }: { priceSlippage: Percent }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<YellowCard style={{ padding: '20px', paddingTop: '10px' }}>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<RowFixed style={{ paddingTop: '8px' }}>
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>{' '}
|
||||
<Text fontWeight={500} marginLeft="4px" color={theme.text1}>
|
||||
Price Warning
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<Text lineHeight="145.23%;" fontSize={16} fontWeight={400} color={theme.text1}>
|
||||
This trade will move the price by ~{priceSlippage.toFixed(2)}%.
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -37,6 +37,11 @@ export default function SwapModalFooter({
|
||||
confirmText: string
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
if (!trade) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoColumn gap="0px">
|
||||
|
||||
@@ -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>
|
||||
|
||||
38
src/components/swap/SwapRoute.tsx
Normal file
38
src/components/swap/SwapRoute.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Trade } from '@uniswap/sdk'
|
||||
import React, { Fragment, memo, useContext } from 'react'
|
||||
import { ChevronRight } from 'react-feather'
|
||||
import { Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { TYPE } from '../../theme'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
|
||||
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<Flex
|
||||
px="1rem"
|
||||
py="0.5rem"
|
||||
my="0.5rem"
|
||||
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
|
||||
flexWrap="wrap"
|
||||
width="100%"
|
||||
justifyContent="space-evenly"
|
||||
alignItems="center"
|
||||
>
|
||||
{trade.route.path.map((token, i, path) => {
|
||||
const isLastItem: boolean = i === path.length - 1
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
|
||||
<TokenLogo address={token.address} size="1.5rem" />
|
||||
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
|
||||
{token.symbol}
|
||||
</TYPE.black>
|
||||
</Flex>
|
||||
{isLastItem ? null : <ChevronRight color={theme.text2} />}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
)
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Trade } from '@uniswap/sdk'
|
||||
import { Price, Token } from '@uniswap/sdk'
|
||||
import { useContext } from 'react'
|
||||
import { Repeat } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -7,20 +7,19 @@ import { ThemeContext } from 'styled-components'
|
||||
import { StyledBalanceMaxMini } from './styleds'
|
||||
|
||||
interface TradePriceProps {
|
||||
trade?: Trade
|
||||
price?: Price
|
||||
inputToken?: Token
|
||||
outputToken?: Token
|
||||
showInverted: boolean
|
||||
setShowInverted: (showInverted: boolean) => void
|
||||
}
|
||||
|
||||
export default function TradePrice({ trade, showInverted, setShowInverted }: TradePriceProps) {
|
||||
export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const inputToken = trade?.inputAmount?.token
|
||||
const outputToken = trade?.outputAmount?.token
|
||||
|
||||
const price = showInverted
|
||||
? trade?.executionPrice?.toSignificant(6)
|
||||
: trade?.executionPrice?.invert()?.toSignificant(6)
|
||||
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
|
||||
|
||||
const show = Boolean(inputToken && outputToken)
|
||||
const label = showInverted
|
||||
? `${outputToken?.symbol} per ${inputToken?.symbol}`
|
||||
: `${inputToken?.symbol} per ${outputToken?.symbol}`
|
||||
@@ -32,10 +31,16 @@ export default function TradePrice({ trade, showInverted, setShowInverted }: Tra
|
||||
color={theme.text2}
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
>
|
||||
{price && `${price} ${label}`}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
{show ? (
|
||||
<>
|
||||
{formattedPrice ?? '-'} {label}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBetter: string }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return v1TradeLinkIfBetter ? (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on{' '}
|
||||
<ExternalLink href={v1TradeLinkIfBetter}>
|
||||
<b>Uniswap V1 ↗</b>
|
||||
</ExternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null
|
||||
}
|
||||
@@ -21,19 +21,6 @@ export const ArrowWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export const AdvancedDropdown = styled.div`
|
||||
padding-top: calc(10px + 2rem);
|
||||
padding-bottom: 10px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
`
|
||||
|
||||
export const SectionBreak = styled.div`
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
@@ -45,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.text1 : theme.green1};
|
||||
severity === 3 || severity === 4
|
||||
? theme.red1
|
||||
: severity === 2
|
||||
? theme.yellow2
|
||||
: severity === 1
|
||||
? theme.text1
|
||||
: theme.green1};
|
||||
`
|
||||
|
||||
export const InputGroup = styled(AutoColumn)`
|
||||
@@ -65,7 +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};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const injected = new InjectedConnector({
|
||||
export const walletconnect = new WalletConnectConnector({
|
||||
rpc: { 1: NETWORK_URL },
|
||||
bridge: 'https://bridge.walletconnect.org',
|
||||
qrcode: false,
|
||||
qrcode: true,
|
||||
pollingInterval: POLLING_INTERVAL
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +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,25 +1,77 @@
|
||||
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
|
||||
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
|
||||
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
|
||||
|
||||
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
|
||||
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
|
||||
|
||||
// used for display in the default list when adding liquidity
|
||||
export const COMMON_BASES = {
|
||||
[ChainId.MAINNET]: [
|
||||
WETH[ChainId.MAINNET],
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
],
|
||||
// a list of tokens by chain
|
||||
type ChainTokenList = {
|
||||
readonly [chainId in ChainId]: Token[]
|
||||
}
|
||||
|
||||
const WETH_ONLY: ChainTokenList = {
|
||||
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
|
||||
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
|
||||
[ChainId.RINKEBY]: [
|
||||
WETH[ChainId.RINKEBY],
|
||||
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]]
|
||||
}
|
||||
|
||||
const MAINNET_WALLETS = {
|
||||
// used to construct intermediary pairs for trading
|
||||
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR]
|
||||
}
|
||||
|
||||
// used for display in the default list when adding liquidity
|
||||
export const SUGGESTED_BASES: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
|
||||
}
|
||||
|
||||
// used to construct the list of all pairs we consider by default in the frontend
|
||||
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
|
||||
}
|
||||
|
||||
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
|
||||
[ChainId.MAINNET]: [
|
||||
new Pair(
|
||||
new TokenAmount(
|
||||
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
|
||||
'0'
|
||||
),
|
||||
new TokenAmount(
|
||||
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||
'0'
|
||||
)
|
||||
),
|
||||
new Pair(
|
||||
new TokenAmount(
|
||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
|
||||
'0'
|
||||
),
|
||||
new TokenAmount(
|
||||
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
|
||||
'0'
|
||||
)
|
||||
),
|
||||
new Pair(
|
||||
new TokenAmount(
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
'0'
|
||||
),
|
||||
new TokenAmount(
|
||||
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
|
||||
'0'
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
const TESTNET_CAPABLE_WALLETS = {
|
||||
INJECTED: {
|
||||
connector: injected,
|
||||
name: 'Injected',
|
||||
@@ -41,9 +93,9 @@ const MAINNET_WALLETS = {
|
||||
|
||||
export const SUPPORTED_WALLETS =
|
||||
process.env.REACT_APP_CHAIN_ID !== '1'
|
||||
? MAINNET_WALLETS
|
||||
? TESTNET_CAPABLE_WALLETS
|
||||
: {
|
||||
...MAINNET_WALLETS,
|
||||
...TESTNET_CAPABLE_WALLETS,
|
||||
...{
|
||||
WALLET_CONNECT: {
|
||||
connector: walletconnect,
|
||||
@@ -51,7 +103,8 @@ export const SUPPORTED_WALLETS =
|
||||
iconName: 'walletConnectIcon.svg',
|
||||
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
||||
href: null,
|
||||
color: '#4196FC'
|
||||
color: '#4196FC',
|
||||
mobile: true
|
||||
},
|
||||
WALLET_LINK: {
|
||||
connector: walletlink,
|
||||
@@ -70,15 +123,6 @@ export const SUPPORTED_WALLETS =
|
||||
mobile: true,
|
||||
mobileOnly: true
|
||||
},
|
||||
TRUST_WALLET_LINK: {
|
||||
name: 'Open in Trust Wallet',
|
||||
iconName: 'trustWallet.png',
|
||||
description: 'iOS and Android app.',
|
||||
href: 'https://link.trustwallet.com/open_url?coin_id=60&url=https://uniswap.exchange/swap',
|
||||
color: '#1C74CC',
|
||||
mobile: true,
|
||||
mobileOnly: true
|
||||
},
|
||||
FORTMATIC: {
|
||||
connector: fortmatic,
|
||||
name: 'Fortmatic',
|
||||
@@ -112,12 +156,13 @@ export const ONE_BIPS = new Percent(JSBI.BigInt(1), JSBI.BigInt(10000))
|
||||
export const BIPS_BASE = JSBI.BigInt(10000)
|
||||
// used for warning states
|
||||
export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1%
|
||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||
|
||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(300), BIPS_BASE) // 3%
|
||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||
// if the price slippage exceeds this number, force the user to type 'confirm' to execute
|
||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(2500), BIPS_BASE) // 25%
|
||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||
// for non expert mode disable swaps above this
|
||||
export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BIPS_BASE) // 15%
|
||||
|
||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||
export const V1_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
|
||||
export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
|
||||
export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD')
|
||||
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
|
||||
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
|
||||
|
||||
export default [
|
||||
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
|
||||
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
|
||||
@@ -7,8 +13,10 @@ 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, '0xba100000625a3754423978a60c9317c58a424e3D', 18, 'BAL', 'Balancer'),
|
||||
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
|
||||
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
|
||||
@@ -18,8 +26,9 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
|
||||
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
|
||||
COMP,
|
||||
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
DAI,
|
||||
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
|
||||
new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
|
||||
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
|
||||
@@ -31,6 +40,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'),
|
||||
@@ -59,9 +69,10 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
|
||||
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
|
||||
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
|
||||
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'),
|
||||
MKR,
|
||||
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
|
||||
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum Token'),
|
||||
new Token(ChainId.MAINNET, '0xe2f2a5C287993345a840Db3B0845fbC70f5935a5', 18, 'mUSD', 'mStable USD'),
|
||||
new Token(ChainId.MAINNET, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
|
||||
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
|
||||
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
|
||||
@@ -77,7 +88,10 @@ 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, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REP', 'Reputation'),
|
||||
new Token(ChainId.MAINNET, '0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf', 8, 'renBCH', 'renBCH'),
|
||||
new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC'),
|
||||
new Token(ChainId.MAINNET, '0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2', 8, 'renZEC', 'renZEC'),
|
||||
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REPv1', 'Augur v1 Reputation'),
|
||||
new Token(ChainId.MAINNET, '0x9469D013805bFfB7D3DEBe5E7839237e535ec483', 18, 'RING', 'Darwinia Network Native Token'),
|
||||
new Token(ChainId.MAINNET, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),
|
||||
new Token(ChainId.MAINNET, '0xB4EFd85c19999D84251304bDA99E90B92300Bd93', 18, 'RPL', 'Rocket Pool'),
|
||||
@@ -89,6 +103,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
|
||||
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
|
||||
new Token(ChainId.MAINNET, '0x0Ae055097C6d159879521C384F1D2123D1f195e6', 18, 'STAKE', 'STAKE'),
|
||||
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
|
||||
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
|
||||
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
|
||||
@@ -104,10 +119,11 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
|
||||
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
|
||||
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
|
||||
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
|
||||
USDC,
|
||||
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
|
||||
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
|
||||
USDT,
|
||||
new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'),
|
||||
new Token(ChainId.MAINNET, '0x9A48BD0EC040ea4f1D3147C025cd4076A2e71e3e', 18, 'USD++', 'PieDAO USD++'),
|
||||
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
|
||||
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),
|
||||
new Token(ChainId.MAINNET, '0x09fE5f0236F0Ea5D930197DCE254d77B04128075', 18, 'WCK', 'Wrapped CryptoKitties'),
|
||||
|
||||
@@ -1,9 +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_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
|
||||
const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {
|
||||
[ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
|
||||
[ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
|
||||
[ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
|
||||
[ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',
|
||||
[ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
|
||||
}
|
||||
|
||||
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
|
||||
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
|
||||
|
||||
export { V1_FACTORY_ADDRESS, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
|
||||
export { V1_FACTORY_ADDRESSES, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
|
||||
import { usePairContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useMultipleContractSingleData } from '../state/multicall/hooks'
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
|
||||
/*
|
||||
* if loading, return undefined
|
||||
* if no pair created yet, return null
|
||||
* if pair already created (even if 0 reserves), return pair
|
||||
*/
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const contract = usePairContract(pairAddress, false)
|
||||
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
|
||||
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
|
||||
const pairAddresses = useMemo(
|
||||
() =>
|
||||
tokens.map(([tokenA, tokenB]) => {
|
||||
return tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
}),
|
||||
[tokens]
|
||||
)
|
||||
|
||||
const results = useMultipleContractSingleData(pairAddresses, PAIR_INTERFACE, 'getReserves')
|
||||
|
||||
return useMemo(() => {
|
||||
if (loading || !tokenA || !tokenB) return undefined
|
||||
if (!reserves) return null
|
||||
const { reserve0, reserve1 } = reserves
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||
}, [loading, reserves, tokenA, tokenB])
|
||||
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]
|
||||
}
|
||||
|
||||
148
src/data/V1.ts
148
src/data/V1.ts
@@ -1,27 +1,26 @@
|
||||
import { ChainId, JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../hooks'
|
||||
import { useAllTokens } from '../hooks/Tokens'
|
||||
import { useV1FactoryContract } from '../hooks/useContract'
|
||||
import { Version } from '../hooks/useToggledVersion'
|
||||
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
|
||||
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
|
||||
function useV1PairAddress(tokenAddress?: string): string | undefined {
|
||||
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
|
||||
const contract = useV1FactoryContract()
|
||||
|
||||
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
|
||||
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
|
||||
}
|
||||
|
||||
class MockV1Pair extends Pair {
|
||||
readonly isV1: true = true
|
||||
}
|
||||
class MockV1Pair extends Pair {}
|
||||
|
||||
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
||||
const isWETH = token?.equals(WETH[token?.chainId])
|
||||
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
|
||||
|
||||
// will only return an address on mainnet, and not for WETH
|
||||
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
|
||||
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
|
||||
const tokenBalance = useTokenBalance(v1PairAddress, token)
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
||||
|
||||
@@ -30,42 +29,40 @@ function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
||||
: undefined
|
||||
}
|
||||
|
||||
// returns ALL v1 exchange addresses
|
||||
export function useAllV1ExchangeAddresses(): string[] {
|
||||
const factory = useV1FactoryContract()
|
||||
const exchangeCount = useSingleCallResult(factory, 'tokenCount')?.result
|
||||
|
||||
const parsedCount = parseInt(exchangeCount?.toString() ?? '0')
|
||||
|
||||
const indices = useMemo(() => [...Array(parsedCount).keys()].map(ix => [ix]), [parsedCount])
|
||||
const data = useSingleContractMultipleData(factory, 'getTokenWithId', indices, NEVER_RELOAD)
|
||||
|
||||
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
|
||||
}
|
||||
|
||||
// returns all v1 exchange addresses in the user's token list
|
||||
export function useAllTokenV1ExchangeAddresses(): string[] {
|
||||
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?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
|
||||
return useMemo(
|
||||
() =>
|
||||
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
|
||||
if (result?.[0] && result[0] !== AddressZero) {
|
||||
const token = allTokens[args[ix][0]]
|
||||
memo[result[0]] = token
|
||||
}
|
||||
return memo
|
||||
}, {}) ?? {},
|
||||
[allTokens, args, data]
|
||||
)
|
||||
}
|
||||
|
||||
// returns whether any of the tokens in the user's token list have liquidity on v1
|
||||
export function useUserProbablyHasV1Liquidity(): boolean | undefined {
|
||||
const exchangeAddresses = useAllTokenV1ExchangeAddresses()
|
||||
|
||||
export function useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const fakeTokens = useMemo(
|
||||
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
|
||||
[chainId, exchangeAddresses]
|
||||
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, fakeTokens)
|
||||
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
@@ -77,24 +74,23 @@ export function useUserProbablyHasV1Liquidity(): boolean | undefined {
|
||||
)
|
||||
}
|
||||
|
||||
export function useV1TradeLinkIfBetter(
|
||||
/**
|
||||
* Returns the trade to execute on V1 to go between input and output token
|
||||
*/
|
||||
export function useV1Trade(
|
||||
isExactIn?: boolean,
|
||||
input?: Token,
|
||||
output?: Token,
|
||||
exactAmount?: TokenAmount,
|
||||
v2Trade?: Trade,
|
||||
minimumDelta: Percent = new Percent('0')
|
||||
): string | undefined {
|
||||
inputToken?: Token,
|
||||
outputToken?: Token,
|
||||
exactAmount?: TokenAmount
|
||||
): Trade | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const isMainnet: boolean = chainId === ChainId.MAINNET
|
||||
|
||||
// get the mock v1 pairs
|
||||
const inputPair = useMockV1Pair(input)
|
||||
const outputPair = useMockV1Pair(output)
|
||||
const inputPair = useMockV1Pair(inputToken)
|
||||
const outputPair = useMockV1Pair(outputToken)
|
||||
|
||||
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET])
|
||||
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET])
|
||||
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
|
||||
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
|
||||
|
||||
// construct a direct or through ETH v1 route
|
||||
let pairs: Pair[] = []
|
||||
@@ -108,7 +104,7 @@ export function useV1TradeLinkIfBetter(
|
||||
pairs = [inputPair, outputPair]
|
||||
}
|
||||
|
||||
const route = input && pairs && pairs.length > 0 && new Route(pairs, input)
|
||||
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
|
||||
let v1Trade: Trade | undefined
|
||||
try {
|
||||
v1Trade =
|
||||
@@ -116,25 +112,53 @@ export function useV1TradeLinkIfBetter(
|
||||
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
|
||||
: undefined
|
||||
} catch {}
|
||||
return v1Trade
|
||||
}
|
||||
|
||||
let v1HasBetterTrade = false
|
||||
if (v1Trade) {
|
||||
if (isExactIn) {
|
||||
// discount the v1 output amount by minimumDelta
|
||||
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
|
||||
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
||||
} else {
|
||||
// inflate the v1 amount by minimumDelta
|
||||
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
|
||||
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
||||
}
|
||||
export function getTradeVersion(trade?: Trade): Version | undefined {
|
||||
const isV1 = trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair)
|
||||
if (isV1) return Version.v1
|
||||
if (isV1 === false) return Version.v2
|
||||
return undefined
|
||||
}
|
||||
|
||||
// returns the v1 exchange against which a trade should be executed
|
||||
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
|
||||
const tokenAddress: string | undefined = useMemo(() => {
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
const isV1 = tradeVersion === Version.v1
|
||||
return isV1
|
||||
? trade &&
|
||||
WETH[trade.inputAmount.token.chainId] &&
|
||||
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
|
||||
? trade.outputAmount.token.address
|
||||
: trade?.inputAmount?.token?.address
|
||||
: undefined
|
||||
}, [trade])
|
||||
return useV1ExchangeAddress(tokenAddress)
|
||||
}
|
||||
|
||||
const ZERO_PERCENT = new Percent('0')
|
||||
const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||
// returns whether tradeB is better than tradeA by at least a threshold
|
||||
export function isTradeBetter(
|
||||
tradeA: Trade | undefined,
|
||||
tradeB: Trade | undefined,
|
||||
minimumDelta: Percent = ZERO_PERCENT
|
||||
): boolean | undefined {
|
||||
if (!tradeA || !tradeB) return undefined
|
||||
|
||||
if (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
|
||||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
|
||||
) {
|
||||
throw new Error('Trades are not comparable')
|
||||
}
|
||||
|
||||
return v1HasBetterTrade && input && output
|
||||
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
|
||||
outputIsWETH ? 'ETH' : output.address
|
||||
}`
|
||||
: undefined
|
||||
if (minimumDelta.equalTo(ZERO_PERCENT)) {
|
||||
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
|
||||
} else {
|
||||
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,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,47 @@
|
||||
import { Pair, Token, TokenAmount, Trade } from '@uniswap/sdk'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { useMemo } from 'react'
|
||||
import { WETH, Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { usePair } from '../data/Reserves'
|
||||
|
||||
const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
|
||||
const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
|
||||
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
|
||||
import { usePairs } from '../data/Reserves'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
// check for direct pair between tokens
|
||||
const pairBetween = usePair(tokenA, tokenB)
|
||||
const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[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]
|
||||
// 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]
|
||||
Object.values(
|
||||
allPairs
|
||||
// filter out invalid pairs
|
||||
.filter((p): p is Pair => !!p)
|
||||
// filter out duplicated pairs
|
||||
.reduce<{ [pairAddress: string]: Pair }>((memo, curr) => {
|
||||
memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
|
||||
return memo
|
||||
}, {})
|
||||
),
|
||||
[allPairs]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,14 +49,11 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
|
||||
* Returns the best trade for the exact amount of tokens in to the given token out
|
||||
*/
|
||||
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
|
||||
const inputToken = amountIn?.token
|
||||
const outputToken = tokenOut
|
||||
|
||||
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
|
||||
const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
|
||||
|
||||
return useMemo(() => {
|
||||
if (amountIn && tokenOut && allowedPairs.length > 0) {
|
||||
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut)[0] ?? null
|
||||
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||
}
|
||||
return null
|
||||
}, [allowedPairs, amountIn, tokenOut])
|
||||
@@ -66,14 +63,11 @@ export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade
|
||||
* Returns the best trade for the token in to the exact amount of token out
|
||||
*/
|
||||
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
|
||||
const inputToken = tokenIn
|
||||
const outputToken = amountOut?.token
|
||||
|
||||
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
|
||||
const allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
|
||||
|
||||
return useMemo(() => {
|
||||
if (tokenIn && amountOut && allowedPairs.length > 0) {
|
||||
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut)[0] ?? null
|
||||
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||
}
|
||||
return null
|
||||
}, [allowedPairs, tokenIn, amountOut])
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
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 { injected } from '../connectors'
|
||||
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
|
||||
|
||||
@@ -4,12 +4,14 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { ROUTER_ADDRESS } from '../constants'
|
||||
import { useTokenAllowance } from '../data/Allowances'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin } from '../utils'
|
||||
import { useTokenContract } from './useContract'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
export enum ApprovalState {
|
||||
UNKNOWN,
|
||||
@@ -21,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
|
||||
@@ -38,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
|
||||
}
|
||||
@@ -59,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
|
||||
@@ -91,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)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import { Contract } from '@ethersproject/contracts'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { useMemo } from 'react'
|
||||
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
|
||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESS } from '../constants/v1'
|
||||
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
|
||||
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
|
||||
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
|
||||
import { getContract } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
@@ -25,18 +27,26 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
|
||||
|
||||
export function useV1FactoryContract(): Contract | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
return useContract(chainId === 1 ? V1_FACTORY_ADDRESS : undefined, V1_FACTORY_ABI, false)
|
||||
return useContract(V1_FACTORY_ADDRESSES[chainId as ChainId], V1_FACTORY_ABI, false)
|
||||
}
|
||||
|
||||
export function useV1ExchangeContract(address: string): Contract | null {
|
||||
return useContract(address, V1_EXCHANGE_ABI, false)
|
||||
export function useV2MigratorContract(): Contract | null {
|
||||
return useContract(MIGRATOR_ADDRESS, MIGRATOR_ABI, true)
|
||||
}
|
||||
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
export function useV1ExchangeContract(address?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(address, V1_EXCHANGE_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
|
||||
13
src/hooks/useLast.ts
Normal file
13
src/hooks/useLast.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Returns the last truthy value of type T
|
||||
* @param value changing value
|
||||
*/
|
||||
export default function useLast<T>(value: T | undefined | null): T | null | undefined {
|
||||
const [last, setLast] = useState<T | null | undefined>(value)
|
||||
useEffect(() => {
|
||||
setLast(last => value ?? last)
|
||||
}, [value])
|
||||
return last
|
||||
}
|
||||
11
src/hooks/useParsedQueryString.ts
Normal file
11
src/hooks/useParsedQueryString.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { parse, ParsedQs } from 'qs'
|
||||
import { useMemo } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
export default function useParsedQueryString(): ParsedQs {
|
||||
const { search } = useLocation()
|
||||
return useMemo(
|
||||
() => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}),
|
||||
[search]
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { MaxUint256 } from '@ethersproject/constants'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { ChainId, Token, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { ChainId, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
|
||||
import { useTokenAllowance } from '../data/Allowances'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useV1ExchangeContract } from './useContract'
|
||||
import useENSName from './useENSName'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
enum SwapType {
|
||||
EXACT_TOKENS_FOR_TOKENS,
|
||||
@@ -17,25 +21,37 @@ enum SwapType {
|
||||
EXACT_ETH_FOR_TOKENS,
|
||||
TOKENS_FOR_EXACT_TOKENS,
|
||||
TOKENS_FOR_EXACT_ETH,
|
||||
ETH_FOR_EXACT_TOKENS
|
||||
ETH_FOR_EXACT_TOKENS,
|
||||
V1_EXACT_ETH_FOR_TOKENS,
|
||||
V1_EXACT_TOKENS_FOR_ETH,
|
||||
V1_EXACT_TOKENS_FOR_TOKENS,
|
||||
V1_ETH_FOR_EXACT_TOKENS,
|
||||
V1_TOKENS_FOR_EXACT_ETH,
|
||||
V1_TOKENS_FOR_EXACT_TOKENS
|
||||
}
|
||||
|
||||
function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, chainId: number): SwapType {
|
||||
function getSwapType(trade: Trade | undefined): SwapType | undefined {
|
||||
if (!trade) return undefined
|
||||
const chainId = trade.inputAmount.token.chainId
|
||||
const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
|
||||
const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
|
||||
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
|
||||
const isV1 = getTradeVersion(trade) === Version.v1
|
||||
if (isExactIn) {
|
||||
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.EXACT_ETH_FOR_TOKENS
|
||||
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.EXACT_TOKENS_FOR_ETH
|
||||
if (inputWETH) {
|
||||
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
|
||||
} else if (outputWETH) {
|
||||
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
|
||||
} else {
|
||||
return SwapType.EXACT_TOKENS_FOR_TOKENS
|
||||
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
|
||||
}
|
||||
} else {
|
||||
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.ETH_FOR_EXACT_TOKENS
|
||||
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.TOKENS_FOR_EXACT_ETH
|
||||
if (inputWETH) {
|
||||
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
|
||||
} else if (outputWETH) {
|
||||
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
|
||||
} else {
|
||||
return SwapType.TOKENS_FOR_EXACT_TOKENS
|
||||
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,18 +60,24 @@ function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, c
|
||||
// and the user has approved the slippage adjusted input amount for the trade
|
||||
export function useSwapCallback(
|
||||
trade?: Trade, // trade to execute, required
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips, optional
|
||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now, optional
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
|
||||
to?: string // recipient of output, optional
|
||||
): null | (() => Promise<string>) {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const inputAllowance = useTokenAllowance(trade?.inputAmount?.token, account ?? undefined, ROUTER_ADDRESS)
|
||||
const addTransaction = useTransactionAdder()
|
||||
const recipient = to ? isAddress(to) : account
|
||||
const ensName = useENSName(to)
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
|
||||
const inputAllowance = useTokenAllowance(
|
||||
trade?.inputAmount?.token,
|
||||
account ?? undefined,
|
||||
tradeVersion === Version.v1 ? v1Exchange?.address : ROUTER_ADDRESS
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !recipient) return null
|
||||
if (!trade || !recipient || !tradeVersion) return null
|
||||
|
||||
// will always be defined
|
||||
const {
|
||||
@@ -78,23 +100,25 @@ export function useSwapCallback(
|
||||
throw new Error('missing dependencies in onSwap callback')
|
||||
}
|
||||
|
||||
const routerContract: Contract = getRouterContract(chainId, library, account)
|
||||
const contract: Contract | null =
|
||||
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
|
||||
if (!contract) {
|
||||
throw new Error('Failed to get a swap contract')
|
||||
}
|
||||
|
||||
const path = trade.route.path.map(t => t.address)
|
||||
|
||||
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
|
||||
|
||||
const swapType = getSwapType(
|
||||
{ [Field.INPUT]: trade.inputAmount.token, [Field.OUTPUT]: trade.outputAmount.token },
|
||||
trade.tradeType === TradeType.EXACT_INPUT,
|
||||
chainId as ChainId
|
||||
)
|
||||
const swapType = getSwapType(trade)
|
||||
|
||||
let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null
|
||||
// let estimate: Function, method: Function,
|
||||
let methodNames: string[],
|
||||
args: Array<string | string[] | number>,
|
||||
value: BigNumber | null = null
|
||||
switch (swapType) {
|
||||
case SwapType.EXACT_TOKENS_FOR_TOKENS:
|
||||
estimate = routerContract.estimateGas.swapExactTokensForTokens
|
||||
method = routerContract.swapExactTokensForTokens
|
||||
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
|
||||
args = [
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
@@ -102,11 +126,9 @@ export function useSwapCallback(
|
||||
recipient,
|
||||
deadlineFromNow
|
||||
]
|
||||
value = null
|
||||
break
|
||||
case SwapType.TOKENS_FOR_EXACT_TOKENS:
|
||||
estimate = routerContract.estimateGas.swapTokensForExactTokens
|
||||
method = routerContract.swapTokensForExactTokens
|
||||
methodNames = ['swapTokensForExactTokens']
|
||||
args = [
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
@@ -114,17 +136,14 @@ export function useSwapCallback(
|
||||
recipient,
|
||||
deadlineFromNow
|
||||
]
|
||||
value = null
|
||||
break
|
||||
case SwapType.EXACT_ETH_FOR_TOKENS:
|
||||
estimate = routerContract.estimateGas.swapExactETHForTokens
|
||||
method = routerContract.swapExactETHForTokens
|
||||
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
|
||||
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
|
||||
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
||||
break
|
||||
case SwapType.TOKENS_FOR_EXACT_ETH:
|
||||
estimate = routerContract.estimateGas.swapTokensForExactETH
|
||||
method = routerContract.swapTokensForExactETH
|
||||
methodNames = ['swapTokensForExactETH']
|
||||
args = [
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
@@ -132,11 +151,9 @@ export function useSwapCallback(
|
||||
recipient,
|
||||
deadlineFromNow
|
||||
]
|
||||
value = null
|
||||
break
|
||||
case SwapType.EXACT_TOKENS_FOR_ETH:
|
||||
estimate = routerContract.estimateGas.swapExactTokensForETH
|
||||
method = routerContract.swapExactTokensForETH
|
||||
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
|
||||
args = [
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
@@ -144,58 +161,164 @@ export function useSwapCallback(
|
||||
recipient,
|
||||
deadlineFromNow
|
||||
]
|
||||
value = null
|
||||
break
|
||||
case SwapType.ETH_FOR_EXACT_TOKENS:
|
||||
estimate = routerContract.estimateGas.swapETHForExactTokens
|
||||
method = routerContract.swapETHForExactTokens
|
||||
methodNames = ['swapETHForExactTokens']
|
||||
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
|
||||
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
||||
break
|
||||
case SwapType.V1_EXACT_ETH_FOR_TOKENS:
|
||||
methodNames = ['ethToTokenTransferInput']
|
||||
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
|
||||
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
||||
break
|
||||
case SwapType.V1_EXACT_TOKENS_FOR_TOKENS:
|
||||
methodNames = ['tokenToTokenTransferInput']
|
||||
args = [
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
1,
|
||||
deadlineFromNow,
|
||||
recipient,
|
||||
trade.outputAmount.token.address
|
||||
]
|
||||
break
|
||||
case SwapType.V1_EXACT_TOKENS_FOR_ETH:
|
||||
methodNames = ['tokenToEthTransferOutput']
|
||||
args = [
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
deadlineFromNow,
|
||||
recipient
|
||||
]
|
||||
break
|
||||
case SwapType.V1_ETH_FOR_EXACT_TOKENS:
|
||||
methodNames = ['ethToTokenTransferOutput']
|
||||
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
|
||||
value = BigNumber.from(slippageAdjustedInput.raw.toString())
|
||||
break
|
||||
case SwapType.V1_TOKENS_FOR_EXACT_ETH:
|
||||
methodNames = ['tokenToEthTransferOutput']
|
||||
args = [
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
deadlineFromNow,
|
||||
recipient
|
||||
]
|
||||
break
|
||||
case SwapType.V1_TOKENS_FOR_EXACT_TOKENS:
|
||||
methodNames = ['tokenToTokenTransferOutput']
|
||||
args = [
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
MaxUint256.toString(),
|
||||
deadlineFromNow,
|
||||
recipient,
|
||||
trade.outputAmount.token.address
|
||||
]
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unhandled swap type: ${swapType}`)
|
||||
}
|
||||
|
||||
return estimate(...args, value ? { value } : {})
|
||||
.then(estimatedGasLimit =>
|
||||
method(...args, {
|
||||
...(value ? { value } : {}),
|
||||
gasLimit: calculateGasMargin(estimatedGasLimit)
|
||||
})
|
||||
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
|
||||
methodNames.map(methodName =>
|
||||
contract.estimateGas[methodName](...args, value ? { value } : {})
|
||||
.then(calculateGasMargin)
|
||||
.catch(error => {
|
||||
console.error(`estimateGas failed for ${methodName}`, error)
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
.then(response => {
|
||||
if (recipient === account) {
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Swap ' +
|
||||
slippageAdjustedInput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.inputAmount.token.symbol +
|
||||
' for ' +
|
||||
slippageAdjustedOutput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.outputAmount.token.symbol
|
||||
})
|
||||
} else {
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Swap ' +
|
||||
slippageAdjustedInput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.inputAmount.token.symbol +
|
||||
' for ' +
|
||||
slippageAdjustedOutput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.outputAmount.token.symbol +
|
||||
' to ' +
|
||||
(ensName ?? recipient)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return response.hash
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`Swap or gas estimate failed`, error)
|
||||
throw error
|
||||
// we expect failures from left to right, so throw if we see failures
|
||||
// from right to left
|
||||
for (let i = 0; i < safeGasEstimates.length - 1; i++) {
|
||||
// if the FoT method fails, but the regular method does not, we should not
|
||||
// use the regular method. this probably means something is wrong with the fot token.
|
||||
if (BigNumber.isBigNumber(safeGasEstimates[i]) && !BigNumber.isBigNumber(safeGasEstimates[i + 1])) {
|
||||
throw new Error(
|
||||
'An error occurred. Please try raising your slippage. If that does not work, contact support.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
|
||||
BigNumber.isBigNumber(safeGasEstimate)
|
||||
)
|
||||
|
||||
// all estimations failed...
|
||||
if (indexOfSuccessfulEstimation === -1) {
|
||||
// if only 1 method exists, either:
|
||||
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
|
||||
// b) the token is FoT and the user specified an exact output, which is not allowed
|
||||
if (methodNames.length === 1) {
|
||||
throw Error(
|
||||
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.`
|
||||
)
|
||||
}
|
||||
// if 2 methods exists, either:
|
||||
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
|
||||
// b) the token is FoT and is taking more than the specified slippage
|
||||
else if (methodNames.length === 2) {
|
||||
throw Error(
|
||||
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.`
|
||||
)
|
||||
} else {
|
||||
throw Error('This transaction would fail. Please contact support.')
|
||||
}
|
||||
} else {
|
||||
const methodName = methodNames[indexOfSuccessfulEstimation]
|
||||
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
|
||||
|
||||
return contract[methodName](...args, {
|
||||
gasLimit: safeGasEstimate,
|
||||
...(value ? { value } : {})
|
||||
})
|
||||
.then((response: any) => {
|
||||
const inputSymbol = trade.inputAmount.token.symbol
|
||||
const outputSymbol = trade.outputAmount.token.symbol
|
||||
const inputAmount = slippageAdjustedInput.toSignificant(3)
|
||||
const outputAmount = slippageAdjustedOutput.toSignificant(3)
|
||||
|
||||
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
|
||||
const withRecipient = recipient === account ? base : `${base} to ${ensName ?? recipient}`
|
||||
|
||||
const withVersion =
|
||||
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
|
||||
|
||||
addTransaction(response, {
|
||||
summary: withVersion
|
||||
})
|
||||
|
||||
return response.hash
|
||||
})
|
||||
.catch((error: any) => {
|
||||
// if the user rejected the tx, pass this along
|
||||
if (error?.code === 4001) {
|
||||
throw error
|
||||
}
|
||||
// otherwise, the error was unexpected and we need to convey that
|
||||
else {
|
||||
console.error(`Swap failed`, error, methodName, args, value)
|
||||
throw Error('An error occurred while swapping. Please contact support.')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient])
|
||||
}, [
|
||||
trade,
|
||||
recipient,
|
||||
tradeVersion,
|
||||
allowedSlippage,
|
||||
chainId,
|
||||
inputAllowance,
|
||||
library,
|
||||
account,
|
||||
v1Exchange,
|
||||
deadline,
|
||||
addTransaction,
|
||||
ensName
|
||||
])
|
||||
}
|
||||
|
||||
15
src/hooks/useToggledVersion.ts
Normal file
15
src/hooks/useToggledVersion.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import useParsedQueryString from './useParsedQueryString'
|
||||
|
||||
export enum Version {
|
||||
v1 = 'v1',
|
||||
v2 = 'v2'
|
||||
}
|
||||
|
||||
export const DEFAULT_VERSION: Version = Version.v2
|
||||
|
||||
export default function useToggledVersion(): Version {
|
||||
const { use } = useParsedQueryString()
|
||||
if (!use || typeof use !== 'string') return Version.v2
|
||||
if (use.toLowerCase() === 'v1') return Version.v1
|
||||
return DEFAULT_VERSION
|
||||
}
|
||||
@@ -3,15 +3,13 @@ import { initReactI18next } from 'react-i18next'
|
||||
import XHR from 'i18next-xhr-backend'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
|
||||
const LOAD_PATH: string = process.env.PUBLIC_URL === '.' ? `./locales/{{lng}}.json` : '/locales/{{lng}}.json'
|
||||
|
||||
i18next
|
||||
.use(XHR)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
backend: {
|
||||
loadPath: LOAD_PATH
|
||||
loadPath: `./locales/{{lng}}.json`
|
||||
},
|
||||
react: {
|
||||
useSuspense: true
|
||||
|
||||
@@ -6,6 +6,7 @@ import ReactDOM from 'react-dom'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Provider } from 'react-redux'
|
||||
import { NetworkContextName } from './constants'
|
||||
import 'inter-ui'
|
||||
import './i18n'
|
||||
import App from './pages/App'
|
||||
import store from './state'
|
||||
@@ -18,7 +19,9 @@ import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
|
||||
function getLibrary(provider: any): Web3Provider {
|
||||
return new Web3Provider(provider)
|
||||
const library = new Web3Provider(provider)
|
||||
library.pollingInterval = 15000
|
||||
return library
|
||||
}
|
||||
|
||||
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
||||
|
||||
@@ -17,7 +17,7 @@ import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Ro
|
||||
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
|
||||
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
@@ -34,9 +34,9 @@ import {
|
||||
import { Field } from '../../state/mint/actions'
|
||||
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import { useUserSlippageTolerance, useUserDeadline, useIsExpertMode } from '../../state/user/hooks'
|
||||
|
||||
export default function AddLiquidity({ match: { params }, history }: RouteComponentProps<{ tokens: string }>) {
|
||||
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||
useDefaultsFromURLMatchParams(params)
|
||||
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
@@ -45,6 +45,8 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
const expertMode = useIsExpertMode()
|
||||
|
||||
// mint state
|
||||
const { independentField, typedValue, otherTypedValue } = useMintState()
|
||||
const {
|
||||
@@ -65,14 +67,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
|
||||
// modal and loading
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false)
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
|
||||
|
||||
// txn values
|
||||
const [deadline] = useUserDeadline() // custom from users settings
|
||||
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
|
||||
// get formatted amounts
|
||||
const formattedAmounts = {
|
||||
@@ -114,8 +114,6 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
async function onAdd() {
|
||||
setAttemptingTxn(true)
|
||||
|
||||
const router = getRouterContract(chainId, library, account)
|
||||
|
||||
const amountsMin = {
|
||||
@@ -155,12 +153,15 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
value = null
|
||||
}
|
||||
|
||||
setAttemptingTxn(true)
|
||||
await estimate(...args, value ? { value } : {})
|
||||
.then(estimatedGasLimit =>
|
||||
method(...args, {
|
||||
...(value ? { value } : {}),
|
||||
gasLimit: calculateGasMargin(estimatedGasLimit)
|
||||
}).then(response => {
|
||||
setAttemptingTxn(false)
|
||||
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Add ' +
|
||||
@@ -174,7 +175,6 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
})
|
||||
|
||||
setTxHash(response.hash)
|
||||
setPendingConfirmation(false)
|
||||
|
||||
ReactGA.event({
|
||||
category: 'Liquidity',
|
||||
@@ -183,12 +183,12 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
})
|
||||
})
|
||||
)
|
||||
.catch((e: Error) => {
|
||||
console.error(e)
|
||||
setPendingConfirmation(true)
|
||||
.catch(error => {
|
||||
setAttemptingTxn(false)
|
||||
setShowConfirm(false)
|
||||
setShowAdvanced(false)
|
||||
// we only care if the error is something _other_ than the user rejected the tx
|
||||
if (error?.code !== 4001) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -270,8 +270,8 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
|
||||
const PriceBar = () => {
|
||||
return (
|
||||
<AutoColumn gap="md" justify="space-between">
|
||||
<AutoRow justify="space-between">
|
||||
<AutoColumn gap="md">
|
||||
<AutoRow justify="space-around" gap="4px">
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
@@ -311,17 +311,15 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
<ConfirmationModal
|
||||
isOpen={showConfirm}
|
||||
onDismiss={() => {
|
||||
if (attemptingTxn) {
|
||||
history.push('/pool')
|
||||
return
|
||||
}
|
||||
setPendingConfirmation(true)
|
||||
setAttemptingTxn(false)
|
||||
setShowConfirm(false)
|
||||
// if there was a tx hash, we want to clear the input
|
||||
if (txHash) {
|
||||
onUserInput(Field.TOKEN_A, '')
|
||||
}
|
||||
setTxHash('')
|
||||
}}
|
||||
attemptingTxn={attemptingTxn}
|
||||
pendingConfirmation={pendingConfirmation}
|
||||
hash={txHash ? txHash : ''}
|
||||
hash={txHash}
|
||||
topContent={() => modalHeader()}
|
||||
bottomContent={modalBottom}
|
||||
pendingText={pendingText}
|
||||
@@ -392,50 +390,59 @@ export default function AddLiquidity({ match: { params }, history }: RouteCompon
|
||||
|
||||
{!account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : approvalA === ApprovalState.NOT_APPROVED || approvalA === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveACallback} disabled={approvalA === ApprovalState.PENDING}>
|
||||
{approvalA === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : approvalB === ApprovalState.NOT_APPROVED || approvalB === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveBCallback} disabled={approvalB === ApprovalState.PENDING}>
|
||||
{approvalB === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? 'Supply'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
<AutoColumn gap={'md'}>
|
||||
{(approvalA === ApprovalState.NOT_APPROVED ||
|
||||
approvalA === ApprovalState.PENDING ||
|
||||
approvalB === ApprovalState.NOT_APPROVED ||
|
||||
approvalB === ApprovalState.PENDING) &&
|
||||
isValid && (
|
||||
<RowBetween>
|
||||
{approvalA !== ApprovalState.APPROVED && (
|
||||
<ButtonPrimary
|
||||
onClick={approveACallback}
|
||||
disabled={approvalA === ApprovalState.PENDING}
|
||||
width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'}
|
||||
>
|
||||
{approvalA === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
{approvalB !== ApprovalState.APPROVED && (
|
||||
<ButtonPrimary
|
||||
onClick={approveBCallback}
|
||||
disabled={approvalB === ApprovalState.PENDING}
|
||||
width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'}
|
||||
>
|
||||
{approvalB === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
)}
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
expertMode ? onAdd() : setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? 'Supply'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B] ? (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{pair && !noLiquidity ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
||||
<PositionCard pair={pair} minimal={true} />
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { Suspense } from 'react'
|
||||
import { BrowserRouter, HashRouter, Route, Switch } from 'react-router-dom'
|
||||
import { HashRouter, Route, Switch } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
|
||||
import Footer from '../components/Footer'
|
||||
import Header from '../components/Header'
|
||||
import Popups from '../components/Popups'
|
||||
import Web3ReactManager from '../components/Web3ReactManager'
|
||||
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
|
||||
import AddLiquidity from './AddLiquidity'
|
||||
import CreatePool from './CreatePool'
|
||||
import MigrateV1 from './MigrateV1'
|
||||
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
|
||||
import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
|
||||
import Pool from './Pool'
|
||||
import PoolFinder from './PoolFinder'
|
||||
import RemoveLiquidity from './RemoveLiquidity'
|
||||
@@ -47,40 +49,14 @@ const BodyWrapper = styled.div`
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const BackgroundGradient = styled.div`
|
||||
width: 100%;
|
||||
height: 200vh;
|
||||
background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`};
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
|
||||
transform: translateY(-70vh);
|
||||
|
||||
@media (max-width: 960px) {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
transform: translateY(-150px);
|
||||
}
|
||||
`
|
||||
|
||||
const Marginer = styled.div`
|
||||
margin-top: 5rem;
|
||||
`
|
||||
|
||||
let Router: React.ComponentType
|
||||
if (process.env.PUBLIC_URL === '.') {
|
||||
Router = HashRouter
|
||||
} else {
|
||||
Router = BrowserRouter
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Router>
|
||||
<HashRouter>
|
||||
<Route component={GoogleAnalyticsReporter} />
|
||||
<Route component={DarkModeQueryParamReader} />
|
||||
<AppWrapper>
|
||||
@@ -99,15 +75,16 @@ export default function App() {
|
||||
<Route exact strict path="/create" component={CreatePool} />
|
||||
<Route exact strict path="/add/:tokens" component={AddLiquidity} />
|
||||
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
|
||||
<Route exact strict path="/migrate/v1" component={MigrateV1} />
|
||||
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
|
||||
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
|
||||
<Route component={RedirectPathToSwapOnly} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
<Marginer />
|
||||
<Footer />
|
||||
</BodyWrapper>
|
||||
<BackgroundGradient />
|
||||
</AppWrapper>
|
||||
</Router>
|
||||
</HashRouter>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import NavigationTabs from '../components/NavigationTabs'
|
||||
|
||||
const Body = styled.div`
|
||||
export const BodyWrapper = styled.div`
|
||||
position: relative;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
@@ -18,9 +18,9 @@ const Body = styled.div`
|
||||
*/
|
||||
export default function AppBody({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Body>
|
||||
<BodyWrapper>
|
||||
<NavigationTabs />
|
||||
<>{children}</>
|
||||
</Body>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import AppBody from '../AppBody'
|
||||
|
||||
import Row, { AutoRow } from '../../components/Row'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
|
||||
import { Text } from 'rebass'
|
||||
import { Plus } from 'react-feather'
|
||||
import { TYPE, StyledInternalLink } from '../../theme'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button'
|
||||
import { ButtonPrimary, ButtonDropdown, ButtonDropdownLight } from '../../components/Button'
|
||||
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
@@ -59,16 +59,16 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
<AutoColumn gap="20px">
|
||||
<AutoColumn gap="24px">
|
||||
{!token0Address ? (
|
||||
<ButtonDropwdown
|
||||
<ButtonDropdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select first token</Text>
|
||||
</ButtonDropwdown>
|
||||
</ButtonDropdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
@@ -83,13 +83,13 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
{token0?.address === WETH[chainId]?.address && '(default)'}
|
||||
</TYPE.darkGray>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
</ButtonDropdownLight>
|
||||
)}
|
||||
<ColumnCenter>
|
||||
<Plus size="16" color="#888D9B" />
|
||||
</ColumnCenter>
|
||||
{!token1Address ? (
|
||||
<ButtonDropwdown
|
||||
<ButtonDropdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
@@ -97,9 +97,9 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
disabled={step !== STEP.SELECT_TOKENS}
|
||||
>
|
||||
<Text fontSize={20}>Select second token</Text>
|
||||
</ButtonDropwdown>
|
||||
</ButtonDropdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
@@ -111,7 +111,7 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
{token1?.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
</ButtonDropdownLight>
|
||||
)}
|
||||
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
|
||||
<AutoRow padding="10px" justify="center">
|
||||
@@ -128,9 +128,8 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</AutoColumn>
|
||||
<SearchModal
|
||||
<TokenSearchModal
|
||||
isOpen={showSearch}
|
||||
filterType="tokens"
|
||||
onTokenSelect={address => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
}}
|
||||
|
||||
11
src/pages/MigrateV1/EmptyState.tsx
Normal file
11
src/pages/MigrateV1/EmptyState.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
export function EmptyState({ message }: { message: string }) {
|
||||
return (
|
||||
<AutoColumn style={{ minHeight: 200, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<TYPE.body>{message}</TYPE.body>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
395
src/pages/MigrateV1/MigrateV1Exchange.tsx
Normal file
395
src/pages/MigrateV1/MigrateV1Exchange.tsx
Normal file
@@ -0,0 +1,395 @@
|
||||
import { TransactionResponse } from '@ethersproject/abstract-provider'
|
||||
import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Redirect, RouteComponentProps } from 'react-router'
|
||||
import { ButtonConfirmed } from '../../components/Button'
|
||||
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
|
||||
import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useContract'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
|
||||
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { TYPE, ExternalLink } from '../../theme'
|
||||
import { isAddress, getEtherscanLink } from '../../utils'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
|
||||
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
const ONE = JSBI.BigInt(1)
|
||||
const ZERO_FRACTION = new Fraction(ZERO, ONE)
|
||||
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
|
||||
|
||||
function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
|
||||
return (
|
||||
<>
|
||||
{tokenAmount.equalTo(JSBI.BigInt(0))
|
||||
? '0'
|
||||
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
|
||||
? tokenAmount.toSignificant(4)
|
||||
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function V1LiquidityInfo({
|
||||
token,
|
||||
liquidityTokenAmount,
|
||||
tokenWorth,
|
||||
ethWorth
|
||||
}: {
|
||||
token: Token
|
||||
liquidityTokenAmount: TokenAmount
|
||||
tokenWorth: TokenAmount
|
||||
ethWorth: Fraction
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
|
||||
<TokenLogo size="24px" address={token.address} />
|
||||
<div style={{ marginLeft: '.75rem' }}>
|
||||
<TYPE.mediumHeader>
|
||||
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
|
||||
{token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH
|
||||
</TYPE.mediumHeader>
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
<RowBetween my="1rem">
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token.equals(WETH[chainId]) ? 'WETH' : token.symbol}:
|
||||
</Text>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{tokenWorth.toSignificant(4)}
|
||||
</Text>
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token.address} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween mb="1rem">
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled ETH:
|
||||
</Text>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{ethWorth.toSignificant(4)}
|
||||
</Text>
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={WETH[chainId].address} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount: TokenAmount; token: Token }) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
|
||||
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
|
||||
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
|
||||
|
||||
const v2Pair = usePair(WETH[chainId as ChainId], token)
|
||||
const isFirstLiquidityProvider: boolean = v2Pair === null
|
||||
|
||||
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId as ChainId]))
|
||||
|
||||
const [confirmingMigration, setConfirmingMigration] = useState<boolean>(false)
|
||||
const [pendingMigrationHash, setPendingMigrationHash] = useState<string | null>(null)
|
||||
|
||||
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
|
||||
|
||||
const ethWorth: Fraction = exchangeETHBalance
|
||||
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
|
||||
: ZERO_FRACTION
|
||||
|
||||
const tokenWorth: TokenAmount = exchangeTokenBalance
|
||||
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
|
||||
: new TokenAmount(token, ZERO)
|
||||
|
||||
const [approval, approve] = useApproveCallback(liquidityTokenAmount, MIGRATOR_ADDRESS)
|
||||
|
||||
const v1SpotPrice =
|
||||
exchangeTokenBalance && exchangeETHBalance
|
||||
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance, WEI_DENOM))
|
||||
: null
|
||||
|
||||
const priceDifferenceFraction: Fraction | undefined =
|
||||
v1SpotPrice && v2SpotPrice
|
||||
? v1SpotPrice
|
||||
.divide(v2SpotPrice)
|
||||
.multiply('100')
|
||||
.subtract('100')
|
||||
: undefined
|
||||
|
||||
const priceDifferenceAbs: Fraction | undefined = priceDifferenceFraction?.lessThan(ZERO)
|
||||
? priceDifferenceFraction?.multiply('-1')
|
||||
: priceDifferenceFraction
|
||||
|
||||
const minAmountETH: JSBI | undefined =
|
||||
v2SpotPrice && tokenWorth
|
||||
? tokenWorth
|
||||
.divide(v2SpotPrice)
|
||||
.multiply(WEI_DENOM)
|
||||
.multiply(ALLOWED_OUTPUT_MIN_PERCENT).quotient
|
||||
: ethWorth?.numerator
|
||||
|
||||
const minAmountToken: JSBI | undefined =
|
||||
v2SpotPrice && ethWorth
|
||||
? ethWorth
|
||||
.multiply(v2SpotPrice)
|
||||
.multiply(JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(token.decimals)))
|
||||
.multiply(ALLOWED_OUTPUT_MIN_PERCENT).quotient
|
||||
: tokenWorth?.numerator
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
const isMigrationPending = useIsTransactionPending(pendingMigrationHash)
|
||||
|
||||
const migrator = useV2MigratorContract()
|
||||
const migrate = useCallback(() => {
|
||||
if (!minAmountToken || !minAmountETH) return
|
||||
|
||||
setConfirmingMigration(true)
|
||||
migrator
|
||||
.migrate(
|
||||
token.address,
|
||||
minAmountToken.toString(),
|
||||
minAmountETH.toString(),
|
||||
account,
|
||||
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
|
||||
)
|
||||
.then((response: TransactionResponse) => {
|
||||
ReactGA.event({
|
||||
category: 'Migrate',
|
||||
action: 'V1->V2',
|
||||
label: token?.symbol
|
||||
})
|
||||
|
||||
addTransaction(response, {
|
||||
summary: `Migrate ${token.symbol} liquidity to V2`
|
||||
})
|
||||
setPendingMigrationHash(response.hash)
|
||||
})
|
||||
.catch(() => {
|
||||
setConfirmingMigration(false)
|
||||
})
|
||||
}, [minAmountToken, minAmountETH, migrator, token, account, addTransaction])
|
||||
|
||||
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
|
||||
|
||||
const largePriceDifference = !!priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5))
|
||||
|
||||
const isSuccessfullyMigrated = !!pendingMigrationHash && !!noLiquidityTokens
|
||||
|
||||
return (
|
||||
<AutoColumn gap="20px">
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
This tool will safely migrate your V1 liquidity to V2 with minimal price risk. The process is completely
|
||||
trustless thanks to the{' '}
|
||||
<ExternalLink href={getEtherscanLink(chainId, MIGRATOR_ADDRESS, 'address')}>
|
||||
<TYPE.blue display="inline">Uniswap migration contract↗</TYPE.blue>
|
||||
</ExternalLink>
|
||||
.
|
||||
</TYPE.body>
|
||||
|
||||
{!isFirstLiquidityProvider && largePriceDifference ? (
|
||||
<YellowCard>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
It{"'"}s best to deposit liquidity into Uniswap V2 at a price you believe is correct. If the V2 price seems
|
||||
incorrect, you can either make a swap to move the price or wait for someone else to do so.
|
||||
</TYPE.body>
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<TYPE.body>V1 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
|
||||
<RowBetween>
|
||||
<TYPE.body>V2 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v2SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
|
||||
<RowBetween>
|
||||
<TYPE.body color="inherit">Price Difference:</TYPE.body>
|
||||
<TYPE.black color="inherit">{priceDifferenceAbs.toSignificant(4)}%</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null}
|
||||
|
||||
{isFirstLiquidityProvider && (
|
||||
<PinkCard>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
|
||||
current V1 price. Your transaction cost also includes the gas to create the pool.
|
||||
</TYPE.body>
|
||||
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<TYPE.body>V1 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</PinkCard>
|
||||
)}
|
||||
|
||||
<LightCard>
|
||||
<V1LiquidityInfo
|
||||
token={token}
|
||||
liquidityTokenAmount={liquidityTokenAmount}
|
||||
tokenWorth={tokenWorth}
|
||||
ethWorth={ethWorth}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', marginTop: '1rem' }}>
|
||||
<AutoColumn gap="12px" style={{ flex: '1', marginRight: 12 }}>
|
||||
<ButtonConfirmed
|
||||
confirmed={approval === ApprovalState.APPROVED}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED}
|
||||
onClick={approve}
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="12px" style={{ flex: '1' }}>
|
||||
<ButtonConfirmed
|
||||
confirmed={isSuccessfullyMigrated}
|
||||
disabled={
|
||||
isSuccessfullyMigrated ||
|
||||
noLiquidityTokens ||
|
||||
isMigrationPending ||
|
||||
approval !== ApprovalState.APPROVED ||
|
||||
confirmingMigration
|
||||
}
|
||||
onClick={migrate}
|
||||
>
|
||||
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? <Dots>Migrating</Dots> : 'Migrate'}
|
||||
</ButtonConfirmed>
|
||||
</AutoColumn>
|
||||
</div>
|
||||
</LightCard>
|
||||
<TYPE.darkGray style={{ textAlign: 'center' }}>
|
||||
{`Your Uniswap V1 ${token.symbol}/ETH liquidity will become Uniswap V2 ${token.symbol}/ETH liquidity.`}
|
||||
</TYPE.darkGray>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MigrateV1Exchange({
|
||||
history,
|
||||
match: {
|
||||
params: { address }
|
||||
}
|
||||
}: RouteComponentProps<{ address: string }>) {
|
||||
const validatedAddress = isAddress(address)
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined)
|
||||
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
|
||||
|
||||
const token = useToken(tokenAddress)
|
||||
|
||||
const liquidityToken: Token | undefined = useMemo(
|
||||
() =>
|
||||
validatedAddress && token
|
||||
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
|
||||
: undefined,
|
||||
[chainId, validatedAddress, token]
|
||||
)
|
||||
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
history.push('/migrate/v1')
|
||||
}, [history])
|
||||
|
||||
// redirect for invalid url params
|
||||
if (!validatedAddress || tokenAddress === AddressZero) {
|
||||
console.error('Invalid address in path', address)
|
||||
return <Redirect to="/migrate/v1" />
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<ArrowLeft onClick={handleBack} />
|
||||
</div>
|
||||
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
{!account ? (
|
||||
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
|
||||
) : validatedAddress && token?.equals(WETH[chainId]) ? (
|
||||
<>
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
Because Uniswap V2 uses WETH under the hood, your Uniswap V1 WETH/ETH liquidity cannot be migrated. You
|
||||
may want to remove your liquidity instead.
|
||||
</TYPE.body>
|
||||
|
||||
<ButtonConfirmed
|
||||
onClick={() => {
|
||||
history.push(`/remove/v1/${validatedAddress}`)
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</ButtonConfirmed>
|
||||
</>
|
||||
) : userLiquidityBalance && token ? (
|
||||
<V1PairMigration liquidityTokenAmount={userLiquidityBalance} token={token} />
|
||||
) : (
|
||||
<EmptyState message="Loading..." />
|
||||
)}
|
||||
</AutoColumn>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
189
src/pages/MigrateV1/RemoveV1Exchange.tsx
Normal file
189
src/pages/MigrateV1/RemoveV1Exchange.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { TransactionResponse } from '@ethersproject/abstract-provider'
|
||||
import { JSBI, Token, TokenAmount, WETH, Fraction, Percent } from '@uniswap/sdk'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Redirect, RouteComponentProps } from 'react-router'
|
||||
import { ButtonConfirmed } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow } from '../../components/Row'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useV1ExchangeContract } from '../../hooks/useContract'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
|
||||
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { useTokenBalance, useETHBalances } from '../../state/wallet/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { V1LiquidityInfo } from './MigrateV1Exchange'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
|
||||
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
const ONE = JSBI.BigInt(1)
|
||||
const ZERO_FRACTION = new Fraction(ZERO, ONE)
|
||||
|
||||
function V1PairRemoval({
|
||||
exchangeContract,
|
||||
liquidityTokenAmount,
|
||||
token
|
||||
}: {
|
||||
exchangeContract: Contract
|
||||
liquidityTokenAmount: TokenAmount
|
||||
token: Token
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
|
||||
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
|
||||
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
|
||||
|
||||
const [confirmingRemoval, setConfirmingRemoval] = useState<boolean>(false)
|
||||
const [pendingRemovalHash, setPendingRemovalHash] = useState<string | null>(null)
|
||||
|
||||
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
|
||||
|
||||
const ethWorth: Fraction = exchangeETHBalance
|
||||
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
|
||||
: ZERO_FRACTION
|
||||
|
||||
const tokenWorth: TokenAmount = exchangeTokenBalance
|
||||
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
|
||||
: new TokenAmount(token, ZERO)
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
const isRemovalPending = useIsTransactionPending(pendingRemovalHash)
|
||||
|
||||
const remove = useCallback(() => {
|
||||
if (!liquidityTokenAmount) return
|
||||
|
||||
setConfirmingRemoval(true)
|
||||
exchangeContract
|
||||
.removeLiquidity(
|
||||
liquidityTokenAmount.raw.toString(),
|
||||
1, // min_eth, this is safe because we're removing liquidity
|
||||
1, // min_tokens, this is safe because we're removing liquidity
|
||||
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
|
||||
)
|
||||
.then((response: TransactionResponse) => {
|
||||
ReactGA.event({
|
||||
category: 'Remove',
|
||||
action: 'V1',
|
||||
label: token?.symbol
|
||||
})
|
||||
|
||||
addTransaction(response, {
|
||||
summary: `Remove ${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH V1 liquidity`
|
||||
})
|
||||
setPendingRemovalHash(response.hash)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
setConfirmingRemoval(false)
|
||||
})
|
||||
}, [exchangeContract, liquidityTokenAmount, token, chainId, addTransaction])
|
||||
|
||||
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
|
||||
|
||||
const isSuccessfullyRemoved = !!pendingRemovalHash && !!noLiquidityTokens
|
||||
|
||||
return (
|
||||
<AutoColumn gap="20px">
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
This tool will remove your V1 liquidity and send the underlying assets to your wallet.
|
||||
</TYPE.body>
|
||||
|
||||
<LightCard>
|
||||
<V1LiquidityInfo
|
||||
token={token}
|
||||
liquidityTokenAmount={liquidityTokenAmount}
|
||||
tokenWorth={tokenWorth}
|
||||
ethWorth={ethWorth}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', marginTop: '1rem' }}>
|
||||
<ButtonConfirmed
|
||||
confirmed={isSuccessfullyRemoved}
|
||||
disabled={isSuccessfullyRemoved || noLiquidityTokens || isRemovalPending || confirmingRemoval}
|
||||
onClick={remove}
|
||||
>
|
||||
{isSuccessfullyRemoved ? 'Success' : isRemovalPending ? <Dots>Removing</Dots> : 'Remove'}
|
||||
</ButtonConfirmed>
|
||||
</div>
|
||||
</LightCard>
|
||||
<TYPE.darkGray style={{ textAlign: 'center' }}>
|
||||
{`Your Uniswap V1 ${
|
||||
token.equals(WETH[chainId]) ? 'WETH' : token.symbol
|
||||
}/ETH liquidity will be redeemed for underlying assets.`}
|
||||
</TYPE.darkGray>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RemoveV1Exchange({
|
||||
history,
|
||||
match: {
|
||||
params: { address }
|
||||
}
|
||||
}: RouteComponentProps<{ address: string }>) {
|
||||
const validatedAddress = isAddress(address)
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined, true)
|
||||
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
|
||||
const token = useToken(tokenAddress)
|
||||
|
||||
const liquidityToken: Token | undefined = useMemo(
|
||||
() =>
|
||||
validatedAddress && token
|
||||
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
|
||||
: undefined,
|
||||
[chainId, validatedAddress, token]
|
||||
)
|
||||
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
history.push('/migrate/v1')
|
||||
}, [history])
|
||||
|
||||
// redirect for invalid url params
|
||||
if (!validatedAddress || tokenAddress === AddressZero) {
|
||||
console.error('Invalid address in path', address)
|
||||
return <Redirect to="/migrate/v1" />
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<ArrowLeft onClick={handleBack} />
|
||||
</div>
|
||||
<TYPE.mediumHeader>Remove V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<QuestionHelper text="Remove your Uniswap V1 liquidity tokens." />
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
{!account ? (
|
||||
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
|
||||
) : userLiquidityBalance && token ? (
|
||||
<V1PairRemoval
|
||||
exchangeContract={exchangeContract}
|
||||
liquidityTokenAmount={userLiquidityBalance}
|
||||
token={token}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState message="Loading..." />
|
||||
)}
|
||||
</AutoColumn>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +1,124 @@
|
||||
import { JSBI, Token } from '@uniswap/sdk'
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
import { useAllV1ExchangeAddresses } from '../../data/V1'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import { AutoRow } from '../../components/Row'
|
||||
import { SearchInput } from '../../components/SearchModal/styleds'
|
||||
import { useAllTokenV1Exchanges } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { useToken, useAllTokens } from '../../hooks/Tokens'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import V1PositionCard from '../../components/PositionCard/V1'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { useAddUserToken } from '../../state/user/hooks'
|
||||
import { isDefaultToken, isCustomAddedToken } from '../../utils'
|
||||
|
||||
const PLACEHOLDER_ACCOUNT = (
|
||||
<div>
|
||||
<h1>You must connect a wallet to use this tool.</h1>
|
||||
</div>
|
||||
)
|
||||
|
||||
/**
|
||||
* Page component for migrating liquidity from V1
|
||||
*/
|
||||
export default function MigrateV1({}: RouteComponentProps) {
|
||||
export default function MigrateV1({ history }: RouteComponentProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const v1ExchangeAddresses = useAllV1ExchangeAddresses()
|
||||
|
||||
const v1ExchangeTokens: Token[] = useMemo(() => {
|
||||
return v1ExchangeAddresses.map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
|
||||
}, [chainId, v1ExchangeAddresses])
|
||||
const [tokenSearch, setTokenSearch] = useState<string>('')
|
||||
const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch])
|
||||
|
||||
const tokenBalances = useTokenBalances(account, v1ExchangeTokens)
|
||||
// automatically add the search token
|
||||
const token = useToken(tokenSearch)
|
||||
const isDefault = isDefaultToken(token)
|
||||
const allTokens = useAllTokens()
|
||||
const isCustomAdded = isCustomAddedToken(allTokens, token)
|
||||
const addToken = useAddUserToken()
|
||||
useEffect(() => {
|
||||
if (token && !isDefault && !isCustomAdded) {
|
||||
addToken(token)
|
||||
}
|
||||
}, [token, isDefault, isCustomAdded, addToken])
|
||||
|
||||
const unmigratedExchangeAddresses = useMemo(
|
||||
() =>
|
||||
Object.keys(tokenBalances).filter(tokenAddress =>
|
||||
tokenBalances[tokenAddress] ? JSBI.greaterThan(tokenBalances[tokenAddress]?.raw, JSBI.BigInt(0)) : false
|
||||
),
|
||||
[tokenBalances]
|
||||
// get V1 LP balances
|
||||
const V1Exchanges = useAllTokenV1Exchanges()
|
||||
const V1LiquidityTokens: Token[] = useMemo(() => {
|
||||
return Object.keys(V1Exchanges).map(
|
||||
exchangeAddress => new Token(chainId, exchangeAddress, 18, 'UNI-V1', 'Uniswap V1')
|
||||
)
|
||||
}, [chainId, V1Exchanges])
|
||||
const [V1LiquidityBalances, V1LiquidityBalancesLoading] = useTokenBalancesWithLoadingIndicator(
|
||||
account,
|
||||
V1LiquidityTokens
|
||||
)
|
||||
const allV1PairsWithLiquidity = V1LiquidityTokens.filter(V1LiquidityToken => {
|
||||
return (
|
||||
V1LiquidityBalances?.[V1LiquidityToken.address] &&
|
||||
JSBI.greaterThan(V1LiquidityBalances[V1LiquidityToken.address].raw, JSBI.BigInt(0))
|
||||
)
|
||||
}).map(V1LiquidityToken => {
|
||||
return (
|
||||
<V1PositionCard
|
||||
key={V1LiquidityToken.address}
|
||||
token={V1Exchanges[V1LiquidityToken.address]}
|
||||
V1LiquidityBalance={V1LiquidityBalances[V1LiquidityToken.address]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
if (!account) {
|
||||
return PLACEHOLDER_ACCOUNT
|
||||
}
|
||||
// should never always be false, because a V1 exhchange exists for WETH on all testnets
|
||||
const isLoading = Object.keys(V1Exchanges)?.length === 0 || V1LiquidityBalancesLoading
|
||||
|
||||
return <div>{unmigratedExchangeAddresses?.join('\n')}</div>
|
||||
const handleBackClick = useCallback(() => {
|
||||
history.push('/pool')
|
||||
}, [history])
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<ArrowLeft onClick={handleBackClick} />
|
||||
</div>
|
||||
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
For each pool shown below, click migrate to remove your liquidity from Uniswap V1 and deposit it into Uniswap
|
||||
V2.
|
||||
</TYPE.body>
|
||||
|
||||
{!account ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
Connect to a wallet to view your V1 liquidity.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : isLoading ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
<Dots>Loading</Dots>
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : (
|
||||
<>
|
||||
<AutoRow>
|
||||
<SearchInput
|
||||
value={tokenSearch}
|
||||
onChange={handleTokenSearchChange}
|
||||
placeholder="Enter a token address to find liquidity"
|
||||
/>
|
||||
</AutoRow>
|
||||
{allV1PairsWithLiquidity?.length > 0 ? (
|
||||
<>{allV1PairsWithLiquidity}</>
|
||||
) : (
|
||||
<EmptyState message="No V1 Liquidity found." />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useState, useContext } from 'react'
|
||||
import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { JSBI, Pair } from '@uniswap/sdk'
|
||||
import { JSBI } from '@uniswap/sdk'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
import Question from '../../components/QuestionHelper'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import PairSearchModal from '../../components/SearchModal/PairSearchModal'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import { useUserProbablyHasV1Liquidity } from '../../data/V1'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { LinkStyledButton, StyledInternalLink, TYPE } from '../../theme'
|
||||
import { useUserHasLiquidityInAllTokens } from '../../data/V1'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { StyledInternalLink, TYPE } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
@@ -16,9 +16,10 @@ import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { usePairs } from '../../data/Reserves'
|
||||
import { useAllDummyPairs } from '../../state/user/hooks'
|
||||
import AppBody from '../AppBody'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
|
||||
const Positions = styled.div`
|
||||
position: relative;
|
||||
@@ -31,35 +32,41 @@ const FixedBottom = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) {
|
||||
const pair = usePair(dummyPair.token0, dummyPair.token1)
|
||||
return <PositionCard pair={pair} />
|
||||
}
|
||||
|
||||
export default function Pool({ history }: RouteComponentProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { account } = useActiveWeb3React()
|
||||
const [showPoolSearch, setShowPoolSearch] = useState(false)
|
||||
|
||||
// initiate listener for LP balances
|
||||
const pairs = useAllDummyPairs()
|
||||
const pairBalances = useTokenBalances(
|
||||
// fetch the user's balances of all tracked V2 LP tokens
|
||||
const V2DummyPairs = useAllDummyPairs()
|
||||
const [V2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
|
||||
account,
|
||||
pairs?.map(p => p.liquidityToken)
|
||||
V2DummyPairs?.map(p => p.liquidityToken)
|
||||
)
|
||||
// fetch the reserves for all V2 pools in which the user has a balance
|
||||
const V2DummyPairsWithABalance = V2DummyPairs.filter(
|
||||
V2DummyPair =>
|
||||
V2PairsBalances[V2DummyPair.liquidityToken.address] &&
|
||||
JSBI.greaterThan(V2PairsBalances[V2DummyPair.liquidityToken.address].raw, JSBI.BigInt(0))
|
||||
)
|
||||
const V2Pairs = usePairs(
|
||||
V2DummyPairsWithABalance.map(V2DummyPairWithABalance => [
|
||||
V2DummyPairWithABalance.token0,
|
||||
V2DummyPairWithABalance.token1
|
||||
])
|
||||
)
|
||||
const V2IsLoading =
|
||||
fetchingV2PairBalances || V2Pairs?.length < V2DummyPairsWithABalance.length || V2Pairs?.some(V2Pair => !!!V2Pair)
|
||||
|
||||
const filteredExchangeList = pairs
|
||||
.filter(pair => {
|
||||
return (
|
||||
pairBalances?.[pair.liquidityToken.address] &&
|
||||
JSBI.greaterThan(pairBalances[pair.liquidityToken.address].raw, JSBI.BigInt(0))
|
||||
)
|
||||
})
|
||||
.map((pair, i) => {
|
||||
return <PositionCardWrapper key={i} dummyPair={pair} />
|
||||
})
|
||||
const allV2PairsWithLiquidity = V2Pairs.filter(V2Pair => !!V2Pair).map(V2Pair => (
|
||||
<PositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />
|
||||
))
|
||||
|
||||
const hasV1Liquidity = useUserProbablyHasV1Liquidity()
|
||||
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
|
||||
|
||||
const handleSearchDismiss = useCallback(() => {
|
||||
setShowPoolSearch(false)
|
||||
}, [setShowPoolSearch])
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
@@ -72,40 +79,49 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
}}
|
||||
>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool
|
||||
Join {allV2PairsWithLiquidity?.length > 0 ? 'another' : 'a'} pool
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
|
||||
<Positions>
|
||||
<AutoColumn gap="12px">
|
||||
<RowBetween padding={'0 8px'}>
|
||||
<Text color={theme.text1} fontWeight={500}>
|
||||
Your Pooled Liquidity
|
||||
Your Liquidity
|
||||
</Text>
|
||||
<Question text="When you add liquidity, you are given pool tokens that represent your share. If you don’t see a pool you joined in this list, try importing a pool below." />
|
||||
</RowBetween>
|
||||
{filteredExchangeList?.length === 0 && (
|
||||
<LightCard
|
||||
padding="40px
|
||||
"
|
||||
>
|
||||
|
||||
{!account ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
Connect to a wallet to view your liquidity.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : V2IsLoading ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
<Dots>Loading</Dots>
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : allV2PairsWithLiquidity?.length > 0 ? (
|
||||
<>{allV2PairsWithLiquidity}</>
|
||||
) : (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
No liquidity found.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
)}
|
||||
{filteredExchangeList}
|
||||
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
|
||||
{!hasV1Liquidity ? (
|
||||
<>
|
||||
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
|
||||
<StyledInternalLink id="import-pool-link" to="/find">
|
||||
Import it.
|
||||
</StyledInternalLink>
|
||||
</>
|
||||
) : (
|
||||
<LinkStyledButton id="migrate-v1-liquidity-link">Migrate your V1 liquidity.</LinkStyledButton>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<div>
|
||||
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
|
||||
{hasV1Liquidity ? 'Uniswap V1 liquidity found!' : "Don't see a pool you joined?"}{' '}
|
||||
<StyledInternalLink id="import-pool-link" to={hasV1Liquidity ? '/migrate/v1' : '/find'}>
|
||||
{hasV1Liquidity ? 'Migrate now.' : 'Import it.'}
|
||||
</StyledInternalLink>
|
||||
</Text>
|
||||
</div>
|
||||
</AutoColumn>
|
||||
<FixedBottom>
|
||||
<ColumnCenter>
|
||||
@@ -115,7 +131,7 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
</ColumnCenter>
|
||||
</FixedBottom>
|
||||
</Positions>
|
||||
<SearchModal isOpen={showPoolSearch} onDismiss={() => setShowPoolSearch(false)} />
|
||||
<PairSearchModal isOpen={showPoolSearch} onDismiss={handleSearchDismiss} />
|
||||
</AutoColumn>
|
||||
</AppBody>
|
||||
)
|
||||
|
||||
@@ -17,9 +17,13 @@ export const MaxButton = styled.button<{ width: string }>`
|
||||
border: 1px solid ${({ theme }) => theme.primary5};
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
padding: 0.25rem 0.5rem;
|
||||
`};
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
margin: 0.25rem;
|
||||
overflow: hidden;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ButtonDropwdown, ButtonDropwdownLight, ButtonPrimary } from '../../components/Button'
|
||||
import { ButtonDropdownLight } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import Row from '../../components/Row'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
@@ -23,138 +22,133 @@ enum Fields {
|
||||
TOKEN1 = 1
|
||||
}
|
||||
|
||||
export default function PoolFinder({ history }: RouteComponentProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
|
||||
export default function PoolFinder() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const [token0Address, setToken0Address] = useState<string>()
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN1)
|
||||
|
||||
const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
|
||||
const [token1Address, setToken1Address] = useState<string>()
|
||||
const token0: Token = useToken(token0Address)
|
||||
const token1: Token = useToken(token1Address)
|
||||
|
||||
const pair: Pair = usePair(token0, token1)
|
||||
const addPair = usePairAdder()
|
||||
|
||||
useEffect(() => {
|
||||
if (pair) {
|
||||
addPair(pair)
|
||||
}
|
||||
}, [pair, addPair])
|
||||
|
||||
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
|
||||
|
||||
const newPair: boolean =
|
||||
pair === null ||
|
||||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
|
||||
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
|
||||
|
||||
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
|
||||
const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
|
||||
|
||||
const handleTokenSelect = useCallback(
|
||||
(address: string) => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
},
|
||||
[activeField]
|
||||
)
|
||||
|
||||
const handleSearchDismiss = useCallback(() => {
|
||||
setShowSearch(false)
|
||||
}, [setShowSearch])
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<AutoColumn gap="md">
|
||||
{!token0Address ? (
|
||||
<ButtonDropwdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select first token</Text>
|
||||
</ButtonDropwdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
{token0 ? (
|
||||
<Row>
|
||||
<TokenLogo address={token0Address} />
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
{token0?.symbol}
|
||||
{token0.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
)}
|
||||
) : (
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
Select a Token
|
||||
</Text>
|
||||
)}
|
||||
</ButtonDropdownLight>
|
||||
|
||||
<ColumnCenter>
|
||||
<Plus size="16" color="#888D9B" />
|
||||
</ColumnCenter>
|
||||
{!token1Address ? (
|
||||
<ButtonDropwdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select second token</Text>
|
||||
</ButtonDropwdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
{token1 ? (
|
||||
<Row>
|
||||
<TokenLogo address={token1Address} />
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
{token1?.symbol}
|
||||
{token1.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
)}
|
||||
{allowImport && (
|
||||
) : (
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
Select a Token
|
||||
</Text>
|
||||
)}
|
||||
</ButtonDropdownLight>
|
||||
|
||||
{poolImported && (
|
||||
<ColumnCenter
|
||||
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
|
||||
>
|
||||
<Text textAlign="center" fontWeight={500} color="">
|
||||
Pool Imported!
|
||||
Pool Found!
|
||||
</Text>
|
||||
</ColumnCenter>
|
||||
)}
|
||||
|
||||
{position ? (
|
||||
!JSBI.equal(position.raw, JSBI.BigInt(0)) ? (
|
||||
poolImported ? (
|
||||
<PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" />
|
||||
) : (
|
||||
<LightCard padding="45px 10px">
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Text textAlign="center">Pool found, you don’t have liquidity on this pair yet.</Text>
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>
|
||||
<Text textAlign="center">Add liquidity to this pair instead.</Text>
|
||||
<Text textAlign="center">You don’t have liquidity in this pool yet.</Text>
|
||||
<StyledInternalLink to={`/add/${token0.address}-${token1.address}`}>
|
||||
<Text textAlign="center">Add liquidity?</Text>
|
||||
</StyledInternalLink>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
)
|
||||
) : newPair ? (
|
||||
<LightCard padding="45px">
|
||||
<LightCard padding="45px 10px">
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Text color="">No pool found.</Text>
|
||||
<Text textAlign="center">No pool found.</Text>
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
) : (
|
||||
<LightCard padding={'45px'}>
|
||||
<Text color="#C3C5CB" textAlign="center">
|
||||
Select a token pair to find your liquidity.
|
||||
<LightCard padding="45px 10px">
|
||||
<Text textAlign="center">
|
||||
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
|
||||
</Text>
|
||||
</LightCard>
|
||||
)}
|
||||
|
||||
<ButtonPrimary disabled={!allowImport} onClick={() => history.goBack()}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
<SearchModal
|
||||
|
||||
<TokenSearchModal
|
||||
isOpen={showSearch}
|
||||
filterType="tokens"
|
||||
onTokenSelect={address => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
}}
|
||||
onDismiss={() => {
|
||||
setShowSearch(false)
|
||||
}}
|
||||
onTokenSelect={handleTokenSelect}
|
||||
onDismiss={handleSearchDismiss}
|
||||
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
|
||||
/>
|
||||
</AppBody>
|
||||
|
||||
@@ -7,7 +7,7 @@ import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonConfirmed, ButtonPrimary, ButtonLight, ButtonError } from '../../components/Button'
|
||||
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
@@ -18,7 +18,7 @@ import Row, { RowBetween, RowFixed } from '../../components/Row'
|
||||
|
||||
import Slider from '../../components/Slider'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { ROUTER_ADDRESS } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { usePairContract } from '../../hooks/useContract'
|
||||
|
||||
@@ -31,11 +31,12 @@ import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallbac
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
|
||||
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import { Field } from '../../state/burn/actions'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
|
||||
export default function RemoveLiquidity({ match: { params }, history }: RouteComponentProps<{ tokens: string }>) {
|
||||
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||
useDefaultsFromURLMatchParams(params)
|
||||
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
@@ -47,20 +48,18 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
// burn state
|
||||
const { independentField, typedValue } = useBurnState()
|
||||
const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
|
||||
const { onUserInput } = useBurnActionHandlers()
|
||||
const { onUserInput: _onUserInput } = useBurnActionHandlers()
|
||||
const isValid = !error
|
||||
|
||||
// modal and loading
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false)
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
|
||||
const [showDetailed, setShowDetailed] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for
|
||||
|
||||
// txn values
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
const [deadline] = useUserDeadline()
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const formattedAmounts = {
|
||||
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
|
||||
@@ -144,17 +143,18 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
})
|
||||
}
|
||||
|
||||
function resetModalState() {
|
||||
setSignatureData(null)
|
||||
setAttemptingTxn(false)
|
||||
setPendingConfirmation(true)
|
||||
}
|
||||
// wrapped onUserInput to clear signatures
|
||||
const onUserInput = useCallback(
|
||||
(field: Field, typedValue: string) => {
|
||||
setSignatureData(null)
|
||||
return _onUserInput(field, typedValue)
|
||||
},
|
||||
[_onUserInput]
|
||||
)
|
||||
|
||||
// tx sending
|
||||
const addTransaction = useTransactionAdder()
|
||||
async function onRemove() {
|
||||
setAttemptingTxn(true)
|
||||
|
||||
const router = getRouterContract(chainId, library, account)
|
||||
|
||||
const amountsMin = {
|
||||
@@ -167,13 +167,12 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
|
||||
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
|
||||
|
||||
let estimate, method: Function, args: Array<string | string[] | number | boolean>
|
||||
let methodNames: string[], args: Array<string | string[] | number | boolean>
|
||||
// we have approval, use normal remove liquidity
|
||||
if (approval === ApprovalState.APPROVED) {
|
||||
// removeLiquidityETH
|
||||
if (oneTokenIsETH) {
|
||||
estimate = router.estimateGas.removeLiquidityETH
|
||||
method = router.removeLiquidityETH
|
||||
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
|
||||
args = [
|
||||
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
|
||||
parsedAmounts[Field.LIQUIDITY].raw.toString(),
|
||||
@@ -185,8 +184,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
}
|
||||
// removeLiquidity
|
||||
else {
|
||||
estimate = router.estimateGas.removeLiquidity
|
||||
method = router.removeLiquidity
|
||||
methodNames = ['removeLiquidity']
|
||||
args = [
|
||||
tokens[Field.TOKEN_A].address,
|
||||
tokens[Field.TOKEN_B].address,
|
||||
@@ -202,8 +200,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
else if (signatureData !== null) {
|
||||
// removeLiquidityETHWithPermit
|
||||
if (oneTokenIsETH) {
|
||||
estimate = router.estimateGas.removeLiquidityETHWithPermit
|
||||
method = router.removeLiquidityETHWithPermit
|
||||
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
|
||||
args = [
|
||||
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
|
||||
parsedAmounts[Field.LIQUIDITY].raw.toString(),
|
||||
@@ -219,8 +216,7 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
}
|
||||
// removeLiquidityETHWithPermit
|
||||
else {
|
||||
estimate = router.estimateGas.removeLiquidityWithPermit
|
||||
method = router.removeLiquidityWithPermit
|
||||
methodNames = ['removeLiquidityWithPermit']
|
||||
args = [
|
||||
tokens[Field.TOKEN_A].address,
|
||||
tokens[Field.TOKEN_B].address,
|
||||
@@ -236,14 +232,37 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
]
|
||||
}
|
||||
} else {
|
||||
console.error('Attempting to confirm without approval or a signature.')
|
||||
console.error('Attempting to confirm without approval or a signature. Please contact support.')
|
||||
}
|
||||
|
||||
await estimate(...args)
|
||||
.then(estimatedGasLimit =>
|
||||
method(...args, {
|
||||
gasLimit: calculateGasMargin(estimatedGasLimit)
|
||||
}).then(response => {
|
||||
const safeGasEstimates = await Promise.all(
|
||||
methodNames.map(methodName =>
|
||||
router.estimateGas[methodName](...args)
|
||||
.then(calculateGasMargin)
|
||||
.catch(error => {
|
||||
console.error(`estimateGas failed for ${methodName}`, error)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
|
||||
BigNumber.isBigNumber(safeGasEstimate)
|
||||
)
|
||||
|
||||
// all estimations failed...
|
||||
if (indexOfSuccessfulEstimation === -1) {
|
||||
console.error('This transaction would fail. Please contact support.')
|
||||
} else {
|
||||
const methodName = methodNames[indexOfSuccessfulEstimation]
|
||||
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
|
||||
|
||||
setAttemptingTxn(true)
|
||||
await router[methodName](...args, {
|
||||
gasLimit: safeGasEstimate
|
||||
})
|
||||
.then(response => {
|
||||
setAttemptingTxn(false)
|
||||
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Remove ' +
|
||||
@@ -257,7 +276,6 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
})
|
||||
|
||||
setTxHash(response.hash)
|
||||
setPendingConfirmation(false)
|
||||
|
||||
ReactGA.event({
|
||||
category: 'Liquidity',
|
||||
@@ -265,13 +283,14 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
|
||||
})
|
||||
})
|
||||
)
|
||||
.catch(e => {
|
||||
console.error(e)
|
||||
resetModalState()
|
||||
setShowConfirm(false)
|
||||
setShowAdvanced(false)
|
||||
})
|
||||
.catch(error => {
|
||||
setAttemptingTxn(false)
|
||||
// we only care if the error is something _other_ than the user rejected the tx
|
||||
if (error?.code !== 4001) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function modalHeader() {
|
||||
@@ -348,34 +367,11 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
</RowBetween>
|
||||
</>
|
||||
)}
|
||||
<RowBetween mt="1rem">
|
||||
<ButtonConfirmed
|
||||
onClick={onAttemptToApprove}
|
||||
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
|
||||
mr="0.5rem"
|
||||
fontWeight={500}
|
||||
fontSize={20}
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
|
||||
<ButtonPrimary
|
||||
disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)}
|
||||
onClick={onRemove}
|
||||
ml="0.5rem"
|
||||
>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Confirm
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Confirm
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -398,16 +394,15 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
<ConfirmationModal
|
||||
isOpen={showConfirm}
|
||||
onDismiss={() => {
|
||||
if (attemptingTxn) {
|
||||
history.push('/pool')
|
||||
return
|
||||
}
|
||||
resetModalState()
|
||||
setShowConfirm(false)
|
||||
setShowAdvanced(false)
|
||||
setSignatureData(null) // important that we clear signature data to avoid bad sigs
|
||||
// if there was a tx hash, we want to clear the input
|
||||
if (txHash) {
|
||||
onUserInput(Field.LIQUIDITY_PERCENT, '0')
|
||||
}
|
||||
setTxHash('')
|
||||
}}
|
||||
attemptingTxn={attemptingTxn}
|
||||
pendingConfirmation={pendingConfirmation}
|
||||
hash={txHash ? txHash : ''}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
@@ -561,34 +556,41 @@ export default function RemoveLiquidity({ match: { params }, history }: RouteCom
|
||||
{!account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error || 'Remove'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
<RowBetween>
|
||||
<ButtonConfirmed
|
||||
onClick={onAttemptToApprove}
|
||||
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
|
||||
mr="0.5rem"
|
||||
fontWeight={500}
|
||||
fontSize={16}
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{error || 'Remove'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
)}
|
||||
</div>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{isValid ? (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{pair ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
||||
<PositionCard pair={pair} minimal={true} />
|
||||
|
||||
@@ -2,7 +2,6 @@ import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||
@@ -11,24 +10,24 @@ import Card, { BlueCard, GreyCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||
import { AutoRow, RowBetween } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
|
||||
import SwapModalFooter from '../../components/swap/SwapModalFooter'
|
||||
import { ArrowWrapper, BottomGrouping, Dots, InputGroup, StyledNumerical, Wrapper } from '../../components/swap/styleds'
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import { TransferModalHeader } from '../../components/swap/TransferModalHeader'
|
||||
import V1TradeLink from '../../components/swap/V1TradeLink'
|
||||
import BetterTradeLink from '../../components/swap/BetterTradeLink'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
|
||||
import { INITIAL_ALLOWED_SLIPPAGE, MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
|
||||
import { getTradeVersion, isTradeBetter } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useSendCallback } from '../../hooks/useSendCallback'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
@@ -40,10 +39,12 @@ import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
import { useUserSlippageTolerance, useUserDeadline, useExpertModeManager } from '../../state/user/hooks'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
|
||||
export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURLSearch(search)
|
||||
export default function Send() {
|
||||
// override auto ETH populate to allow for single inputs or swap and send
|
||||
useDefaultsFromURLSearch(true)
|
||||
|
||||
// text translation
|
||||
// const { t } = useTranslation()
|
||||
@@ -53,6 +54,10 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// for expert mode
|
||||
const toggleSettings = useToggleSettingsMenu()
|
||||
const [expertMode] = useExpertModeManager()
|
||||
|
||||
// sending state
|
||||
const [sendingWithSwap, setSendingWithSwap] = useState<boolean>(false)
|
||||
const [recipient, setRecipient] = useState<string>('')
|
||||
@@ -60,29 +65,55 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
const [recipientError, setRecipientError] = useState<string | null>('Enter a Recipient')
|
||||
|
||||
// trade details, check query params for initial state
|
||||
const { independentField, typedValue } = useSwapState()
|
||||
const {
|
||||
parsedAmounts,
|
||||
bestTrade,
|
||||
independentField,
|
||||
typedValue,
|
||||
[Field.OUTPUT]: { address: output }
|
||||
} = useSwapState()
|
||||
|
||||
// if output is valid set to sending view (will reset to undefined on remove swap)
|
||||
useEffect(() => {
|
||||
if (output) {
|
||||
setSendingWithSwap(true)
|
||||
}
|
||||
}, [output])
|
||||
|
||||
const {
|
||||
parsedAmount,
|
||||
bestTrade: bestTradeV2,
|
||||
tokenBalances,
|
||||
tokens,
|
||||
error: swapError,
|
||||
v1TradeLinkIfBetter
|
||||
v1Trade
|
||||
} = useDerivedSwapInfo()
|
||||
const isSwapValid = !swapError && !recipientError && bestTrade
|
||||
|
||||
const toggledVersion = useToggledVersion()
|
||||
const bestTrade = {
|
||||
[Version.v1]: v1Trade,
|
||||
[Version.v2]: bestTradeV2
|
||||
}[toggledVersion]
|
||||
|
||||
const betterTradeLinkVersion: Version | undefined =
|
||||
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
|
||||
? Version.v1
|
||||
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
|
||||
? Version.v2
|
||||
: undefined
|
||||
|
||||
const parsedAmounts = {
|
||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
|
||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
|
||||
}
|
||||
|
||||
const isSwapValid = !swapError && !recipientError && bestTrade
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
// modal and loading
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false)
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
|
||||
|
||||
// txn values
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
const [deadline] = useUserDeadline() // custom from user settings
|
||||
const [allowedSlippage] = useUserSlippageTolerance() // custom from user settings
|
||||
|
||||
const route = bestTrade?.route
|
||||
const userHasSpecifiedInputOutput =
|
||||
@@ -95,6 +126,16 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
// check whether the user has approved the router on the input token
|
||||
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
|
||||
|
||||
// check if user has gone through approval process, used to show two step buttons, reset on token change
|
||||
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
|
||||
|
||||
// mark when a user has submitted an approval, reset onTokenSelection for input field
|
||||
useEffect(() => {
|
||||
if (approval === ApprovalState.PENDING) {
|
||||
setApprovalSubmitted(true)
|
||||
}
|
||||
}, [approval, approvalSubmitted])
|
||||
|
||||
const formattedAmounts = {
|
||||
[independentField]: typedValue,
|
||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
||||
@@ -106,13 +147,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
|
||||
// reset field if sending with with swap is cancled
|
||||
useEffect(() => {
|
||||
if (!sendingWithSwap) {
|
||||
onTokenSelection(Field.OUTPUT, null)
|
||||
}
|
||||
}, [onTokenSelection, sendingWithSwap])
|
||||
|
||||
const maxAmountInput: TokenAmount =
|
||||
!!tokenBalances[Field.INPUT] &&
|
||||
!!tokens[Field.INPUT] &&
|
||||
@@ -127,17 +161,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
const atMaxAmountInput: boolean =
|
||||
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
|
||||
|
||||
// reset modal state when closed
|
||||
function resetModal() {
|
||||
// clear input if txn submitted
|
||||
if (!pendingConfirmation) {
|
||||
onUserInput(Field.INPUT, '')
|
||||
}
|
||||
setPendingConfirmation(true)
|
||||
setAttemptingTxn(false)
|
||||
setShowAdvanced(false)
|
||||
}
|
||||
|
||||
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline, recipient)
|
||||
|
||||
function onSwap() {
|
||||
@@ -146,16 +169,28 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
}
|
||||
|
||||
setAttemptingTxn(true)
|
||||
swapCallback().then(hash => {
|
||||
setTxHash(hash)
|
||||
setPendingConfirmation(false)
|
||||
swapCallback()
|
||||
.then(hash => {
|
||||
setAttemptingTxn(false)
|
||||
setTxHash(hash)
|
||||
|
||||
ReactGA.event({
|
||||
category: 'Send',
|
||||
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
|
||||
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join(';')
|
||||
ReactGA.event({
|
||||
category: 'Send',
|
||||
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
|
||||
label: [
|
||||
bestTrade.inputAmount.token.symbol,
|
||||
bestTrade.outputAmount.token.symbol,
|
||||
getTradeVersion(bestTrade)
|
||||
].join('/')
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
setAttemptingTxn(false)
|
||||
// we only care if the error is something _other_ than the user rejected the tx
|
||||
if (error?.code !== 4001) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const sendCallback = useSendCallback(parsedAmounts?.[Field.INPUT], recipient)
|
||||
@@ -163,16 +198,19 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
|
||||
async function onSend() {
|
||||
setAttemptingTxn(true)
|
||||
|
||||
sendCallback()
|
||||
.then(hash => {
|
||||
setAttemptingTxn(false)
|
||||
setTxHash(hash)
|
||||
|
||||
ReactGA.event({ category: 'Send', action: 'Send', label: tokens[Field.INPUT]?.symbol })
|
||||
setPendingConfirmation(false)
|
||||
})
|
||||
.catch(() => {
|
||||
resetModal()
|
||||
setShowConfirm(false)
|
||||
.catch(error => {
|
||||
setAttemptingTxn(false)
|
||||
// we only care if the error is something _other_ than the user rejected the tx
|
||||
if (error?.code !== 4001) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,6 +219,14 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
// warnings on slippage
|
||||
const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
// show approval buttons when: no errors on input, not approved or pending, or has been approved in this session
|
||||
const showApproveFlow =
|
||||
((sendingWithSwap && isSwapValid) || (!sendingWithSwap && isSendValid)) &&
|
||||
(approval === ApprovalState.NOT_APPROVED ||
|
||||
approval === ApprovalState.PENDING ||
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
|
||||
!(severity > 3 && !expertMode)
|
||||
|
||||
function modalHeader() {
|
||||
if (!sendingWithSwap) {
|
||||
return <TransferModalHeader amount={parsedAmounts?.[Field.INPUT]} ENSName={ENS} recipient={recipient} />
|
||||
@@ -289,11 +335,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
isOpen={showConfirm}
|
||||
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
|
||||
onDismiss={() => {
|
||||
resetModal()
|
||||
setShowConfirm(false)
|
||||
if (txHash) {
|
||||
onUserInput(Field.INPUT, '')
|
||||
}
|
||||
setTxHash('')
|
||||
}}
|
||||
attemptingTxn={attemptingTxn}
|
||||
pendingConfirmation={pendingConfirmation}
|
||||
hash={txHash}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
@@ -363,11 +411,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
onMax={() => {
|
||||
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
|
||||
}}
|
||||
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
|
||||
onTokenSelection={address => {
|
||||
setApprovalSubmitted(false)
|
||||
onTokenSelection(Field.INPUT, address)
|
||||
}}
|
||||
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
|
||||
id="swap-currency-input"
|
||||
/>
|
||||
|
||||
{sendingWithSwap ? (
|
||||
<ColumnCenter>
|
||||
<RowBetween padding="0 1rem 0 12px">
|
||||
@@ -375,7 +425,10 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
<ArrowDown size="16" color={theme.text2} onClick={onSwitchTokens} />
|
||||
</ArrowWrapper>
|
||||
<ButtonSecondary
|
||||
onClick={() => setSendingWithSwap(false)}
|
||||
onClick={() => {
|
||||
setSendingWithSwap(false)
|
||||
onTokenSelection(Field.OUTPUT, null)
|
||||
}}
|
||||
style={{ marginRight: '0px', width: 'auto', fontSize: '14px' }}
|
||||
padding={'4px 6px'}
|
||||
>
|
||||
@@ -400,7 +453,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={onUserInput}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
@@ -430,28 +482,34 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
}}
|
||||
/>
|
||||
</AutoColumn>
|
||||
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
|
||||
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
|
||||
{sendingWithSwap && (
|
||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
Price
|
||||
</Text>
|
||||
<TradePrice showInverted={showInverted} setShowInverted={setShowInverted} trade={bestTrade} />
|
||||
<TradePrice
|
||||
inputToken={tokens[Field.INPUT]}
|
||||
outputToken={tokens[Field.OUTPUT]}
|
||||
price={bestTrade?.executionPrice}
|
||||
showInverted={showInverted}
|
||||
setShowInverted={setShowInverted}
|
||||
/>
|
||||
</RowBetween>
|
||||
|
||||
{bestTrade && severity > 1 && (
|
||||
<RowBetween>
|
||||
<TYPE.main
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
fontSize={14}
|
||||
>
|
||||
Price Impact
|
||||
</TYPE.main>
|
||||
<RowFixed>
|
||||
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
|
||||
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
|
||||
</RowFixed>
|
||||
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
|
||||
<RowBetween align="center">
|
||||
<ClickableText>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
Slippage Tolerance
|
||||
</Text>
|
||||
</ClickableText>
|
||||
<ClickableText>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
{allowedSlippage ? allowedSlippage / 100 : '-'}%
|
||||
</Text>
|
||||
</ClickableText>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
@@ -471,53 +529,64 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
<GreyCard style={{ textAlign: 'center' }}>
|
||||
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
|
||||
</GreyCard>
|
||||
) : approval === ApprovalState.NOT_APPROVED || approval === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveCallback} disabled={approval === ApprovalState.PENDING}>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.INPUT]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : showApproveFlow ? (
|
||||
<RowBetween>
|
||||
<ButtonPrimary
|
||||
onClick={approveCallback}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
|
||||
width="48%"
|
||||
altDisbaledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve ' + tokens[Field.INPUT]?.symbol
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
|
||||
}}
|
||||
width="48%"
|
||||
id="send-button"
|
||||
disabled={approval !== ApprovalState.APPROVED}
|
||||
error={sendingWithSwap && isSwapValid && severity > 2}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{severity > 3 && !expertMode ? `Price Impact High` : `Send${severity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
|
||||
}}
|
||||
id="send-button"
|
||||
disabled={(sendingWithSwap && !isSwapValid) || (!sendingWithSwap && !isSendValid)}
|
||||
disabled={
|
||||
(sendingWithSwap && !isSwapValid) ||
|
||||
(!sendingWithSwap && !isSendValid) ||
|
||||
(severity > 3 && !expertMode && sendingWithSwap)
|
||||
}
|
||||
error={sendingWithSwap && isSwapValid && severity > 2}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{(sendingWithSwap ? swapError : null) ||
|
||||
sendAmountError ||
|
||||
recipientError ||
|
||||
(severity > 3 && !expertMode && `Price Impact Too High`) ||
|
||||
`Send${severity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{priceImpactWithoutFee && severity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
<AdvancedSwapDetailsDropdown trade={bestTrade} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import React, { useContext, useState, useEffect } from 'react'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonError, ButtonLight } from '../../components/Button'
|
||||
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
|
||||
import Card, { GreyCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../../components/Row'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
|
||||
import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds'
|
||||
import SwapModalFooter from '../../components/swap/SwapModalFooter'
|
||||
import SwapModalHeader from '../../components/swap/SwapModalHeader'
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import V1TradeLink from '../../components/swap/V1TradeLink'
|
||||
import BetterTradeLink from '../../components/swap/BetterTradeLink'
|
||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
|
||||
|
||||
import { INITIAL_ALLOWED_SLIPPAGE, MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
|
||||
import { getTradeVersion, isTradeBetter } from '../../data/V1'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
@@ -36,10 +37,10 @@ import {
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
|
||||
export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURLSearch(search)
|
||||
export default function Swap() {
|
||||
useDefaultsFromURLSearch()
|
||||
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
@@ -47,23 +48,43 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// for expert mode
|
||||
const toggleSettings = useToggleSettingsMenu()
|
||||
const [expertMode] = useExpertModeManager()
|
||||
|
||||
// get custom setting values for user
|
||||
const [deadline] = useUserDeadline()
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
// swap state
|
||||
const { independentField, typedValue } = useSwapState()
|
||||
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
|
||||
const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo()
|
||||
const toggledVersion = useToggledVersion()
|
||||
const bestTrade = {
|
||||
[Version.v1]: v1Trade,
|
||||
[Version.v2]: bestTradeV2
|
||||
}[toggledVersion]
|
||||
|
||||
const betterTradeLinkVersion: Version | undefined =
|
||||
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
|
||||
? Version.v1
|
||||
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
|
||||
? Version.v2
|
||||
: undefined
|
||||
|
||||
const parsedAmounts = {
|
||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
|
||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
|
||||
}
|
||||
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
const isValid = !error
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
// modal and loading
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false)
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
|
||||
|
||||
// txn values
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
|
||||
const formattedAmounts = {
|
||||
[independentField]: typedValue,
|
||||
@@ -81,6 +102,16 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// check whether the user has approved the router on the input token
|
||||
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
|
||||
|
||||
// check if user has gone through approval process, used to show two step buttons, reset on token change
|
||||
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
|
||||
|
||||
// mark when a user has submitted an approval, reset onTokenSelection for input field
|
||||
useEffect(() => {
|
||||
if (approval === ApprovalState.PENDING) {
|
||||
setApprovalSubmitted(true)
|
||||
}
|
||||
}, [approval, approvalSubmitted])
|
||||
|
||||
const maxAmountInput: TokenAmount =
|
||||
!!tokenBalances[Field.INPUT] &&
|
||||
!!tokens[Field.INPUT] &&
|
||||
@@ -97,17 +128,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
|
||||
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
|
||||
|
||||
// reset modal state when closed
|
||||
function resetModal() {
|
||||
// clear input if txn submitted
|
||||
if (!pendingConfirmation) {
|
||||
onUserInput(Field.INPUT, '')
|
||||
}
|
||||
setPendingConfirmation(true)
|
||||
setAttemptingTxn(false)
|
||||
setShowAdvanced(false)
|
||||
}
|
||||
|
||||
// the callback to execute the swap
|
||||
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline)
|
||||
|
||||
@@ -117,18 +137,29 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
|
||||
return
|
||||
}
|
||||
|
||||
setAttemptingTxn(true)
|
||||
swapCallback().then(hash => {
|
||||
setTxHash(hash)
|
||||
setPendingConfirmation(false)
|
||||
swapCallback()
|
||||
.then(hash => {
|
||||
setAttemptingTxn(false)
|
||||
setTxHash(hash)
|
||||
|
||||
ReactGA.event({
|
||||
category: 'Swap',
|
||||
action: 'Swap w/o Send',
|
||||
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
|
||||
ReactGA.event({
|
||||
category: 'Swap',
|
||||
action: 'Swap w/o Send',
|
||||
label: [
|
||||
bestTrade.inputAmount.token.symbol,
|
||||
bestTrade.outputAmount.token.symbol,
|
||||
getTradeVersion(bestTrade)
|
||||
].join('/')
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
setAttemptingTxn(false)
|
||||
// we only care if the error is something _other_ than the user rejected the tx
|
||||
if (error?.code !== 4001) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// errors
|
||||
@@ -137,14 +168,23 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// warnings on slippage
|
||||
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
|
||||
// never show if price impact is above threshold in non expert mode
|
||||
const showApproveFlow =
|
||||
!error &&
|
||||
(approval === ApprovalState.NOT_APPROVED ||
|
||||
approval === ApprovalState.PENDING ||
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
|
||||
!(priceImpactSeverity > 3 && !expertMode)
|
||||
|
||||
function modalHeader() {
|
||||
return (
|
||||
<SwapModalHeader
|
||||
independentField={independentField}
|
||||
priceImpactSeverity={priceImpactSeverity}
|
||||
tokens={tokens}
|
||||
formattedAmounts={formattedAmounts}
|
||||
slippageAdjustedAmounts={slippageAdjustedAmounts}
|
||||
priceImpactSeverity={priceImpactSeverity}
|
||||
independentField={independentField}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -180,11 +220,14 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
isOpen={showConfirm}
|
||||
title="Confirm Swap"
|
||||
onDismiss={() => {
|
||||
resetModal()
|
||||
setShowConfirm(false)
|
||||
// if there was a tx hash, we want to clear the input
|
||||
if (txHash) {
|
||||
onUserInput(Field.INPUT, '')
|
||||
}
|
||||
setTxHash('')
|
||||
}}
|
||||
attemptingTxn={attemptingTxn}
|
||||
pendingConfirmation={pendingConfirmation}
|
||||
hash={txHash}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
@@ -203,7 +246,10 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
onMax={() => {
|
||||
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
|
||||
}}
|
||||
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
|
||||
onTokenSelection={address => {
|
||||
setApprovalSubmitted(false) // reset 2 step UI for approvals
|
||||
onTokenSelection(Field.INPUT, address)
|
||||
}}
|
||||
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
|
||||
id="swap-currency-input"
|
||||
/>
|
||||
@@ -213,18 +259,19 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
<ArrowWrapper>
|
||||
<ArrowDown
|
||||
size="16"
|
||||
onClick={onSwitchTokens}
|
||||
onClick={() => {
|
||||
setApprovalSubmitted(false) // reset 2 step UI for approvals
|
||||
onSwitchTokens()
|
||||
}}
|
||||
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
|
||||
/>
|
||||
</ArrowWrapper>
|
||||
</AutoColumn>
|
||||
</CursorPointer>
|
||||
|
||||
<CurrencyInputPanel
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={onUserInput}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
@@ -234,33 +281,33 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
/>
|
||||
</>
|
||||
|
||||
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
|
||||
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
Price
|
||||
</Text>
|
||||
<TradePrice trade={bestTrade} showInverted={showInverted} setShowInverted={setShowInverted} />
|
||||
</RowBetween>
|
||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
Price
|
||||
</Text>
|
||||
<TradePrice
|
||||
inputToken={tokens[Field.INPUT]}
|
||||
outputToken={tokens[Field.OUTPUT]}
|
||||
price={bestTrade?.executionPrice}
|
||||
showInverted={showInverted}
|
||||
setShowInverted={setShowInverted}
|
||||
/>
|
||||
</RowBetween>
|
||||
|
||||
{bestTrade && priceImpactSeverity > 1 && (
|
||||
<RowBetween>
|
||||
<TYPE.main
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
fontSize={14}
|
||||
>
|
||||
Price Impact
|
||||
</TYPE.main>
|
||||
<RowFixed>
|
||||
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
|
||||
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
)}
|
||||
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
|
||||
<RowBetween align="center">
|
||||
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
Slippage Tolerance
|
||||
</ClickableText>
|
||||
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
{allowedSlippage ? allowedSlippage / 100 : '-'}%
|
||||
</ClickableText>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
</AutoColumn>
|
||||
<BottomGrouping>
|
||||
{!account ? (
|
||||
@@ -269,50 +316,62 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
<GreyCard style={{ textAlign: 'center' }}>
|
||||
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
|
||||
</GreyCard>
|
||||
) : approval === ApprovalState.NOT_APPROVED || approval === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveCallback} disabled={approval === ApprovalState.PENDING}>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.INPUT]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : showApproveFlow ? (
|
||||
<RowBetween>
|
||||
<ButtonPrimary
|
||||
onClick={approveCallback}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
|
||||
width="48%"
|
||||
altDisbaledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve ' + tokens[Field.INPUT]?.symbol
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
expertMode ? onSwap() : setShowConfirm(true)
|
||||
}}
|
||||
width="48%"
|
||||
id="swap-button"
|
||||
disabled={!isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !expertMode)}
|
||||
error={isValid && priceImpactSeverity > 2}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{priceImpactSeverity > 3 && !expertMode
|
||||
? `Price Impact High`
|
||||
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
expertMode ? onSwap() : setShowConfirm(true)
|
||||
}}
|
||||
id="swap-button"
|
||||
disabled={!isValid}
|
||||
disabled={!isValid || (priceImpactSeverity > 3 && !expertMode)}
|
||||
error={isValid && priceImpactSeverity > 2}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
{error
|
||||
? error
|
||||
: priceImpactSeverity > 3 && !expertMode
|
||||
? `Price Impact Too High`
|
||||
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{priceImpactWithoutFee && priceImpactSeverity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
<AdvancedSwapDetailsDropdown trade={bestTrade} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@ export type PopupContent =
|
||||
|
||||
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
|
||||
export const toggleWalletModal = createAction<void>('toggleWalletModal')
|
||||
export const addPopup = createAction<{ content: PopupContent }>('addPopup')
|
||||
export const toggleSettingsMenu = createAction<void>('toggleSettingsMenu')
|
||||
export const addPopup = createAction<{ key?: string; content: PopupContent }>('addPopup')
|
||||
export const removePopup = createAction<{ key: string }>('removePopup')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { addPopup, PopupContent, removePopup, toggleWalletModal } from './actions'
|
||||
import { addPopup, PopupContent, removePopup, toggleWalletModal, toggleSettingsMenu } from './actions'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { AppState } from '../index'
|
||||
|
||||
@@ -19,13 +19,22 @@ export function useWalletModalToggle(): () => void {
|
||||
return useCallback(() => dispatch(toggleWalletModal()), [dispatch])
|
||||
}
|
||||
|
||||
export function useSettingsMenuOpen(): boolean {
|
||||
return useSelector((state: AppState) => state.application.settingsMenuOpen)
|
||||
}
|
||||
|
||||
export function useToggleSettingsMenu(): () => void {
|
||||
const dispatch = useDispatch()
|
||||
return useCallback(() => dispatch(toggleSettingsMenu()), [dispatch])
|
||||
}
|
||||
|
||||
// returns a function that allows adding a popup
|
||||
export function useAddPopup(): (content: PopupContent) => void {
|
||||
export function useAddPopup(): (content: PopupContent, key?: string) => void {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
return useCallback(
|
||||
(content: PopupContent) => {
|
||||
dispatch(addPopup({ content }))
|
||||
(content: PopupContent, key?: string) => {
|
||||
dispatch(addPopup({ content, key }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { createReducer, nanoid } from '@reduxjs/toolkit'
|
||||
import { addPopup, PopupContent, removePopup, toggleWalletModal, updateBlockNumber } from './actions'
|
||||
import {
|
||||
addPopup,
|
||||
PopupContent,
|
||||
removePopup,
|
||||
toggleWalletModal,
|
||||
toggleSettingsMenu,
|
||||
updateBlockNumber
|
||||
} from './actions'
|
||||
|
||||
type PopupList = Array<{ key: string; show: boolean; content: PopupContent }>
|
||||
|
||||
@@ -7,12 +14,14 @@ interface ApplicationState {
|
||||
blockNumber: { [chainId: number]: number }
|
||||
popupList: PopupList
|
||||
walletModalOpen: boolean
|
||||
settingsMenuOpen: boolean
|
||||
}
|
||||
|
||||
const initialState: ApplicationState = {
|
||||
blockNumber: {},
|
||||
popupList: [],
|
||||
walletModalOpen: false
|
||||
walletModalOpen: false,
|
||||
settingsMenuOpen: false
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
@@ -28,9 +37,13 @@ export default createReducer(initialState, builder =>
|
||||
.addCase(toggleWalletModal, state => {
|
||||
state.walletModalOpen = !state.walletModalOpen
|
||||
})
|
||||
.addCase(addPopup, (state, { payload: { content } }) => {
|
||||
.addCase(toggleSettingsMenu, state => {
|
||||
state.settingsMenuOpen = !state.settingsMenuOpen
|
||||
})
|
||||
.addCase(addPopup, (state, { payload: { content, key } }) => {
|
||||
if (key && state.popupList.some(popup => popup.key === key)) return
|
||||
state.popupList.push({
|
||||
key: nanoid(),
|
||||
key: key || nanoid(),
|
||||
show: true,
|
||||
content
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
|
||||
@@ -10,41 +10,48 @@ export default function Updater() {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const windowVisible = useIsWindowVisible()
|
||||
const [maxBlockNumber, setMaxBlockNumber] = useState<number | null>(null)
|
||||
// because blocks arrive in bunches with longer polling periods, we just want
|
||||
// to process the latest one.
|
||||
const debouncedMaxBlockNumber = useDebounce<number | null>(maxBlockNumber, 100)
|
||||
|
||||
// update block number
|
||||
useEffect(() => {
|
||||
if (!library || !chainId) return
|
||||
const [state, setState] = useState<{ chainId: number | undefined; blockNumber: number | null }>({
|
||||
chainId,
|
||||
blockNumber: null
|
||||
})
|
||||
|
||||
const blockListener = (blockNumber: number) => {
|
||||
setMaxBlockNumber(maxBlockNumber => {
|
||||
if (typeof maxBlockNumber !== 'number') return blockNumber
|
||||
return Math.max(maxBlockNumber, blockNumber)
|
||||
const blockNumberCallback = useCallback(
|
||||
(blockNumber: number) => {
|
||||
setState(state => {
|
||||
if (chainId === state.chainId) {
|
||||
if (typeof state.blockNumber !== 'number') return { chainId, blockNumber }
|
||||
return { chainId, blockNumber: Math.max(blockNumber, state.blockNumber) }
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
},
|
||||
[chainId, setState]
|
||||
)
|
||||
|
||||
setMaxBlockNumber(null)
|
||||
// attach/detach listeners
|
||||
useEffect(() => {
|
||||
if (!library || !chainId || !windowVisible) return
|
||||
|
||||
setState({ chainId, blockNumber: null })
|
||||
|
||||
library
|
||||
.getBlockNumber()
|
||||
.then(blockNumber => dispatch(updateBlockNumber({ chainId, blockNumber })))
|
||||
.catch(error => console.error(`Failed to get block number for chainId ${chainId}`, error))
|
||||
.then(blockNumberCallback)
|
||||
.catch(error => console.error(`Failed to get block number for chainId: ${chainId}`, error))
|
||||
|
||||
library.on('block', blockListener)
|
||||
library.on('block', blockNumberCallback)
|
||||
return () => {
|
||||
library.removeListener('block', blockListener)
|
||||
library.removeListener('block', blockNumberCallback)
|
||||
}
|
||||
}, [dispatch, chainId, library])
|
||||
}, [dispatch, chainId, library, blockNumberCallback, windowVisible])
|
||||
|
||||
const debouncedState = useDebounce(state, 100)
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainId || !debouncedMaxBlockNumber) return
|
||||
if (windowVisible) {
|
||||
dispatch(updateBlockNumber({ chainId, blockNumber: debouncedMaxBlockNumber }))
|
||||
}
|
||||
}, [chainId, debouncedMaxBlockNumber, windowVisible, dispatch])
|
||||
if (!debouncedState.chainId || !debouncedState.blockNumber || !windowVisible) return
|
||||
dispatch(updateBlockNumber({ chainId: debouncedState.chainId, blockNumber: debouncedState.blockNumber }))
|
||||
}, [windowVisible, dispatch, debouncedState.blockNumber, debouncedState.chainId])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useActiveWeb3React } from '../../hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { Field, typeInput } from './actions'
|
||||
import { setDefaultsFromURLMatchParams } from '../mint/actions'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { Token, Pair, TokenAmount, Percent, JSBI, Route } from '@uniswap/sdk'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTokenBalances } from '../wallet/hooks'
|
||||
@@ -40,12 +40,12 @@ export function useDerivedBurnInfo(): {
|
||||
} = useBurnState()
|
||||
|
||||
// tokens
|
||||
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
|
||||
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
|
||||
const tokenA = useToken(tokenAAddress)
|
||||
const tokenB = useToken(tokenBAddress)
|
||||
const tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token } = useMemo(
|
||||
() => ({
|
||||
[Field.TOKEN_A]: tokenA,
|
||||
[Field.TOKEN_B]: tokenB
|
||||
[Field.TOKEN_A]: tokenA ?? undefined,
|
||||
[Field.TOKEN_B]: tokenB ?? undefined
|
||||
}),
|
||||
[tokenA, tokenB]
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Token, TokenAmount, Route, JSBI, Price, Percent, Pair } from '@uniswap/
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { setDefaultsFromURLMatchParams, Field, typeInput } from './actions'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
@@ -42,8 +42,8 @@ export function useDerivedMintInfo(): {
|
||||
const dependentField = independentField === Field.TOKEN_A ? Field.TOKEN_B : Field.TOKEN_A
|
||||
|
||||
// tokens
|
||||
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
|
||||
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
|
||||
const tokenA = useToken(tokenAAddress)
|
||||
const tokenB = useToken(tokenBAddress)
|
||||
const tokens: { [field in Field]?: Token } = useMemo(
|
||||
() => ({
|
||||
[Field.TOKEN_A]: tokenA,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user