Compare commits

..

52 Commits

Author SHA1 Message Date
Moody Salem
f33bba2176 chore(token lists): Bump token lists spec version 2020-09-01 11:37:53 -05:00
Moody Salem
fb31c75838 test failed due to randomness. make it less likely to fail. 2020-09-01 11:28:18 -05:00
Moody Salem
b8c383c20e fix(#1083): fix wrapping of tokens added/removed in list update popup 2020-09-01 10:45:26 -05:00
Tran Vinh Quang
fbf39e4932 Update vi.json (#1057) 2020-08-31 15:23:02 -05:00
Ian Lapham
975570fa97 improvement(swap): progress bar and more minimal default UI, also fix custom add/remove (#1069)
* add progress bar and minimal UI updates on swap

* add hook to explicity check user added tokens, fixes add/remove bug

* update with latest

* remove confusing comment

* update styles on loading, update arrow placement, code cleanup

* fix typo on progress import
2020-08-31 15:27:30 -04:00
Moody Salem
d6aa0e98a4 chore(token lists): replace aave token list with ens name 2020-08-28 10:52:21 -05:00
Ian Lapham
4644cd7b0a update missing logo icon (#1070) 2020-08-27 20:49:28 -04:00
Moody Salem
9ddedd8dab fix(pending approves): pending approves that are too old should not cause 'approving' to get stuck 2020-08-27 15:34:04 -05:00
Moody Salem
95030a52c5 fix(remove liquidity): price display in remove liquidity incorrect 2020-08-27 14:26:23 -05:00
Moody Salem
1911f72536 improvement(swap): show better trade link if trade doesn't exist on one version 2020-08-27 13:25:25 -05:00
Moody Salem
85217452db improvement(ts): strict everywhere 2020-08-27 13:10:00 -05:00
Moody Salem
f7a1a2ab58 move noImplicitAny and some type declarations 2020-08-27 12:24:03 -05:00
Moody Salem
66a2006284 more strictness everywhere, fix a pair pricing issue in mint/hooks.ts 2020-08-27 12:05:09 -05:00
Moody Salem
610b7f4464 make integration tests pass more reliably, some reducer refactoring 2020-08-27 10:21:51 -05:00
Callil Capuozzo
ce12635332 Merge branch 'master' of https://github.com/Uniswap/uniswap-interface 2020-08-26 15:15:06 -04:00
Callil Capuozzo
2182e18f85 Add coingecko and tweak list introduction screen 2020-08-26 15:14:48 -04:00
Moody Salem
ad2c7dfdff add 3 more lists 2020-08-26 13:36:55 -05:00
Moody Salem
cb36c9103e fix integration tests, update default list 2020-08-26 11:43:10 -05:00
Moody Salem
0a1459ee83 remove any bias from the list selection 2020-08-26 10:47:55 -05:00
Moody Salem
8896a042f0 title for list URL only on list origin 2020-08-26 10:14:38 -05:00
Moody Salem
61ad07c3f2 add zerion list 2020-08-26 10:10:57 -05:00
Moody Salem
81a5164d99 fix the browse lists link 2020-08-26 09:34:46 -05:00
Moody Salem
467e80a42f improvement(#1043): do not allow swapping to bad addresses 2020-08-26 09:19:59 -05:00
Moody Salem
58f25aa439 another list 2020-08-26 08:54:33 -05:00
Moody Salem
377c71f2e5 bump to latest token lists version 2020-08-26 08:48:59 -05:00
Moody Salem
7cf25ac7c8 feat(lists): allow selecting and adding token lists (#1023)
* more list stuff

Use the selected list instead of the default list, but also use the default list

start list selection code

* move token warning to a modal, fix the install issue

* add/remove/enter key

* handle enter on currency select for ETHER

* change slippage tolerance to be a slider

* make ui closer to the mocks

* commit slider changes

* back to tabs

* copy changes

* bump list version

* some styling for the list select

* bump uniswap default list version

* use contract calls to get ens names and addresses

* show list logo

* fix failing integration test

* .eth.link

* list introduction screen

* remove showSendWithSwap

* fix integration and unit tests

* resolve ENS names

* logos from ens

* fix the lint errors

* some refactoring to better support using a the library provider from the user for resolving ENS names

* load list info from the list url for the introduction page

* make it slightly harder to remove a list

* minor clean up, some help text and links

* remove icon from list update popup

* show added/removed tokens

* add GA everywhere, don't debounce contenthash lookups

* show tags

* fix tag key

* tag display, list rendering, needs optimization

* fix list fetching in firefox, style issue in safari

* sort the lists, clean up styling

* use client provider when possible

* show token warning for url loaded tokens

* improve the warning modal

* some refactoring to fix the list fetching on networks other than mainnet

* fix tests

* some minor improvements

* increase timeout to maybe fix integration tests which pass locally

* build for tests using the dev network url

* reset the lists if we deleted the other two copies

* improve how we handle updating the default list of lists

* fix integration test

* Update token list selection styles

* fix external links, reuse the on click outside code, show add errors

* show the list origin instead of the full url

* fix update list link

* show host instead of hostname
do not automatically dismiss major version upgrades for lists

* fix link to tokenlists.org

* add uma

* clean up styling in list rows

* bump token list version

* bump token list version again

* hover symbol to see currency name

* bump version

* add cmc lists, dharma list

Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
2020-08-26 08:46:21 -05:00
Moody Salem
09b54570e1 fix integration test for recipient 2020-08-24 13:52:41 -05:00
Moody Salem
73580de922 improvement(swap): show add a send only for expert mode 2020-08-24 13:33:46 -05:00
Moody Salem
e32fd3a8fc disable blank issues 2020-08-18 10:26:33 -05:00
Moody Salem
057417c666 add a snippet to the issue templates 2020-08-18 10:25:02 -05:00
Moody Salem
f1b300af70 add a hook for getting the USDC price of any currency 2020-08-17 08:36:38 -05:00
Moody Salem
600049bc6e fix(list): change the url of the default token list so we can move the file in the npm package 2020-08-11 14:44:25 -05:00
Moody Salem
6e91311489 remove console.log statement 2020-08-10 15:11:05 -05:00
Moody Salem
f6a464cb3b fix(ampl): do not swap ampl via pairs other than DAI/ETH 2020-08-10 15:05:41 -05:00
Moody Salem
e589c751d7 improvement(swap errors): show more information about the swap error 2020-08-10 12:05:35 -05:00
Moody Salem
0f91af1df2 improvement(swap): Better swap errors for FoT (#1015)
* move the gas estimation stuff into its own hook and report errors from the gas estimation

* fix linter errors

* show the swap callback error separately

* rename some variables

* use a manually specified key for gas estimates

* flip price... thought i did this already

* only show swap callback error if approval state is approved

* some clean up to the swap components

* stop proactively looking for gas estimates

* improve some retry stuff, show errors inline

* add another retry test

* latest ethers

* fix integration tests

* simplify modal and fix jitter on open in mobile

* refactor confirmation modal into pieces before creating the error content

* finish refactoring of transaction confirmation modal

* show error state in the transaction confirmation modal

* fix lint errors

* error not always relevant

* fix lint errors, remove action item

* move a lot of code into ConfirmSwapModal.tsx

* show accept changes flow, not styled

* Adjust styles for slippage error states

* Add styles for updated price prompt

* Add input/output highlighting

* lint errors

* fix link to wallets in modal

* use total supply instead of reserves for `noLiquidity` (fixes #701)

* bump the walletconnect version to the fixed alpha

Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
2020-08-06 18:18:43 -05:00
Moody Salem
10ef04510a fix(fonts): font-display in non-font-variation-settings conditioned css 2020-07-31 12:12:42 -05:00
Moody Salem
e3b3d9e825 fix(swaps): band-aid fix for gas estimates to disable multihop for eth-ampl 2020-07-31 11:50:30 -05:00
Moody Salem
3050e967f7 fix(token lists): automatic updates to token lists 2020-07-30 18:32:14 -05:00
Ian Lapham
2150450760 improvement(token warnings): show better warnings for imported tokens (#1005)
* add updated ui warnings for imported tokens

* remove useless styling

* update to surpress on default tokens

* add integration tests for warning cards on token import

* remove callbacks as props in token warning card
2020-07-30 15:21:37 -04:00
Moody Salem
1b07e95885 fix(add liquidity): fix the mint hooks to return a price as well as return the dependent amount in the input currency (#1011) 2020-07-28 17:04:33 -05:00
Moody Salem
9bb50d6a7b unit tests for the uri to http method 2020-07-28 08:10:52 -05:00
Moody Salem
b08bb7eaff add an integration test 2020-07-27 13:29:25 -05:00
Moody Salem
3a36ac5538 chore(release): update dns again 2020-07-27 12:33:31 -05:00
Moody Salem
2962cd0e14 fix(migrate v1): migrate v1 pages and formatting 2020-07-27 12:33:02 -05:00
Moody Salem
6a311aa6d7 fix(v1 swap): exact out swaps not working 2020-07-27 08:45:48 -05:00
Moody Salem
e78b6d61f2 improvement(transactions): some clean up and unit tests
- fetch transaction state less often for old transactions
- fix a bug calling non payable methods with value 0
2020-07-27 08:45:48 -05:00
Moody Salem
365b429c0b feat(token lists): implement the uniswap default list as a token list (#983)
* load tokens from url `useTokenList`

* improve performance of the loading

* move the loading to redux and save loaded lists

* lint error

* move the list fetching code to a separate component

* change how token lists are fetched to use the updater and add unit tests

* fix a crash with currencyEquals

* bump sdk version

* token lists should automatically update for minor/patch changes

* nit

* show popups for list updates

* support pointing at localhost

* spuport ipfs/ipns logos

* use the updater to bump list versions

* save the old/new list in the popup for viewing diffs

* improve the list popup

* fix linter error, make sure visibility checking is working

* show list update notifications

* address a couple metamask warnings, linter error

* fix the custom added/default tokens

* refactor some popup stuff to reuse the fader

* linter error

* Revert: refactor some popup stuff to reuse the fader (a7b0f752)

* style improvements, linter

* add to the readme, drop the token-request template

* back to the beta that works with wallet connect

* get the dependencies to a state that works with wallet connect and passes integration tests
2020-07-25 10:41:03 -05:00
Moody Salem
32d300009e just bump the polling interval, aiming to have same # of blockNumber requests as calls or less 2020-07-21 15:57:09 -05:00
Moody Salem
806623c602 save some calls on the redundant chain id requests 2020-07-21 15:43:52 -05:00
Moody Salem
3272f8e9db chore(infura): rotate keys (complete) 2020-07-21 11:07:10 -05:00
Moody Salem
010ef108eb chore(infura): rotate keys 2020-07-21 11:05:55 -05:00
188 changed files with 7521 additions and 3050 deletions

2
.env
View File

@@ -1,2 +1,2 @@
REACT_APP_CHAIN_ID="1"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/acb7e55995d04c49bfb52b7141599467"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"

View File

@@ -1,5 +1,5 @@
REACT_APP_CHAIN_ID="1"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/febcb10ca2754433a61e0805bc6c047d"
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"

View File

@@ -4,11 +4,18 @@ about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
<!--
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
YOUR ISSUE WILL BE DELETED.
SEE https://github.com/Uniswap/default-token-list#adding-a-token
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
-->
**Bug Description**
A clear and concise description of what the bug is.
A clear and concise description of the bug.
**Steps to Reproduce**
1. Go to ...

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -4,9 +4,16 @@ about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
<!--
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
YOUR ISSUE WILL BE DELETED.
SEE https://github.com/Uniswap/default-token-list#adding-a-token
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@@ -4,7 +4,15 @@ about: Tell us something else
title: ''
labels: ''
assignees: ''
---
<!--
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
YOUR ISSUE WILL BE DELETED.
SEE https://github.com/Uniswap/default-token-list#adding-a-token
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
-->

View File

@@ -1,27 +0,0 @@
---
name: Token Request
about: Request a token addition
title: ''
labels: token request
assignees: ''
---
**Please provide the following information for your token.**
Token Address:
Token Name (from contract):
Token Decimals (from contract):
Token Symbol (from contract):
Uniswap Exchange Address of Token:
Link to the official homepage of token:
Link to CoinMarketCap or CoinGecko page of token:
Some tokens (e.g. BNB) do not work with Uniswap v1. In order to assess if your token works correctly, please complete small-value transactions of each of the types below, and submit the Etherscan transaction links for our review.
Test `addLiquidity` transaction:
Test `swap` transaction:
Test `removeLiquidity` transaction:
Are you willing to add liquidity to the liquidity pool for this token? (Y/N):
If so, how much liquidity are you willing to add?:

View File

@@ -59,15 +59,15 @@ jobs:
with:
cidv0: ${{ steps.upload.outputs.hash }}
# - name: Update DNS with new IPFS hash
# 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:
# cid: ${{ steps.upload.outputs.hash }}
- name: Update DNS with new IPFS hash
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:
cid: ${{ steps.upload.outputs.hash }}
- name: Create GitHub Release
id: create_release

View File

@@ -29,6 +29,8 @@ jobs:
- run: yarn install --frozen-lockfile
- run: yarn cypress install
- run: yarn build
env:
REACT_APP_NETWORK_URL: "https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"
- run: yarn integration-test
unit-tests:

View File

@@ -20,6 +20,12 @@ 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).
## Listing a token
Please see the
[@uniswap/default-token-list](https://github.com/uniswap/default-token-list)
repository.
## Development
### Install Dependencies

View File

@@ -3,5 +3,6 @@
"pluginsFile": false,
"fixturesFolder": false,
"supportFile": "cypress/support/index.js",
"video": false
"video": false,
"defaultCommandTimeout": 10000
}

View File

@@ -0,0 +1,24 @@
describe('Swap', () => {
beforeEach(() => {
cy.visit('/swap')
})
it('list selection persists', () => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get('#list-introduction-choose-a-list').click()
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
cy.reload()
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get('#list-introduction-choose-a-list').should('not.exist')
})
it('change list', () => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get('#list-introduction-choose-a-list').click()
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
cy.get('#currency-search-change-list-button').click()
cy.get('#list-row-tokens-1inch-eth .select-button').click()
cy.get('#currency-search-selected-list-name').should('contain', '1inch')
})
})

View File

@@ -0,0 +1,8 @@
describe('Migrate V1 Liquidity', () => {
describe('Remove V1 liquidity', () => {
it('renders the correct page', () => {
cy.visit('/remove/v1/0x93bB63aFe1E0180d0eF100D774B473034fd60C36')
cy.get('#remove-v1-exchange').should('contain', 'MKR/ETH')
})
})
})

View File

@@ -1,5 +1,7 @@
describe('Swap', () => {
beforeEach(() => cy.visit('/swap'))
beforeEach(() => {
cy.visit('/swap')
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input')
.type('0.001', { delay: 200 })
@@ -32,6 +34,8 @@ describe('Swap', () => {
it('can swap ETH for DAI', () => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get('#list-introduction-choose-a-list').click()
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
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')
@@ -41,14 +45,33 @@ describe('Swap', () => {
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
})
it('add a recipient', () => {
cy.get('#add-recipient-button').click()
cy.get('#recipient').should('exist')
it('add a recipient does not exist unless in expert mode', () => {
cy.get('#add-recipient-button').should('not.exist')
})
it('remove recipient', () => {
cy.get('#add-recipient-button').click()
cy.get('#remove-recipient-button').click()
cy.get('#recipient').should('not.exist')
describe('expert mode', () => {
beforeEach(() => {
cy.window().then(win => {
cy.stub(win, 'prompt').returns('confirm')
})
cy.get('#open-settings-dialog-button').click()
cy.get('#toggle-expert-mode-button').click()
cy.get('#confirm-expert-mode').click()
})
it('add a recipient is visible', () => {
cy.get('#add-recipient-button').should('be.visible')
})
it('add a recipient', () => {
cy.get('#add-recipient-button').click()
cy.get('#recipient').should('exist')
})
it('remove recipient', () => {
cy.get('#add-recipient-button').click()
cy.get('#remove-recipient-button').click()
cy.get('#recipient').should('not.exist')
})
})
})

View File

@@ -0,0 +1,17 @@
describe('Warning', () => {
beforeEach(() => {
cy.visit('/swap?outputCurrency=0x0a40f26d74274b7f22b28556a27b35d97ce08e0a')
})
it('Check that warning is displayed', () => {
cy.get('.token-warning-container').should('be.visible')
})
it('Check that warning hides after button dismissal', () => {
cy.get('.token-dismiss-button').should('be.disabled')
cy.get('.understand-checkbox').click()
cy.get('.token-dismiss-button').should('not.be.disabled')
cy.get('.token-dismiss-button').click()
cy.get('.token-warning-container').should('not.be.visible')
})
})

View File

@@ -73,9 +73,10 @@ Cypress.Commands.overwrite('visit', (original, url, options) => {
...options,
onBeforeLoad(win) {
options && options.onBeforeLoad && options.onBeforeLoad(win)
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
win.localStorage.clear()
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
win.ethereum = new CustomizedBridge(signer, provider)
},
}
})
})

View File

@@ -4,34 +4,30 @@
"homepage": ".",
"private": true,
"devDependencies": {
"@ethersproject/address": "^5.0.1",
"@ethersproject/bignumber": "^5.0.3",
"@ethersproject/constants": "^5.0.1",
"@ethersproject/contracts": "^5.0.1",
"@ethersproject/experimental": "^5.0.0",
"@ethersproject/providers": "^5.0.4",
"@ethersproject/strings": "^5.0.1",
"@ethersproject/units": "^5.0.1",
"@ethersproject/wallet": "^5.0.1",
"@ethersproject/experimental": "^5.0.1",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@reduxjs/toolkit": "^1.3.5",
"@types/jest": "^25.2.1",
"@types/lodash.flatmap": "^4.5.6",
"@types/multicodec": "^1.0.0",
"@types/node": "^13.13.5",
"@types/qs": "^6.9.2",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.7",
"@types/react-redux": "^7.1.8",
"@types/react-router-dom": "^5.0.0",
"@types/react-virtualized-auto-sizer": "^1.0.0",
"@types/react-window": "^1.8.2",
"@types/rebass": "^4.0.5",
"@types/styled-components": "^5.1.0",
"@types/testing-library__cypress": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"@uniswap/sdk": "3.0.1",
"@uniswap/default-token-list": "^1.3.1",
"@uniswap/sdk": "3.0.3-beta.1",
"@uniswap/token-lists": "^1.0.0-beta.16",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@web3-react/core": "^6.0.9",
@@ -40,23 +36,27 @@
"@web3-react/portis-connector": "^6.0.9",
"@web3-react/walletconnect-connector": "^6.1.1",
"@web3-react/walletlink-connector": "^6.0.9",
"ajv": "^6.12.3",
"cids": "^1.0.0",
"copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.2",
"cypress": "^4.5.0",
"cypress": "^4.11.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0",
"ethers": "^5.0.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",
"multicodec": "^2.0.0",
"multihashes": "^3.0.1",
"polished": "^3.3.2",
"prettier": "^1.17.0",
"qrcode.react": "^0.9.3",
"qs": "^6.9.4",
"react": "^16.13.1",
"react-device-detect": "^1.6.2",
@@ -70,14 +70,17 @@
"react-scripts": "^3.4.1",
"react-spring": "^8.0.27",
"react-use-gesture": "^6.0.14",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5",
"rebass": "^4.0.7",
"redux-localstorage-simple": "^2.2.0",
"redux-localstorage-simple": "^2.3.1",
"serve": "^11.3.0",
"start-server-and-test": "^1.11.0",
"styled-components": "^4.2.0",
"typescript": "^3.8.3",
"use-media": "^1.4.0"
"typescript": "^3.8.3"
},
"resolutions": {
"@walletconnect/web3-provider": "1.1.1-alpha.0"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -19,8 +19,8 @@
"pending": "Đang chờ xử lý",
"selectToken": "Chọn một đồng tiền ảo",
"searchOrPaste": "Tìm kiếm tên, biểu tượng, hoặc địa chỉ của đồng tiền ảo",
"searchOrPasteMobile": "Tên, Biểu tương, hoặc Địa chỉ",
"noExchange": "Không Tìm Thấy Giao Dịch",
"searchOrPasteMobile": "Tên, Biểu tưng, hoặc Địa chỉ",
"noExchange": "Không tìm thấy giao dịch",
"exchangeRate": "Tỷ giá",
"unknownError": "Rất tiếc! Xảy ra lỗi không xác định. Vui lòng làm mới trang, hoặc truy cập từ trình duyệt hay thiết bị khác.",
"enterValueCont": "Nhập một giá trị {{ missingCurrencyValue }} để tiếp tục.",
@@ -29,7 +29,7 @@
"insufficientLiquidity": "Không đủ tính thanh khoản.",
"unlockTokenCont": "Vui lòng mở khoá đồng tiền ảo để tiếp tục",
"transactionDetails": "Chi tiết nâng cao",
"hideDetails": "Che giấu chi tiết",
"hideDetails": "Ẩn chi tiết",
"slippageWarning": "Cảnh báo trượt giá",
"highSlippageWarning": "Cảnh báo trượt giá cao",
"youAreSelling": "Bạn đang bán",
@@ -59,7 +59,7 @@
"intoPool": "vào nhóm thanh khoản.",
"outPool": "từ nhóm thanh khoản.",
"youWillMint": "Bạn sẽ đúc tiền",
"liquidityTokens": "dồng thanh khoản.",
"liquidityTokens": "đồng thanh khoản.",
"totalSupplyIs": "Tổng cung hiện tại của đồng thanh khoản là",
"youAreSettingExRate": "Bạn đang đặt tỷ giá hối đoái ban đầu thành",
"totalSupplyIs0": "Tổng cung hiện tại của đồng thanh khoản là 0.",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -40,11 +40,12 @@ export default function Transaction({ hash }: { hash: string }) {
const { chainId } = useActiveWeb3React()
const allTransactions = useAllTransactions()
const summary = allTransactions?.[hash]?.summary
const pending = !allTransactions?.[hash]?.receipt
const success =
!pending &&
(allTransactions[hash].receipt.status === 1 || typeof allTransactions[hash].receipt.status === 'undefined')
const tx = allTransactions?.[hash]
const summary = tx?.summary
const pending = !tx?.receipt
const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined')
if (!chainId) return null
return (
<TransactionWrapper>

View File

@@ -200,7 +200,7 @@ const MainWalletAction = styled(WalletAction)`
color: ${({ theme }) => theme.primary1};
`
function renderTransactions(transactions) {
function renderTransactions(transactions: string[]) {
return (
<TransactionListWrapper>
{transactions.map((hash, i) => {
@@ -212,8 +212,8 @@ function renderTransactions(transactions) {
interface AccountDetailsProps {
toggleWalletModal: () => void
pendingTransactions: any[]
confirmedTransactions: any[]
pendingTransactions: string[]
confirmedTransactions: string[]
ENSName?: string
openOptions: () => void
}
@@ -251,26 +251,26 @@ export default function AccountDetails({
} else if (connector === walletconnect) {
return (
<IconWrapper size={16}>
<img src={WalletConnectIcon} alt={''} />
<img src={WalletConnectIcon} alt={'wallet connect logo'} />
</IconWrapper>
)
} else if (connector === walletlink) {
return (
<IconWrapper size={16}>
<img src={CoinbaseWalletIcon} alt={''} />
<img src={CoinbaseWalletIcon} alt={'coinbase wallet logo'} />
</IconWrapper>
)
} else if (connector === fortmatic) {
return (
<IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} />
<img src={FortmaticIcon} alt={'fortmatic logo'} />
</IconWrapper>
)
} else if (connector === portis) {
return (
<>
<IconWrapper size={16}>
<img src={PortisIcon} alt={''} />
<img src={PortisIcon} alt={'portis logo'} />
<MainWalletAction
onClick={() => {
portis.portis.showPortis()
@@ -282,15 +282,12 @@ export default function AccountDetails({
</>
)
}
return null
}
const clearAllTransactionsCallback = useCallback(
(event: React.MouseEvent) => {
event.preventDefault()
dispatch(clearAllTransactions({ chainId }))
},
[dispatch, chainId]
)
const clearAllTransactionsCallback = useCallback(() => {
if (chainId) dispatch(clearAllTransactions({ chainId }))
}, [dispatch, chainId])
return (
<>
@@ -338,7 +335,7 @@ export default function AccountDetails({
<>
<div>
{getStatusIcon()}
<p> {shortenAddress(account)}</p>
<p> {account && shortenAddress(account)}</p>
</div>
</>
)}
@@ -349,17 +346,21 @@ export default function AccountDetails({
<>
<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>
{account && (
<Copy toCopy={account}>
<span style={{ marginLeft: '4px' }}>Copy Address</span>
</Copy>
)}
{chainId && account && (
<AddressLink
hasENS={!!ENSName}
isENS={true}
href={chainId && getEtherscanLink(chainId, ENSName, 'address')}
>
<LinkIcon size={16} />
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
</AddressLink>
)}
</div>
</AccountControl>
</>
@@ -367,22 +368,25 @@ export default function AccountDetails({
<>
<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>
{account && (
<Copy toCopy={account}>
<span style={{ marginLeft: '4px' }}>Copy Address</span>
</Copy>
)}
{chainId && account && (
<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>

View File

@@ -65,11 +65,6 @@ const Input = styled.input<{ error?: boolean }>`
}
`
interface Value {
address: string
name?: string
}
export default function AddressInputPanel({
id,
value,
@@ -106,7 +101,7 @@ export default function AddressInputPanel({
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
Recipient
</TYPE.black>
{address && (
{address && chainId && (
<ExternalLink href={getEtherscanLink(chainId, name ?? address, 'address')} style={{ fontSize: '14px' }}>
(View on Etherscan)
</ExternalLink>

View File

@@ -10,7 +10,7 @@ const Base = styled(RebassButton)<{
padding?: string
width?: string
borderRadius?: string
altDisbaledStyle?: boolean
altDisabledStyle?: boolean
}>`
padding: ${({ padding }) => (padding ? padding : '18px')};
width: ${({ width }) => (width ? width : '100%')};
@@ -27,6 +27,8 @@ const Base = styled(RebassButton)<{
flex-wrap: nowrap;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
&:disabled {
cursor: auto;
}
@@ -51,12 +53,13 @@ export const ButtonPrimary = styled(Base)`
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
}
&:disabled {
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
background-color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? 'white' : theme.text3)};
cursor: auto;
box-shadow: none;
border: 1px solid transparent;
outline: none;
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')};
}
`
@@ -251,11 +254,15 @@ const ButtonErrorStyle = styled(Base)`
}
`
export function ButtonConfirmed({ confirmed, ...rest }: { confirmed?: boolean } & ButtonProps) {
export function ButtonConfirmed({
confirmed,
altDisabledStyle,
...rest
}: { confirmed?: boolean; altDisabledStyle?: boolean } & ButtonProps) {
if (confirmed) {
return <ButtonConfirmedStyle {...rest} />
} else {
return <ButtonPrimary {...rest} />
return <ButtonPrimary {...rest} altDisabledStyle={altDisabledStyle} />
}
}

View File

@@ -19,11 +19,11 @@ export const LightCard = styled(Card)`
`
export const GreyCard = styled(Card)`
background-color: ${({ theme }) => theme.advancedBG};
background-color: ${({ theme }) => theme.bg3};
`
export const OutlineCard = styled(Card)`
border: 1px solid ${({ theme }) => theme.advancedBG};
border: 1px solid ${({ theme }) => theme.bg3};
`
export const YellowCard = styled(Card)`

View File

@@ -1,133 +0,0 @@
import React, { useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import Modal from '../Modal'
import { ExternalLink } from '../../theme'
import { Text } from 'rebass'
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 { getEtherscanLink } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
const Wrapper = styled.div`
width: 100%;
`
const Section = styled(AutoColumn)`
padding: 24px;
`
const BottomSection = styled(Section)`
background-color: ${({ theme }) => theme.bg2};
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
`
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
hash: string
topContent: () => React.ReactChild
bottomContent: () => React.ReactChild
attemptingTxn: boolean
pendingText: string
title?: string
}
export default function ConfirmationModal({
isOpen,
onDismiss,
topContent,
bottomContent,
attemptingTxn,
hash,
pendingText,
title = ''
}: ConfirmationModalProps) {
const { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
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>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
<ConfirmedIcon>
{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}>
{transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'}
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
{pendingText}
</Text>
</AutoColumn>
{transactionBroadcast ? (
<>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
View on Etherscan
</Text>
</ExternalLink>
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
<Text fontWeight={500} fontSize={20}>
Close
</Text>
</ButtonPrimary>
</>
) : (
<Text fontSize={12} color="#565A69" textAlign="center">
Confirm this transaction in your wallet
</Text>
)}
</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>
)
}

View File

@@ -7,7 +7,7 @@ import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { RowBetween } from '../Row'
import { TYPE, CursorPointer } from '../../theme'
import { TYPE } from '../../theme'
import { Input as NumericalInput } from '../NumericalInput'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
@@ -126,7 +126,6 @@ interface CurrencyInputPanelProps {
hideBalance?: boolean
pair?: Pair | null
hideInput?: boolean
showSendWithSwap?: boolean
otherCurrency?: Currency | null
id: string
showCommonBases?: boolean
@@ -138,14 +137,13 @@ export default function CurrencyInputPanel({
onMax,
showMaxButton,
label = 'Input',
onCurrencySelect = null,
currency = null,
onCurrencySelect,
currency,
disableCurrencySelect = false,
hideBalance = false,
pair = null, // used for double token logo
hideInput = false,
showSendWithSwap = false,
otherCurrency = null,
otherCurrency,
id,
showCommonBases
}: CurrencyInputPanelProps) {
@@ -153,7 +151,7 @@ export default function CurrencyInputPanel({
const [modalOpen, setModalOpen] = useState(false)
const { account } = useActiveWeb3React()
const selectedCurrencyBalance = useCurrencyBalance(account, currency)
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
const theme = useContext(ThemeContext)
const handleDismissSearch = useCallback(() => {
@@ -170,19 +168,17 @@ export default function CurrencyInputPanel({
{label}
</TYPE.body>
{account && (
<CursorPointer>
<TYPE.body
onClick={onMax}
color={theme.text2}
fontWeight={500}
fontSize={14}
style={{ display: 'inline' }}
>
{!hideBalance && !!currency && selectedCurrencyBalance
? 'Balance: ' + selectedCurrencyBalance?.toSignificant(6)
: ' -'}
</TYPE.body>
</CursorPointer>
<TYPE.body
onClick={onMax}
color={theme.text2}
fontWeight={500}
fontSize={14}
style={{ display: 'inline', cursor: 'pointer' }}
>
{!hideBalance && !!currency && selectedCurrencyBalance
? 'Balance: ' + selectedCurrencyBalance?.toSignificant(6)
: ' -'}
</TYPE.body>
)}
</RowBetween>
</LabelRow>
@@ -235,13 +231,12 @@ export default function CurrencyInputPanel({
</CurrencySelect>
</InputRow>
</Container>
{!disableCurrencySelect && (
{!disableCurrencySelect && onCurrencySelect && (
<CurrencySearchModal
isOpen={modalOpen}
onDismiss={handleDismissSearch}
onCurrencySelect={onCurrencySelect}
showSendWithSwap={showSendWithSwap}
hiddenCurrency={currency}
selectedCurrency={currency}
otherSelectedCurrency={otherCurrency}
showCommonBases={showCommonBases}
/>

View File

@@ -1,30 +1,14 @@
import React, { useState } from 'react'
import { Currency, ETHER, Token } from '@uniswap/sdk'
import React, { useMemo } from 'react'
import styled from 'styled-components'
import { Currency, Token } from '@uniswap/sdk'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
import useHttpLocations from '../../hooks/useHttpLocations'
import { WrappedTokenInfo } from '../../state/lists/hooks'
import Logo from '../Logo'
const getTokenLogoURL = address =>
const getTokenLogoURL = (address: string) =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
const NO_LOGO_ADDRESSES: { [tokenAddress: string]: true } = {}
const Image = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
background-color: white;
border-radius: 1rem;
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
`
const Emoji = styled.span<{ size?: string }>`
display: flex;
align-items: center;
justify-content: center;
font-size: ${({ size }) => size};
width: ${({ size }) => size};
height: ${({ size }) => size};
margin-bottom: -4px;
`
const StyledEthereumLogo = styled.img<{ size: string }>`
width: ${({ size }) => size};
@@ -33,46 +17,38 @@ const StyledEthereumLogo = styled.img<{ size: string }>`
border-radius: 24px;
`
const StyledLogo = styled(Logo)<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
`
export default function CurrencyLogo({
currency,
size = '24px',
...rest
style
}: {
currency?: Currency
size?: string
style?: React.CSSProperties
}) {
const [, refresh] = useState<number>(0)
const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
if (currency instanceof Token) {
let path = ''
if (!NO_LOGO_ADDRESSES[currency.address]) {
path = getTokenLogoURL(currency.address)
} else {
return (
<Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking">
🤔
</span>
</Emoji>
)
const srcs: string[] = useMemo(() => {
if (currency === ETHER) return []
if (currency instanceof Token) {
if (currency instanceof WrappedTokenInfo) {
return [...uriLocations, getTokenLogoURL(currency.address)]
}
return [getTokenLogoURL(currency.address)]
}
return []
}, [currency, uriLocations])
return (
<Image
{...rest}
alt={`${currency.name} Logo`}
src={path}
size={size}
onError={() => {
if (currency instanceof Token) {
NO_LOGO_ADDRESSES[currency.address] = true
}
refresh(i => i + 1)
}}
/>
)
} else {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
if (currency === ETHER) {
return <StyledEthereumLogo src={EthereumLogo} size={size} style={style} />
}
return <StyledLogo size={size} srcs={srcs} alt={`${currency?.symbol ?? 'token'} logo`} style={style} />
}

View File

@@ -137,7 +137,7 @@ const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
export default function Header() {
const { account, chainId } = useActiveWeb3React()
const userEthBalance = useETHBalances([account])[account]
const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? '']
const [isDark] = useDarkModeManager()
return (
@@ -156,7 +156,7 @@ export default function Header() {
<HeaderControls>
<HeaderElement>
<TestnetWrapper>
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
{!isMobile && chainId && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
</TestnetWrapper>
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (

View File

@@ -5,7 +5,7 @@ import styled from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import Jazzicon from 'jazzicon'
const StyledIdenticon = styled.div`
const StyledIdenticonContainer = styled.div`
height: 1rem;
width: 1rem;
border-radius: 1.125rem;
@@ -24,5 +24,6 @@ export default function Identicon() {
}
}, [account])
return <StyledIdenticon ref={ref} />
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
return <StyledIdenticonContainer ref={ref as any} />
}

View File

@@ -0,0 +1,26 @@
import React from 'react'
import styled from 'styled-components'
import useHttpLocations from '../../hooks/useHttpLocations'
import Logo from '../Logo'
const StyledListLogo = styled(Logo)<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
`
export default function ListLogo({
logoURI,
style,
size = '24px',
alt
}: {
logoURI: string
size?: string
style?: React.CSSProperties
alt?: string
}) {
const srcs: string[] = useHttpLocations(logoURI)
return <StyledListLogo alt={alt} size={size} srcs={srcs} style={style} />
}

View File

@@ -24,7 +24,7 @@ const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
* 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 }) {
export default function Loader({ size = '16px', stroke, ...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

View File

@@ -0,0 +1,34 @@
import React, { useState } from 'react'
import { HelpCircle } from 'react-feather'
import { ImageProps } from 'rebass'
const BAD_SRCS: { [tokenAddress: string]: true } = {}
export interface LogoProps extends Pick<ImageProps, 'style' | 'alt' | 'className'> {
srcs: string[]
}
/**
* Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert
*/
export default function Logo({ srcs, alt, ...rest }: LogoProps) {
const [, refresh] = useState<number>(0)
const src: string | undefined = srcs.find(src => !BAD_SRCS[src])
if (src) {
return (
<img
{...rest}
alt={alt}
src={src}
onError={() => {
if (src) BAD_SRCS[src] = true
refresh(i => i + 1)
}}
/>
)
}
return <HelpCircle {...rest} />
}

View File

@@ -1,7 +1,8 @@
import React, { useRef, useEffect } from 'react'
import React, { useRef } from 'react'
import { Info, BookOpen, Code, PieChart, MessageCircle } from 'react-feather'
import styled from 'styled-components'
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import useToggle from '../../hooks/useToggle'
import { ExternalLink } from '../../theme'
@@ -83,27 +84,11 @@ export default function Menu() {
const node = useRef<HTMLDivElement>()
const [open, toggle] = useToggle(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])
useOnClickOutside(node, open ? toggle : undefined)
return (
<StyledMenu ref={node}>
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
<StyledMenu ref={node as any}>
<StyledMenuButton onClick={toggle}>
<StyledMenuIcon />
</StyledMenuButton>

View File

@@ -1,8 +1,6 @@
import React from 'react'
import styled, { css } from 'styled-components'
import { animated, useTransition, useSpring } from 'react-spring'
import { Spring } from 'react-spring/renderprops'
import { DialogOverlay, DialogContent } from '@reach/dialog'
import { isMobile } from 'react-device-detect'
import '@reach/dialog/styles.css'
@@ -11,39 +9,25 @@ import { useGesture } from 'react-use-gesture'
const AnimatedDialogOverlay = animated(DialogOverlay)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverlay {...rest} />)<{ mobile: boolean }>`
const StyledDialogOverlay = styled(AnimatedDialogOverlay)`
&[data-reach-dialog-overlay] {
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
overflow: hidden;
${({ mobile }) =>
mobile &&
css`
align-items: flex-end;
`}
display: flex;
align-items: center;
justify-content: center;
&::after {
content: '';
background-color: ${({ theme }) => theme.modalBG};
opacity: 0.5;
top: 0;
left: 0;
bottom: 0;
right: 0;
position: fixed;
z-index: -1;
}
background-color: ${({ theme }) => theme.modalBG};
}
`
const AnimatedDialogContent = animated(DialogContent)
// destructure to not pass custom props to Dialog DOM element
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
<DialogContent {...rest} />
<AnimatedDialogContent {...rest} />
)).attrs({
'aria-label': 'dialog'
})`
@@ -54,6 +38,9 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadow1)};
padding: 0px;
width: 50vw;
overflow: hidden;
align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')};
max-width: 420px;
${({ maxHeight }) =>
@@ -99,10 +86,10 @@ export default function Modal({
onDismiss,
minHeight = false,
maxHeight = 50,
initialFocusRef = null,
initialFocusRef,
children
}: ModalProps) {
const transitions = useTransition(isOpen, null, {
const fadeTransition = useTransition(isOpen, null, {
config: { duration: 200 },
from: { opacity: 0 },
enter: { opacity: 1 },
@@ -115,80 +102,37 @@ export default function Modal({
set({
y: state.down ? state.movement[1] : 0
})
if (state.velocity > 3 && state.direction[1] > 0) {
if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) {
onDismiss()
}
}
})
if (isMobile) {
return (
<>
{transitions.map(
({ item, key, props }) =>
item && (
<StyledDialogOverlay
key={key}
style={props}
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
mobile={true}
return (
<>
{fadeTransition.map(
({ item, key, props }) =>
item && (
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
<StyledDialogContent
{...(isMobile
? {
...bind(),
style: { transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`) }
}
: {})}
aria-label="dialog content"
minHeight={minHeight}
maxHeight={maxHeight}
mobile={isMobile}
>
{/* 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)'
}}
to={{
transform: isOpen ? 'translateY(0px)' : 'translateY(200px)'
}}
>
{props => (
<animated.div
{...bind()}
style={{
transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`)
}}
>
<StyledDialogContent
aria-label="dialog content"
style={props}
hidden={true}
minHeight={minHeight}
maxHeight={maxHeight}
mobile={isMobile}
>
{children}
</StyledDialogContent>
</animated.div>
)}
</Spring>
</StyledDialogOverlay>
)
)}
</>
)
} else {
return (
<>
{transitions.map(
({ item, key, props }) =>
item && (
<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>
)
)}
</>
)
}
{!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
{children}
</StyledDialogContent>
</StyledDialogOverlay>
)
)}
</>
)
}

View File

@@ -46,7 +46,7 @@ export const Input = React.memo(function InnerInput({
...rest
}: {
value: string | number
onUserInput: (string) => void
onUserInput: (input: string) => void
error?: boolean
fontSize?: string
align?: 'right' | 'left'

View File

@@ -1,6 +1,6 @@
import { Placement } from '@popperjs/core'
import { transparentize } from 'polished'
import React, { useState } from 'react'
import React, { useCallback, useState } from 'react'
import { usePopper } from 'react-popper'
import styled from 'styled-components'
import useInterval from '../../hooks/useInterval'
@@ -83,9 +83,9 @@ export interface PopoverProps {
}
export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) {
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>(null)
const [popperElement, setPopperElement] = useState<HTMLDivElement>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement>(null)
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null)
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
const { styles, update, attributes } = usePopper(referenceElement, popperElement, {
placement,
strategy: 'fixed',
@@ -94,17 +94,20 @@ export default function Popover({ content, show, children, placement = 'auto' }:
{ name: 'arrow', options: { element: arrowElement } }
]
})
useInterval(update, show ? 100 : null)
const updateCallback = useCallback(() => {
update && update()
}, [update])
useInterval(updateCallback, show ? 100 : null)
return (
<>
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
<ReferenceElement ref={setReferenceElement as any}>{children}</ReferenceElement>
<Portal>
<PopoverContainer show={show} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
<PopoverContainer show={show} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
{content}
<Arrow
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
ref={setArrowElement}
ref={setArrowElement as any}
style={styles.arrow}
{...attributes.arrow}
/>

View File

@@ -0,0 +1,106 @@
import { diffTokenLists, TokenList } from '@uniswap/token-lists'
import React, { useCallback, useMemo } from 'react'
import ReactGA from 'react-ga'
import { useDispatch } from 'react-redux'
import { Text } from 'rebass'
import { AppDispatch } from '../../state'
import { useRemovePopup } from '../../state/application/hooks'
import { acceptListUpdate } from '../../state/lists/actions'
import { TYPE } from '../../theme'
import listVersionLabel from '../../utils/listVersionLabel'
import { ButtonSecondary } from '../Button'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
export default function ListUpdatePopup({
popKey,
listUrl,
oldList,
newList,
auto
}: {
popKey: string
listUrl: string
oldList: TokenList
newList: TokenList
auto: boolean
}) {
const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
const dispatch = useDispatch<AppDispatch>()
const handleAcceptUpdate = useCallback(() => {
if (auto) return
ReactGA.event({
category: 'Lists',
action: 'Update List from Popup',
label: listUrl
})
dispatch(acceptListUpdate(listUrl))
removeThisPopup()
}, [auto, dispatch, listUrl, removeThisPopup])
const { added: tokensAdded, changed: tokensChanged, removed: tokensRemoved } = useMemo(() => {
return diffTokenLists(oldList.tokens, newList.tokens)
}, [newList.tokens, oldList.tokens])
const numTokensChanged = useMemo(
() =>
Object.keys(tokensChanged).reduce((memo, chainId: any) => memo + Object.keys(tokensChanged[chainId]).length, 0),
[tokensChanged]
)
return (
<AutoRow>
<AutoColumn style={{ flex: '1' }} gap="8px">
{auto ? (
<TYPE.body fontWeight={500}>
The token list &quot;{oldList.name}&quot; has been updated to{' '}
<strong>{listVersionLabel(newList.version)}</strong>.
</TYPE.body>
) : (
<>
<div>
<Text>
An update is available for the token list &quot;{oldList.name}&quot; (
{listVersionLabel(oldList.version)} to {listVersionLabel(newList.version)}).
</Text>
<ul>
{tokensAdded.length > 0 ? (
<li>
{tokensAdded.map((token, i) => (
<React.Fragment key={`${token.chainId}-${token.address}`}>
<strong title={token.address}>{token.symbol}</strong>
{i === tokensAdded.length - 1 ? null : ', '}
</React.Fragment>
))}{' '}
added
</li>
) : null}
{tokensRemoved.length > 0 ? (
<li>
{tokensRemoved.map((token, i) => (
<React.Fragment key={`${token.chainId}-${token.address}`}>
<strong title={token.address}>{token.symbol}</strong>
{i === tokensRemoved.length - 1 ? null : ', '}
</React.Fragment>
))}{' '}
removed
</li>
) : null}
{numTokensChanged > 0 ? <li>{numTokensChanged} tokens updated</li> : null}
</ul>
</div>
<AutoRow>
<div style={{ flexGrow: 1, marginRight: 12 }}>
<ButtonSecondary onClick={handleAcceptUpdate}>Accept update</ButtonSecondary>
</div>
<div style={{ flexGrow: 1 }}>
<ButtonSecondary onClick={removeThisPopup}>Dismiss</ButtonSecondary>
</div>
</AutoRow>
</>
)}
</AutoColumn>
</AutoRow>
)
}

View File

@@ -0,0 +1,97 @@
import React, { useCallback, useContext, useEffect } from 'react'
import { X } from 'react-feather'
import { useSpring } from 'react-spring/web'
import styled, { ThemeContext } from 'styled-components'
import { animated } from 'react-spring'
import { PopupContent } from '../../state/application/actions'
import { useRemovePopup } from '../../state/application/hooks'
import ListUpdatePopup from './ListUpdatePopup'
import TransactionPopup from './TransactionPopup'
export const StyledClose = styled(X)`
position: absolute;
right: 10px;
top: 10px;
:hover {
cursor: pointer;
}
`
export const Popup = styled.div`
display: inline-block;
width: 100%;
padding: 1em;
background-color: ${({ theme }) => theme.bg1};
position: relative;
border-radius: 10px;
padding: 20px;
padding-right: 35px;
overflow: hidden;
${({ theme }) => theme.mediaWidth.upToSmall`
min-width: 290px;
`}
`
const Fader = styled.div`
position: absolute;
bottom: 0px;
left: 0px;
width: 100%;
height: 2px;
background-color: ${({ theme }) => theme.bg3};
`
const AnimatedFader = animated(Fader)
export default function PopupItem({
removeAfterMs,
content,
popKey
}: {
removeAfterMs: number | null
content: PopupContent
popKey: string
}) {
const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
useEffect(() => {
if (removeAfterMs === null) return undefined
const timeout = setTimeout(() => {
removeThisPopup()
}, removeAfterMs)
return () => {
clearTimeout(timeout)
}
}, [removeAfterMs, removeThisPopup])
const theme = useContext(ThemeContext)
let popupContent
if ('txn' in content) {
const {
txn: { hash, success, summary }
} = content
popupContent = <TransactionPopup hash={hash} success={success} summary={summary} />
} else if ('listUpdate' in content) {
const {
listUpdate: { listUrl, oldList, newList, auto }
} = content
popupContent = <ListUpdatePopup popKey={popKey} listUrl={listUrl} oldList={oldList} newList={newList} auto={auto} />
}
const faderStyle = useSpring({
from: { width: '100%' },
to: { width: '0%' },
config: { duration: removeAfterMs ?? undefined }
})
return (
<Popup>
<StyledClose color={theme.text2} onClick={removeThisPopup} />
{popupContent}
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
</Popup>
)
}

View File

@@ -0,0 +1,41 @@
import React, { useContext } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather'
import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { TYPE } from '../../theme'
import { ExternalLink } from '../../theme/components'
import { getEtherscanLink } from '../../utils'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
const RowNoFlex = styled(AutoRow)`
flex-wrap: nowrap;
`
export default function TransactionPopup({
hash,
success,
summary
}: {
hash: string
success?: boolean
summary?: string
}) {
const { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
return (
<RowNoFlex>
<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 ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
{chainId && (
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
)}
</AutoColumn>
</RowNoFlex>
)
}

View File

@@ -1,22 +1,8 @@
import React, { useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { useMediaLayout } from 'use-media'
import { X } from 'react-feather'
import { PopupContent } from '../../state/application/actions'
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
import React from 'react'
import styled from 'styled-components'
import { useActivePopups } from '../../state/application/hooks'
import { AutoColumn } from '../Column'
import TxnPopup from '../TxnPopup'
const StyledClose = styled(X)`
position: absolute;
right: 10px;
top: 10px;
:hover {
cursor: pointer;
}
`
import PopupItem from './PopupItem'
const MobilePopupWrapper = styled.div<{ height: string | number }>`
position: relative;
@@ -24,6 +10,11 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
height: ${({ height }) => height};
margin: ${({ height }) => (height ? '0 auto;' : 0)};
margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
display: none;
${({ theme }) => theme.mediaWidth.upToSmall`
display: block;
`};
`
const MobilePopupInner = styled.div`
@@ -39,83 +30,39 @@ const MobilePopupInner = styled.div`
`
const FixedPopupColumn = styled(AutoColumn)`
position: absolute;
top: 112px;
position: fixed;
top: 64px;
right: 1rem;
max-width: 355px !important;
width: 100%;
z-index: 2;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
`
const Popup = styled.div`
display: inline-block;
width: 100%;
padding: 1em;
background-color: ${({ theme }) => theme.bg1};
position: relative;
border-radius: 10px;
padding: 20px;
padding-right: 35px;
z-index: 2;
overflow: hidden;
${({ theme }) => theme.mediaWidth.upToSmall`
min-width: 290px;
`}
`
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
if ('txn' in content) {
const {
txn: { hash, success, summary }
} = content
return <TxnPopup popKey={popKey} hash={hash} success={success} summary={summary} />
}
}
export default function Popups() {
const theme = useContext(ThemeContext)
// get all popups
const activePopups = useActivePopups()
const removePopup = useRemovePopup()
// switch view settings on mobile
const isMobile = useMediaLayout({ maxWidth: '600px' })
if (!isMobile) {
return (
return (
<>
<FixedPopupColumn gap="20px">
{activePopups.map(item => {
return (
<Popup key={item.key}>
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
<PopupItem content={item.content} popKey={item.key} />
</Popup>
)
})}
{activePopups.map(item => (
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))}
</FixedPopupColumn>
)
}
//mobile
else
return (
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
<MobilePopupInner>
{activePopups // reverse so new items up front
.slice(0)
.reverse()
.map(item => {
return (
<Popup key={item.key}>
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
<PopupItem content={item.content} popKey={item.key} />
</Popup>
)
})}
.map(item => (
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))}
</MobilePopupInner>
</MobilePopupWrapper>
)
</>
)
}

View File

@@ -28,7 +28,7 @@ function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
<RowFixed>
<DoubleCurrencyLogo currency0={token} margin={true} size={20} />
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
{`${chainId && token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
</Text>
<Text
fontSize={12}

View File

@@ -46,7 +46,7 @@ export function MinimalPositionCard({ pair, showUnwrapped = false, border }: Pos
const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
const [token0Deposited, token1Deposited] =
@@ -131,7 +131,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
const poolTokenPercentage =

View File

@@ -0,0 +1,79 @@
import React from 'react'
import styled from 'styled-components'
import { RowBetween } from '../Row'
import { AutoColumn } from '../Column'
import { transparentize } from 'polished'
const Wrapper = styled(AutoColumn)`
margin-top: 1.25rem;
`
const Grouping = styled(RowBetween)`
width: 50%;
`
const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
min-width: 20px;
min-height: 20px;
background-color: ${({ theme, confirmed, disabled }) =>
disabled ? theme.bg4 : confirmed ? theme.green1 : theme.primary1};
border-radius: 50%;
color: ${({ theme }) => theme.white};
display: flex;
align-items: center;
justify-content: center;
line-height: 8px;
font-size: 12px;
`
const CircleRow = styled.div`
width: calc(100% - 20px);
display: flex;
align-items: center;
`
const Connector = styled.div<{ prevConfirmed?: boolean }>`
width: 100%;
height: 2px;
background-color: ;
background: linear-gradient(
90deg,
${({ theme, prevConfirmed }) => transparentize(0.5, prevConfirmed ? theme.green1 : theme.primary1)} 0%,
${({ theme, prevConfirmed }) => (prevConfirmed ? theme.primary1 : theme.bg4)} 80%
);
opacity: 0.6;
`
interface ProgressCirclesProps {
steps: boolean[]
}
/**
* Based on array of steps, create a step counter of circles.
* A circle can be enabled, disabled, or confirmed. States are derived
* from previous step.
*
* An extra circle is added to represent the ability to swap, add, or remove.
* This step will never be marked as complete (because no 'txn done' state in body ui).
*
* @param steps array of booleans where true means step is complete
*/
export default function ProgressCircles({ steps }: ProgressCirclesProps) {
return (
<Wrapper justify={'center'}>
<Grouping>
{steps.map((step, i) => {
return (
<CircleRow key={i}>
<Circle confirmed={step} disabled={!steps[i - 1] && i !== 0}>
{step ? '✓' : i + 1}
</Circle>
<Connector prevConfirmed={step} />
</CircleRow>
)
})}
<Circle disabled={!steps[steps.length - 1]}>{steps.length + 1}</Circle>
</Grouping>
</Wrapper>
)
}

View File

@@ -22,7 +22,7 @@ const QuestionWrapper = styled.div`
}
`
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
export default function QuestionHelper({ text }: { text: string }) {
const [show, setShow] = useState<boolean>(false)
const open = useCallback(() => setShow(true), [setShow])
@@ -30,7 +30,7 @@ export default function QuestionHelper({ text, disabled }: { text: string; disab
return (
<span style={{ marginLeft: 4 }}>
<Tooltip text={text} show={show && !disabled}>
<Tooltip text={text} show={show}>
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
<Question size={16} />
</QuestionWrapper>

View File

@@ -30,8 +30,8 @@ export default function CommonBases({
onSelect,
selectedCurrency
}: {
chainId: ChainId
selectedCurrency?: Currency
chainId?: ChainId
selectedCurrency?: Currency | null
onSelect: (currency: Currency) => void
}) {
return (
@@ -44,7 +44,11 @@ export default function CommonBases({
</AutoRow>
<AutoRow gap="4px">
<BaseWrapper
onClick={() => !currencyEquals(selectedCurrency, ETHER) && onSelect(ETHER)}
onClick={() => {
if (!selectedCurrency || !currencyEquals(selectedCurrency, ETHER)) {
onSelect(ETHER)
}
}}
disable={selectedCurrency === ETHER}
>
<CurrencyLogo currency={ETHER} style={{ marginRight: 8 }} />
@@ -52,8 +56,8 @@ export default function CommonBases({
ETH
</Text>
</BaseWrapper>
{(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => {
const selected = currencyEquals(selectedCurrency, token)
{(chainId ? SUGGESTED_BASES[chainId] : []).map((token: Token) => {
const selected = selectedCurrency instanceof Token && selectedCurrency.address === token.address
return (
<BaseWrapper onClick={() => !selected && onSelect(token)} disable={selected} key={token.address}>
<CurrencyLogo currency={token} style={{ marginRight: 8 }} />

View File

@@ -1,152 +1,210 @@
import { Currency, CurrencyAmount, currencyEquals, ETHER, JSBI, Token } from '@uniswap/sdk'
import React, { CSSProperties, memo, useContext, useMemo } from 'react'
import { Currency, CurrencyAmount, currencyEquals, ETHER, Token } from '@uniswap/sdk'
import React, { CSSProperties, MutableRefObject, useCallback, useMemo } from 'react'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import styled from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useETHBalances } from '../../state/wallet/hooks'
import { useCurrencyBalance } from '../../state/wallet/hooks'
import { LinkStyledButton, TYPE } from '../../theme'
import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import { useIsUserAddedToken } from '../../hooks/Tokens'
import Column from '../Column'
import { RowFixed } from '../Row'
import CurrencyLogo from '../CurrencyLogo'
import { MouseoverTooltip } from '../Tooltip'
import { FadedSpan, MenuItem } from './styleds'
import Loader from '../Loader'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
import { isTokenOnList } from '../../utils'
function currencyKey(currency: Currency): string {
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
}
const StyledBalanceText = styled(Text)`
white-space: nowrap;
overflow: hidden;
max-width: 5rem;
text-overflow: ellipsis;
`
const Tag = styled.div`
background-color: ${({ theme }) => theme.bg3};
color: ${({ theme }) => theme.text2};
font-size: 14px;
border-radius: 4px;
padding: 0.25rem 0.3rem 0.25rem 0.3rem;
max-width: 6rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
justify-self: flex-end;
margin-right: 4px;
`
function Balance({ balance }: { balance: CurrencyAmount }) {
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
}
const TagContainer = styled.div`
display: flex;
justify-content: flex-end;
`
function TokenTags({ currency }: { currency: Currency }) {
if (!(currency instanceof WrappedTokenInfo)) {
return <span />
}
const tags = currency.tags
if (!tags || tags.length === 0) return <span />
const tag = tags[0]
return (
<TagContainer>
<MouseoverTooltip text={tag.description}>
<Tag key={tag.id}>{tag.name}</Tag>
</MouseoverTooltip>
{tags.length > 1 ? (
<MouseoverTooltip
text={tags
.slice(1)
.map(({ name, description }) => `${name}: ${description}`)
.join('; \n')}
>
<Tag>...</Tag>
</MouseoverTooltip>
) : null}
</TagContainer>
)
}
function CurrencyRow({
currency,
onSelect,
isSelected,
otherSelected,
style
}: {
currency: Currency
onSelect: () => void
isSelected: boolean
otherSelected: boolean
style: CSSProperties
}) {
const { account, chainId } = useActiveWeb3React()
const key = currencyKey(currency)
const selectedTokenList = useSelectedTokenList()
const isOnSelectedList = isTokenOnList(selectedTokenList, currency)
const customAdded = useIsUserAddedToken(currency)
const balance = useCurrencyBalance(account ?? undefined, currency)
const removeToken = useRemoveUserAddedToken()
const addToken = useAddUserToken()
// only show add or remove buttons if not on selected list
return (
<MenuItem
style={style}
className={`token-item-${key}`}
onClick={() => (isSelected ? null : onSelect())}
disabled={isSelected}
selected={otherSelected}
>
<CurrencyLogo currency={currency} size={'24px'} />
<Column>
<Text title={currency.name} fontWeight={500}>
{currency.symbol}
</Text>
<FadedSpan>
{!isOnSelectedList && customAdded ? (
<TYPE.main fontWeight={500}>
Added by user
<LinkStyledButton
onClick={event => {
event.stopPropagation()
if (chainId && currency instanceof Token) removeToken(chainId, currency.address)
}}
>
(Remove)
</LinkStyledButton>
</TYPE.main>
) : null}
{!isOnSelectedList && !customAdded ? (
<TYPE.main fontWeight={500}>
Found by address
<LinkStyledButton
onClick={event => {
event.stopPropagation()
if (currency instanceof Token) addToken(currency)
}}
>
(Add)
</LinkStyledButton>
</TYPE.main>
) : null}
</FadedSpan>
</Column>
<TokenTags currency={currency} />
<RowFixed style={{ justifySelf: 'flex-end' }}>
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
</RowFixed>
</MenuItem>
)
}
export default function CurrencyList({
height,
currencies,
allBalances,
selectedCurrency,
onCurrencySelect,
otherCurrency,
showSendWithSwap
fixedListRef,
showETH
}: {
height: number
currencies: Currency[]
selectedCurrency: Currency
allBalances: { [tokenAddress: string]: CurrencyAmount }
selectedCurrency?: Currency | null
onCurrencySelect: (currency: Currency) => void
otherCurrency: Currency
showSendWithSwap?: boolean
otherCurrency?: Currency | null
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
showETH: boolean
}) {
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const allTokens = useAllTokens()
const addToken = useAddUserToken()
const removeToken = useRemoveUserAddedToken()
const ETHBalance = useETHBalances([account])[account]
const itemData = useMemo(() => (showETH ? [Currency.ETHER, ...currencies] : currencies), [currencies, showETH])
const CurrencyRow = useMemo(() => {
return memo(function CurrencyRow({ index, style }: { index: number; style: CSSProperties }) {
const currency = index === 0 ? Currency.ETHER : currencies[index - 1]
const key = currencyKey(currency)
const isDefault = isDefaultToken(currency)
const customAdded = isCustomAddedToken(allTokens, currency)
const balance = currency === ETHER ? ETHBalance : allBalances[key]
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
const isSelected = Boolean(selectedCurrency && currencyEquals(currency, selectedCurrency))
const Row = useCallback(
({ data, index, style }) => {
const currency: Currency = data[index]
const isSelected = Boolean(selectedCurrency && currencyEquals(selectedCurrency, currency))
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
const handleSelect = () => onCurrencySelect(currency)
return (
<MenuItem
<CurrencyRow
style={style}
className={`token-item-${key}`}
onClick={() => (isSelected ? null : onCurrencySelect(currency))}
disabled={isSelected}
selected={otherSelected}
>
<RowFixed>
<CurrencyLogo currency={currency} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>{currency.symbol}</Text>
<FadedSpan>
{customAdded ? (
<TYPE.main fontWeight={500}>
Added by user
<LinkStyledButton
onClick={event => {
event.stopPropagation()
if (currency instanceof Token) removeToken(chainId, currency.address)
}}
>
(Remove)
</LinkStyledButton>
</TYPE.main>
) : null}
{!isDefault && !customAdded ? (
<TYPE.main fontWeight={500}>
Found by address
<LinkStyledButton
onClick={event => {
event.stopPropagation()
if (currency instanceof Token) addToken(currency)
}}
>
(Add)
</LinkStyledButton>
</TYPE.main>
) : null}
</FadedSpan>
</Column>
</RowFixed>
<AutoColumn>
{balance ? (
<Text>
{zeroBalance && showSendWithSwap ? (
<ButtonSecondary padding={'4px 8px'}>
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
Send With Swap
</Text>
</ButtonSecondary>
) : balance ? (
balance.toSignificant(6)
) : (
'-'
)}
</Text>
) : account ? (
<Loader />
) : (
'-'
)}
</AutoColumn>
</MenuItem>
currency={currency}
isSelected={isSelected}
onSelect={handleSelect}
otherSelected={otherSelected}
/>
)
})
}, [
ETHBalance,
account,
addToken,
allBalances,
allTokens,
chainId,
currencies,
onCurrencySelect,
otherCurrency,
removeToken,
selectedCurrency,
showSendWithSwap,
theme.primary1
])
},
[onCurrencySelect, otherCurrency, selectedCurrency]
)
const itemKey = useCallback((index: number, data: any) => currencyKey(data[index]), [])
return (
<FixedSizeList
height={height}
ref={fixedListRef as any}
width="100%"
height={500}
itemCount={currencies.length + 1}
itemData={itemData}
itemCount={itemData.length}
itemSize={56}
style={{ flex: '1' }}
itemKey={index => currencyKey(currencies[index])}
itemKey={itemKey}
>
{CurrencyRow}
{Row}
</FixedSizeList>
)
}

View File

@@ -0,0 +1,214 @@
import { Currency, ETHER, Token } from '@uniswap/sdk'
import React, { KeyboardEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import ReactGA from 'react-ga'
import { useTranslation } from 'react-i18next'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useToken } from '../../hooks/Tokens'
import { useSelectedListInfo } from '../../state/lists/hooks'
import { CloseIcon, LinkStyledButton, TYPE } from '../../theme'
import { isAddress } from '../../utils'
import Card from '../Card'
import Column from '../Column'
import ListLogo from '../ListLogo'
import QuestionHelper from '../QuestionHelper'
import Row, { RowBetween } from '../Row'
import CommonBases from './CommonBases'
import CurrencyList from './CurrencyList'
import { filterTokens } from './filtering'
import SortButton from './SortButton'
import { useTokenComparator } from './sorting'
import { PaddedColumn, SearchInput, Separator } from './styleds'
import AutoSizer from 'react-virtualized-auto-sizer'
interface CurrencySearchProps {
isOpen: boolean
onDismiss: () => void
selectedCurrency?: Currency | null
onCurrencySelect: (currency: Currency) => void
otherSelectedCurrency?: Currency | null
showCommonBases?: boolean
onChangeList: () => void
}
export function CurrencySearch({
selectedCurrency,
onCurrencySelect,
otherSelectedCurrency,
showCommonBases,
onDismiss,
isOpen,
onChangeList
}: CurrencySearchProps) {
const { t } = useTranslation()
const { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const fixedList = useRef<FixedSizeList>()
const [searchQuery, setSearchQuery] = useState<string>('')
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
const allTokens = useAllTokens()
// if they input an address, use it
const isAddressSearch = isAddress(searchQuery)
const searchToken = useToken(searchQuery)
useEffect(() => {
if (isAddressSearch) {
ReactGA.event({
category: 'Currency Select',
action: 'Search by address',
label: isAddressSearch
})
}
}, [isAddressSearch])
const showETH: boolean = useMemo(() => {
const s = searchQuery.toLowerCase().trim()
return s === '' || s === 'e' || s === 'et' || s === 'eth'
}, [searchQuery])
const tokenComparator = useTokenComparator(invertSearchOrder)
const filteredTokens: Token[] = useMemo(() => {
if (isAddressSearch) return searchToken ? [searchToken] : []
return filterTokens(Object.values(allTokens), searchQuery)
}, [isAddressSearch, 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 handleCurrencySelect = useCallback(
(currency: Currency) => {
onCurrencySelect(currency)
onDismiss()
},
[onDismiss, onCurrencySelect]
)
// clear the input on open
useEffect(() => {
if (isOpen) setSearchQuery('')
}, [isOpen])
// 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)
fixedList.current?.scrollTo(0)
}, [])
const handleEnter = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
const s = searchQuery.toLowerCase().trim()
if (s === 'eth') {
handleCurrencySelect(ETHER)
} else if (filteredSortedTokens.length > 0) {
if (
filteredSortedTokens[0].symbol?.toLowerCase() === searchQuery.trim().toLowerCase() ||
filteredSortedTokens.length === 1
) {
handleCurrencySelect(filteredSortedTokens[0])
}
}
}
},
[filteredSortedTokens, handleCurrencySelect, searchQuery]
)
const selectedListInfo = useSelectedListInfo()
return (
<Column style={{ width: '100%', flex: '1 1' }}>
<PaddedColumn gap="14px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
Select a token
<QuestionHelper text="Find a token by searching for its name or symbol or by pasting its address below." />
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<SearchInput
type="text"
id="token-search-input"
placeholder={t('tokenSearchPlaceholder')}
value={searchQuery}
ref={inputRef as RefObject<HTMLInputElement>}
onChange={handleInput}
onKeyDown={handleEnter}
/>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={selectedCurrency} />
)}
<RowBetween>
<Text fontSize={14} fontWeight={500}>
Token Name
</Text>
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
</RowBetween>
</PaddedColumn>
<Separator />
<div style={{ flex: '1' }}>
<AutoSizer disableWidth>
{({ height }) => (
<CurrencyList
height={height}
showETH={showETH}
currencies={filteredSortedTokens}
onCurrencySelect={handleCurrencySelect}
otherCurrency={otherSelectedCurrency}
selectedCurrency={selectedCurrency}
fixedListRef={fixedList}
/>
)}
</AutoSizer>
</div>
<Separator />
<Card>
<RowBetween>
{selectedListInfo.current ? (
<Row>
{selectedListInfo.current.logoURI ? (
<ListLogo
style={{ marginRight: 12 }}
logoURI={selectedListInfo.current.logoURI}
alt={`${selectedListInfo.current.name} list logo`}
/>
) : null}
<TYPE.main id="currency-search-selected-list-name">{selectedListInfo.current.name}</TYPE.main>
</Row>
) : null}
<LinkStyledButton
style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }}
onClick={onChangeList}
id="currency-search-change-list-button"
>
{selectedListInfo.current ? 'Change' : 'Select a list'}
</LinkStyledButton>
</RowBetween>
</Card>
</Column>
)
}

View File

@@ -1,35 +1,19 @@
import { Currency, Token } from '@uniswap/sdk'
import React, { KeyboardEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { isMobile } from 'react-device-detect'
import { useTranslation } from 'react-i18next'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Card from '../../components/Card'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useToken } from '../../hooks/Tokens'
import useInterval from '../../hooks/useInterval'
import { useAllTokenBalances, useTokenBalance } from '../../state/wallet/hooks'
import { CloseIcon, LinkStyledButton } from '../../theme'
import { isAddress } from '../../utils'
import Column from '../Column'
import { Currency } from '@uniswap/sdk'
import React, { useCallback, useEffect, useState } from 'react'
import ReactGA from 'react-ga'
import useLast from '../../hooks/useLast'
import { useSelectedListUrl } from '../../state/lists/hooks'
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 CurrencyList from './CurrencyList'
import SortButton from './SortButton'
import { CurrencySearch } from './CurrencySearch'
import ListIntroduction from './ListIntroduction'
import { ListSelect } from './ListSelect'
interface CurrencySearchModalProps {
isOpen?: boolean
onDismiss?: () => void
hiddenCurrency?: Currency
showSendWithSwap?: boolean
onCurrencySelect?: (currency: Currency) => void
otherSelectedCurrency?: Currency
isOpen: boolean
onDismiss: () => void
selectedCurrency?: Currency | null
onCurrencySelect: (currency: Currency) => void
otherSelectedCurrency?: Currency | null
showCommonBases?: boolean
}
@@ -37,53 +21,18 @@ export default function CurrencySearchModal({
isOpen,
onDismiss,
onCurrencySelect,
hiddenCurrency,
showSendWithSwap,
selectedCurrency,
otherSelectedCurrency,
showCommonBases = false
}: CurrencySearchModalProps) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const [listView, setListView] = useState<boolean>(false)
const lastOpen = useLast(isOpen)
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 = useTokenBalance(account, searchToken)
const allTokenBalances_ = useAllTokenBalances()
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])
useEffect(() => {
if (isOpen && !lastOpen) {
setListView(false)
}
}, [isOpen, lastOpen])
const handleCurrencySelect = useCallback(
(currency: Currency) => {
@@ -93,114 +42,44 @@ export default function CurrencySearchModal({
[onDismiss, onCurrencySelect]
)
// 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 handleClickChangeList = useCallback(() => {
ReactGA.event({
category: 'Lists',
action: 'Change Lists'
})
setListView(true)
}, [])
const handleClickBack = useCallback(() => {
ReactGA.event({
category: 'Lists',
action: 'Back'
})
setListView(false)
}, [])
const handleSelectListIntroduction = useCallback(() => {
setListView(true)
}, [])
const openTooltip = useCallback(() => {
setTooltipOpen(true)
}, [setTooltipOpen])
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
useInterval(
() => {
setTooltipOpen(false)
},
tooltipOpen ? 4000 : null,
false
)
const handleEnter = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && filteredSortedTokens.length > 0) {
if (
filteredSortedTokens[0].symbol.toLowerCase() === searchQuery.trim().toLowerCase() ||
filteredSortedTokens.length === 1
) {
handleCurrencySelect(filteredSortedTokens[0])
}
}
},
[filteredSortedTokens, handleCurrencySelect, searchQuery]
)
const selectedListUrl = useSelectedListUrl()
const noListSelected = !selectedListUrl
return (
<Modal
isOpen={isOpen}
onDismiss={onDismiss}
maxHeight={70}
initialFocusRef={isMobile ? undefined : inputRef}
minHeight={70}
>
<Column style={{ width: '100%' }}>
<PaddedColumn gap="14px">
<RowBetween>
<Text fontWeight={500} fontSize={16}>
Select a token
<QuestionHelper
disabled={tooltipOpen}
text="Find a token by searching for its name or symbol or by pasting its address below."
/>
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<Tooltip
text="Import any token into your list by pasting the token address into the search field."
show={tooltipOpen}
placement="bottom"
>
<SearchInput
type="text"
id="token-search-input"
placeholder={t('tokenSearchPlaceholder')}
value={searchQuery}
ref={inputRef}
onChange={handleInput}
onFocus={closeTooltip}
onBlur={closeTooltip}
onKeyDown={handleEnter}
/>
</Tooltip>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={hiddenCurrency} />
)}
<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 }} />
<CurrencyList
currencies={filteredSortedTokens}
allBalances={allTokenBalances}
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90} minHeight={listView ? 40 : noListSelected ? 0 : 80}>
{listView ? (
<ListSelect onDismiss={onDismiss} onBack={handleClickBack} />
) : noListSelected ? (
<ListIntroduction onSelectList={handleSelectListIntroduction} />
) : (
<CurrencySearch
isOpen={isOpen}
onDismiss={onDismiss}
onCurrencySelect={handleCurrencySelect}
otherCurrency={otherSelectedCurrency}
selectedCurrency={hiddenCurrency}
showSendWithSwap={showSendWithSwap}
onChangeList={handleClickChangeList}
selectedCurrency={selectedCurrency}
otherSelectedCurrency={otherSelectedCurrency}
showCommonBases={showCommonBases}
/>
<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>
)
}

View File

@@ -0,0 +1,47 @@
import React from 'react'
import { Text } from 'rebass'
import { ExternalLink } from '../../theme'
import { ButtonPrimary } from '../Button'
import { OutlineCard } from '../Card'
import Column, { AutoColumn } from '../Column'
import { PaddedColumn } from './styleds'
import { useDarkModeManager } from '../../state/user/hooks'
import listLight from '../../assets/images/token-list/lists-light.png'
import listDark from '../../assets/images/token-list/lists-dark.png'
export default function ListIntroduction({ onSelectList }: { onSelectList: () => void }) {
const [isDark] = useDarkModeManager()
return (
<Column style={{ width: '100%', flex: '1 1' }}>
<PaddedColumn>
<AutoColumn gap="14px">
<img
style={{ width: '120px', margin: '0 auto' }}
src={isDark ? listDark : listLight}
alt="token-list-preview"
/>
<img
style={{ width: '100%', borderRadius: '12px' }}
src="https://cloudflare-ipfs.com/ipfs/QmRf1rAJcZjV3pwKTHfPdJh4RxR8yvRHkdLjZCsmp7T6hA"
alt="token-list-preview"
/>
<Text style={{ marginBottom: '8px', textAlign: 'center' }}>
Uniswap now supports token lists. You can add your own custom lists via IPFS, HTTPS and ENS.{' '}
</Text>
<ButtonPrimary onClick={onSelectList} id="list-introduction-choose-a-list">
Choose a list
</ButtonPrimary>
<OutlineCard style={{ marginBottom: '8px', padding: '1rem' }}>
<Text fontWeight={400} fontSize={14} style={{ textAlign: 'center' }}>
Token lists are an{' '}
<ExternalLink href="https://github.com/uniswap/token-lists">open specification</ExternalLink>. Check out{' '}
<ExternalLink href="https://tokenlists.org">tokenlists.org</ExternalLink> to learn more.
</Text>
</OutlineCard>
</AutoColumn>
</PaddedColumn>
</Column>
)
}

View File

@@ -0,0 +1,379 @@
import React, { memo, useCallback, useMemo, useRef, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import ReactGA from 'react-ga'
import { usePopper } from 'react-popper'
import { useDispatch, useSelector } from 'react-redux'
import { Text } from 'rebass'
import styled from 'styled-components'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import useToggle from '../../hooks/useToggle'
import { AppDispatch, AppState } from '../../state'
import { acceptListUpdate, removeList, selectList } from '../../state/lists/actions'
import { useSelectedListUrl } from '../../state/lists/hooks'
import { CloseIcon, ExternalLink, LinkStyledButton, TYPE } from '../../theme'
import listVersionLabel from '../../utils/listVersionLabel'
import { parseENSAddress } from '../../utils/parseENSAddress'
import uriToHttp from '../../utils/uriToHttp'
import { ButtonOutlined, ButtonPrimary, ButtonSecondary } from '../Button'
import Column from '../Column'
import ListLogo from '../ListLogo'
import QuestionHelper from '../QuestionHelper'
import Row, { RowBetween } from '../Row'
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
const UnpaddedLinkStyledButton = styled(LinkStyledButton)`
padding: 0;
font-size: 1rem;
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
`
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 100;
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};
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);
color: ${({ theme }) => theme.text2};
border-radius: 0.5rem;
padding: 1rem;
display: grid;
grid-template-rows: 1fr;
grid-gap: 8px;
font-size: 1rem;
text-align: left;
`
const StyledMenu = styled.div`
display: flex;
justify-content: center;
align-items: center;
position: relative;
border: none;
`
const StyledListUrlText = styled.div`
max-width: 160px;
opacity: 0.6;
margin-right: 0.5rem;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
`
function ListOrigin({ listUrl }: { listUrl: string }) {
const ensName = useMemo(() => parseENSAddress(listUrl)?.ensName, [listUrl])
const host = useMemo(() => {
if (ensName) return undefined
const lowerListUrl = listUrl.toLowerCase()
if (lowerListUrl.startsWith('ipfs://') || lowerListUrl.startsWith('ipns://')) {
return listUrl
}
try {
const url = new URL(listUrl)
return url.host
} catch (error) {
return undefined
}
}, [listUrl, ensName])
return <>{ensName ?? host}</>
}
function listUrlRowHTMLId(listUrl: string) {
return `list-row-${listUrl.replace(/\./g, '-')}`
}
const ListRow = memo(function ListRow({ listUrl, onBack }: { listUrl: string; onBack: () => void }) {
const listsByUrl = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
const selectedListUrl = useSelectedListUrl()
const dispatch = useDispatch<AppDispatch>()
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
const isSelected = listUrl === selectedListUrl
const [open, toggle] = useToggle(false)
const node = useRef<HTMLDivElement>()
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>()
const [popperElement, setPopperElement] = useState<HTMLDivElement>()
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: 'auto',
strategy: 'fixed',
modifiers: [{ name: 'offset', options: { offset: [8, 8] } }]
})
useOnClickOutside(node, open ? toggle : undefined)
const selectThisList = useCallback(() => {
if (isSelected) return
ReactGA.event({
category: 'Lists',
action: 'Select List',
label: listUrl
})
dispatch(selectList(listUrl))
onBack()
}, [dispatch, isSelected, listUrl, onBack])
const handleAcceptListUpdate = useCallback(() => {
if (!pending) return
ReactGA.event({
category: 'Lists',
action: 'Update List from List Select',
label: listUrl
})
dispatch(acceptListUpdate(listUrl))
}, [dispatch, listUrl, pending])
const handleRemoveList = useCallback(() => {
ReactGA.event({
category: 'Lists',
action: 'Start Remove List',
label: listUrl
})
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
ReactGA.event({
category: 'Lists',
action: 'Confirm Remove List',
label: listUrl
})
dispatch(removeList(listUrl))
}
}, [dispatch, listUrl])
if (!list) return null
return (
<Row key={listUrl} align="center" padding="16px" id={listUrlRowHTMLId(listUrl)}>
{list.logoURI ? (
<ListLogo style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
) : (
<div style={{ width: '24px', height: '24px', marginRight: '1rem' }} />
)}
<Column style={{ flex: '1' }}>
<Row>
<Text
fontWeight={isSelected ? 500 : 400}
fontSize={16}
style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{list.name}
</Text>
</Row>
<Row
style={{
marginTop: '4px'
}}
>
<StyledListUrlText title={listUrl}>
<ListOrigin listUrl={listUrl} />
</StyledListUrlText>
</Row>
</Column>
<StyledMenu ref={node as any}>
<ButtonOutlined
style={{
width: '2rem',
padding: '.8rem .35rem',
borderRadius: '12px',
fontSize: '14px',
marginRight: '0.5rem'
}}
onClick={toggle}
ref={setReferenceElement}
>
<DropDown />
</ButtonOutlined>
{open && (
<PopoverContainer show={true} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
<div>{list && listVersionLabel(list.version)}</div>
<SeparatorDark />
<ExternalLink href={`https://tokenlists.org/token-list?url=${listUrl}`}>View list</ExternalLink>
<UnpaddedLinkStyledButton onClick={handleRemoveList} disabled={Object.keys(listsByUrl).length === 1}>
Remove list
</UnpaddedLinkStyledButton>
{pending && (
<UnpaddedLinkStyledButton onClick={handleAcceptListUpdate}>Update list</UnpaddedLinkStyledButton>
)}
</PopoverContainer>
)}
</StyledMenu>
{isSelected ? (
<ButtonPrimary
disabled={true}
className="select-button"
style={{ width: '5rem', minWidth: '5rem', padding: '0.5rem .35rem', borderRadius: '12px', fontSize: '14px' }}
>
Selected
</ButtonPrimary>
) : (
<>
<ButtonPrimary
className="select-button"
style={{
width: '5rem',
minWidth: '4.5rem',
padding: '0.5rem .35rem',
borderRadius: '12px',
fontSize: '14px'
}}
onClick={selectThisList}
>
Select
</ButtonPrimary>
</>
)}
</Row>
)
})
const AddListButton = styled(ButtonSecondary)`
/* height: 1.8rem; */
max-width: 4rem;
margin-left: 1rem;
border-radius: 12px;
padding: 10px 18px;
`
const ListContainer = styled.div`
flex: 1;
overflow: auto;
`
export function ListSelect({ onDismiss, onBack }: { onDismiss: () => void; onBack: () => void }) {
const [listUrlInput, setListUrlInput] = useState<string>('')
const dispatch = useDispatch<AppDispatch>()
const lists = useSelector<AppState, AppState['lists']['byUrl']>(state => state.lists.byUrl)
const adding = Boolean(lists[listUrlInput]?.loadingRequestId)
const [addError, setAddError] = useState<string | null>(null)
const handleInput = useCallback(e => {
setListUrlInput(e.target.value)
setAddError(null)
}, [])
const fetchList = useFetchListCallback()
const handleAddList = useCallback(() => {
if (adding) return
setAddError(null)
fetchList(listUrlInput)
.then(() => {
setListUrlInput('')
ReactGA.event({
category: 'Lists',
action: 'Add List',
label: listUrlInput
})
})
.catch(error => {
ReactGA.event({
category: 'Lists',
action: 'Add List Failed',
label: listUrlInput
})
setAddError(error.message)
dispatch(removeList(listUrlInput))
})
}, [adding, dispatch, fetchList, listUrlInput])
const validUrl: boolean = useMemo(() => {
return uriToHttp(listUrlInput).length > 0 || Boolean(parseENSAddress(listUrlInput))
}, [listUrlInput])
const handleEnterKey = useCallback(
e => {
if (validUrl && e.key === 'Enter') {
handleAddList()
}
},
[handleAddList, validUrl]
)
const sortedLists = useMemo(() => {
const listUrls = Object.keys(lists)
return listUrls
.filter(listUrl => {
return Boolean(lists[listUrl].current)
})
.sort((u1, u2) => {
const { current: l1 } = lists[u1]
const { current: l2 } = lists[u2]
if (l1 && l2) {
return l1.name.toLowerCase() < l2.name.toLowerCase()
? -1
: l1.name.toLowerCase() === l2.name.toLowerCase()
? 0
: 1
}
if (l1) return -1
if (l2) return 1
return 0
})
}, [lists])
return (
<Column style={{ width: '100%', flex: '1 1' }}>
<PaddedColumn>
<RowBetween>
<div>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} />
</div>
<Text fontWeight={500} fontSize={20}>
Manage Lists
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
</PaddedColumn>
<Separator />
<PaddedColumn gap="14px">
<Text fontWeight={600}>
Add a list{' '}
<QuestionHelper text="Token lists are an open specification for lists of ERC20 tokens. You can use any token list by entering its URL below. Beware that third party token lists can contain fake or malicious ERC20 tokens." />
</Text>
<Row>
<SearchInput
type="text"
id="list-add-input"
placeholder="https:// or ipfs:// or ENS name"
value={listUrlInput}
onChange={handleInput}
onKeyDown={handleEnterKey}
style={{ height: '2.75rem', borderRadius: 12, padding: '12px' }}
/>
<AddListButton onClick={handleAddList} disabled={!validUrl}>
Add
</AddListButton>
</Row>
{addError ? (
<TYPE.error title={addError} style={{ textOverflow: 'ellipsis', overflow: 'hidden' }} error>
{addError}
</TYPE.error>
) : null}
</PaddedColumn>
<Separator />
<ListContainer>
{sortedLists.map(listUrl => (
<ListRow key={listUrl} listUrl={listUrl} onBack={onBack} />
))}
</ListContainer>
<Separator />
<div style={{ padding: '16px', textAlign: 'center' }}>
<ExternalLink href="https://tokenlists.org">Browse lists</ExternalLink>
</div>
</Column>
)
}

View File

@@ -31,6 +31,6 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
return tokens.filter(token => {
const { symbol, name } = token
return matchesSearch(symbol) || matchesSearch(name)
return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name))
})
}

View File

@@ -1,6 +1,5 @@
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Token, TokenAmount } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalances } from '../../state/wallet/hooks'
// compare two token amounts with highest one coming first
@@ -15,20 +14,13 @@ function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
return 0
}
function getTokenComparator(
weth: Token | undefined,
balances: { [tokenAddress: string]: TokenAmount }
): (tokenA: Token, tokenB: Token) => number {
function getTokenComparator(balances: {
[tokenAddress: string]: TokenAmount | undefined
}): (tokenA: Token, tokenB: Token) => number {
return function sortTokens(tokenA: Token, tokenB: Token): number {
// -1 = a is first
// 1 = b is first
// sort ETH first
if (weth) {
if (tokenA.equals(weth)) return -1
if (tokenB.equals(weth)) return 1
}
// sort by balances
const balanceA = balances[tokenA.address]
const balanceB = balances[tokenB.address]
@@ -36,16 +28,18 @@ function getTokenComparator(
const balanceComp = balanceComparator(balanceA, balanceB)
if (balanceComp !== 0) return balanceComp
// sort by symbol
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
if (tokenA.symbol && tokenB.symbol) {
// sort by symbol
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
} else {
return tokenA.symbol ? -1 : tokenB.symbol ? -1 : 0
}
}
}
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
const { chainId } = useActiveWeb3React()
const weth = WETH[chainId]
const balances = useAllTokenBalances()
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
const comparator = useMemo(() => getTokenComparator(balances ?? {}), [balances])
return useMemo(() => {
if (inverted) {
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1

View File

@@ -17,12 +17,26 @@ export const FadedSpan = styled(RowFixed)`
font-size: 14px;
`
export const GreySpan = styled.span`
color: ${({ theme }) => theme.text3};
font-weight: 400;
export const PaddedColumn = styled(AutoColumn)`
padding: 20px;
padding-bottom: 12px;
`
export const Input = styled.input`
export const MenuItem = styled(RowBetween)`
padding: 4px 20px;
height: 56px;
display: grid;
grid-template-columns: auto minmax(auto, 1fr) auto minmax(0, 72px);
grid-gap: 16px;
cursor: ${({ disabled }) => !disabled && 'pointer'};
pointer-events: ${({ disabled }) => disabled && 'none'};
:hover {
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
}
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
`
export const SearchInput = styled.input`
position: relative;
display: flex;
padding: 16px;
@@ -43,28 +57,20 @@ export const Input = styled.input`
::placeholder {
color: ${({ theme }) => theme.text3};
}
`
export const PaddedColumn = styled(AutoColumn)`
padding: 20px;
padding-bottom: 12px;
`
export const MenuItem = styled(RowBetween)`
padding: 4px 20px;
height: 56px;
cursor: ${({ disabled }) => !disabled && 'pointer'};
pointer-events: ${({ disabled }) => disabled && 'none'};
:hover {
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
}
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
`
export const SearchInput = styled(Input)`
transition: border 100ms;
:focus {
border: 1px solid ${({ theme }) => theme.primary1};
outline: none;
}
`
export const Separator = styled.div`
width: 100%;
height: 1px;
background-color: ${({ theme }) => theme.bg2};
`
export const SeparatorDark = styled.div`
width: 100%;
height: 1px;
background-color: ${({ theme }) => theme.bg3};
`

View File

@@ -1,14 +1,14 @@
import React, { useRef, useEffect, useContext, useState } from 'react'
import React, { useRef, useContext, useState } from 'react'
import { Settings, X } from 'react-feather'
import styled from 'styled-components'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import {
useUserSlippageTolerance,
useExpertModeManager,
useUserDeadline,
useDarkModeManager
} from '../../state/user/hooks'
import SlippageTabs from '../SlippageTabs'
import TransactionSettings from '../TransactionSettings'
import { RowFixed, RowBetween } from '../Row'
import { TYPE } from '../../theme'
import QuestionHelper from '../QuestionHelper'
@@ -88,6 +88,9 @@ const MenuFlyout = styled.span`
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: 1px solid ${({ theme }) => theme.bg3};
border-radius: 0.5rem;
display: flex;
flex-direction: column;
@@ -135,27 +138,11 @@ export default function SettingsTab() {
// 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])
useOnClickOutside(node, open ? toggle : undefined)
return (
<StyledMenu ref={node}>
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
<StyledMenu ref={node as any}>
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)} maxHeight={100}>
<ModalContentWrapper>
<AutoColumn gap="lg">
@@ -185,7 +172,7 @@ export default function SettingsTab() {
}
}}
>
<Text fontSize={20} fontWeight={500}>
<Text fontSize={20} fontWeight={500} id="confirm-expert-mode">
Turn On Expert Mode
</Text>
</ButtonError>
@@ -193,7 +180,7 @@ export default function SettingsTab() {
</AutoColumn>
</ModalContentWrapper>
</Modal>
<StyledMenuButton onClick={toggle}>
<StyledMenuButton onClick={toggle} id="open-settings-dialog-button">
<StyledMenuIcon />
{expertMode && (
<EmojiWrapper>
@@ -209,7 +196,7 @@ export default function SettingsTab() {
<Text fontWeight={600} fontSize={14}>
Transaction Settings
</Text>
<SlippageTabs
<TransactionSettings
rawSlippage={userSlippageTolerance}
setRawSlippage={setUserslippageTolerance}
deadline={deadline}
@@ -226,6 +213,7 @@ export default function SettingsTab() {
<QuestionHelper text="Bypasses confirmation modals and allows high slippage trades. Use at your own risk." />
</RowFixed>
<Toggle
id="toggle-expert-mode-button"
isActive={expertMode}
toggle={
expertMode

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from 'react'
import styled from 'styled-components'
const StyledRangeInput = styled.input<{ value: number }>`
const StyledRangeInput = styled.input<{ size: number }>`
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
width: 100%; /* Specific width is required for Firefox. */
background: transparent; /* Otherwise white in Chrome */
@@ -17,8 +17,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
&::-webkit-slider-thumb {
-webkit-appearance: none;
height: 28px;
width: 28px;
height: ${({ size }) => size}px;
width: ${({ size }) => size}px;
background-color: #565a69;
border-radius: 100%;
border: none;
@@ -33,8 +33,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
}
&::-moz-range-thumb {
height: 28px;
width: 28px;
height: ${({ size }) => size}px;
width: ${({ size }) => size}px;
background-color: #565a69;
border-radius: 100%;
border: none;
@@ -48,8 +48,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
}
&::-ms-thumb {
height: 28px;
width: 28px;
height: ${({ size }) => size}px;
width: ${({ size }) => size}px;
background-color: #565a69;
border-radius: 100%;
color: ${({ theme }) => theme.bg1};
@@ -62,24 +62,12 @@ const StyledRangeInput = styled.input<{ value: number }>`
}
&::-webkit-slider-runnable-track {
background: linear-gradient(
90deg,
${({ theme }) => theme.bg5},
${({ theme }) => theme.bg5} ${({ value }) => value}%,
${({ theme }) => theme.bg3} ${({ value }) => value}%,
${({ theme }) => theme.bg3}
);
background: linear-gradient(90deg, ${({ theme }) => theme.bg5}, ${({ theme }) => theme.bg3});
height: 2px;
}
&::-moz-range-track {
background: linear-gradient(
90deg,
${({ theme }) => theme.bg5},
${({ theme }) => theme.bg5} ${({ value }) => value}%,
${({ theme }) => theme.bg3} ${({ value }) => value}%,
${({ theme }) => theme.bg3}
);
background: linear-gradient(90deg, ${({ theme }) => theme.bg5}, ${({ theme }) => theme.bg3});
height: 2px;
}
@@ -102,26 +90,31 @@ const StyledRangeInput = styled.input<{ value: number }>`
interface InputSliderProps {
value: number
onChange: (value: number) => void
step?: number
min?: number
max?: number
size?: number
}
export default function InputSlider({ value, onChange }: InputSliderProps) {
export default function Slider({ value, onChange, min = 0, step = 1, max = 100, size = 28 }: InputSliderProps) {
const changeCallback = useCallback(
e => {
onChange(e.target.value)
onChange(parseInt(e.target.value))
},
[onChange]
)
return (
<StyledRangeInput
size={size}
type="range"
value={value}
style={{ width: '90%', marginLeft: 15, marginRight: 15, padding: '15px 0' }}
onChange={changeCallback}
aria-labelledby="input-slider"
step={1}
min={0}
max={100}
aria-labelledby="input slider"
step={step}
min={min}
max={max}
/>
)
}

View File

@@ -10,26 +10,26 @@ const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
font-weight: 400;
`
const StyledToggle = styled.a<{ isActive?: boolean; activeElement?: boolean }>`
const StyledToggle = styled.button<{ 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;
}
outline: none;
padding: 0;
background-color: transparent;
`
export interface ToggleProps {
id?: string
isActive: boolean
toggle: () => void
}
export default function Toggle({ isActive, toggle }: ToggleProps) {
export default function Toggle({ id, isActive, toggle }: ToggleProps) {
return (
<StyledToggle isActive={isActive} target="_self" onClick={toggle}>
<StyledToggle id={id} isActive={isActive} onClick={toggle}>
<ToggleElement isActive={isActive} isOnSwitch={true}>
On
</ToggleElement>

View File

@@ -1,140 +0,0 @@
import { Currency, Token } from '@uniswap/sdk'
import { transparentize } from 'polished'
import React, { useMemo } from 'react'
import styled from 'styled-components'
import { ReactComponent as Close } from '../../assets/images/x.svg'
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, isDefaultToken } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../QuestionHelper'
import CurrencyLogo from '../CurrencyLogo'
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-radius: 10px;
margin-bottom: 20px;
display: grid;
grid-template-rows: 14px auto auto;
grid-row-gap: 14px;
`
const Row = styled.div`
display: flex;
align-items: center;
justify-items: flex-start;
& > * {
margin-right: 6px;
}
`
const CloseColor = styled(Close)`
color: #aeaeae;
`
const CloseIcon = styled.div`
position: absolute;
right: 1rem;
top: 12px;
&:hover {
cursor: pointer;
opacity: 0.6;
}
& > * {
height: 16px;
width: 16px;
}
`
const HELP_TEXT = `
The Uniswap V2 smart contracts are designed to support any ERC20 token on Ethereum. Any token can be
loaded into the interface by entering its Ethereum address into the search field or passing it as a URL
parameter.
`
const DUPLICATE_NAME_HELP_TEXT = `${HELP_TEXT} This token has the same name or symbol as another token in your list.`
interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'> {
token?: Token
}
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
const { chainId } = useActiveWeb3React()
const isDefault = isDefaultToken(token)
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? ''
const [dismissed, dismissTokenWarning] = useTokenWarningDismissal(chainId, token)
const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => {
if (isDefault || !token || !chainId) return false
return Object.keys(allTokens).some(tokenAddress => {
const userToken = allTokens[tokenAddress]
if (userToken.equals(token)) {
return false
}
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
})
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
if (isDefault || !token || dismissed) return null
return (
<Wrapper error={duplicateNameOrSymbol} {...rest}>
{duplicateNameOrSymbol ? null : (
<CloseIcon onClick={dismissTokenWarning}>
<CloseColor />
</CloseIcon>
)}
<Row>
<TYPE.subHeader>{duplicateNameOrSymbol ? 'Duplicate token name or symbol' : 'Imported token'}</TYPE.subHeader>
<QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} />
</Row>
<Row>
<CurrencyLogo currency={token} />
<div style={{ fontWeight: 500 }}>
{token && token.name && token.symbol && token.name !== token.symbol
? `${token.name} (${token.symbol})`
: token.name || token.symbol}
</div>
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
(View on Etherscan)
</ExternalLink>
</Row>
<Row>
<TYPE.italic>Verify this is the correct token before making any transactions.</TYPE.italic>
</Row>
</Wrapper>
)
}
const WarningContainer = styled.div`
max-width: 420px;
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
`
export function TokenWarningCards({ currencies }: { currencies: { [field in Field]?: Currency } }) {
return (
<WarningContainer>
{Object.keys(currencies).map(field =>
currencies[field] instanceof Token ? (
<TokenWarningCard style={{ marginBottom: 14 }} key={field} token={currencies[field]} />
) : null
)}
</WarningContainer>
)
}

View File

@@ -0,0 +1,153 @@
import { Token } from '@uniswap/sdk'
import { transparentize } from 'polished'
import React, { useCallback, useMemo, useState } from 'react'
import styled from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, shortenAddress } from '../../utils'
import CurrencyLogo from '../CurrencyLogo'
import Modal from '../Modal'
import { AutoRow, RowBetween } from '../Row'
import { AutoColumn } from '../Column'
import { AlertTriangle } from 'react-feather'
import { ButtonError } from '../Button'
const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme }) => transparentize(0.6, theme.bg3)};
padding: 0.75rem;
border-radius: 20px;
`
const WarningContainer = styled.div`
max-width: 420px;
width: 100%;
padding: 1rem;
background: rgba(242, 150, 2, 0.05);
border: 1px solid #f3841e;
border-radius: 20px;
overflow: auto;
`
const StyledWarningIcon = styled(AlertTriangle)`
stroke: ${({ theme }) => theme.red2};
`
interface TokenWarningCardProps {
token?: Token
}
function TokenWarningCard({ token }: TokenWarningCardProps) {
const { chainId } = useActiveWeb3React()
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? ''
const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => {
if (!token || !chainId) return false
return Object.keys(allTokens).some(tokenAddress => {
const userToken = allTokens[tokenAddress]
if (userToken.equals(token)) {
return false
}
return userToken.symbol?.toLowerCase() === tokenSymbol || userToken.name?.toLowerCase() === tokenName
})
}, [token, chainId, allTokens, tokenSymbol, tokenName])
if (!token) return null
return (
<Wrapper error={duplicateNameOrSymbol}>
<AutoRow gap="6px">
<AutoColumn gap="24px">
<CurrencyLogo currency={token} size={'16px'} />
<div> </div>
</AutoColumn>
<AutoColumn gap="10px" justify="flex-start">
<TYPE.main>
{token && token.name && token.symbol && token.name !== token.symbol
? `${token.name} (${token.symbol})`
: token.name || token.symbol}{' '}
</TYPE.main>
{chainId && (
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
<TYPE.blue title={token.address}>{shortenAddress(token.address)} (View on Etherscan)</TYPE.blue>
</ExternalLink>
)}
</AutoColumn>
</AutoRow>
</Wrapper>
)
}
export default function TokenWarningModal({
isOpen,
tokens,
onConfirm
}: {
isOpen: boolean
tokens: Token[]
onConfirm: () => void
}) {
const [understandChecked, setUnderstandChecked] = useState(false)
const toggleUnderstand = useCallback(() => setUnderstandChecked(uc => !uc), [])
const handleDismiss = useCallback(() => null, [])
return (
<Modal isOpen={isOpen} onDismiss={handleDismiss} maxHeight={90}>
<WarningContainer className="token-warning-container">
<AutoColumn gap="lg">
<AutoRow gap="6px">
<StyledWarningIcon />
<TYPE.main color={'red2'}>Token imported</TYPE.main>
</AutoRow>
<TYPE.body color={'red2'}>
Anyone can create an ERC20 token on Ethereum with <em>any</em> name, including creating fake versions of
existing tokens and tokens that claim to represent projects that do not have a token.
</TYPE.body>
<TYPE.body color={'red2'}>
This interface can load arbitrary tokens by token addresses. Please take extra caution and do your research
when interacting with arbitrary ERC20 tokens.
</TYPE.body>
<TYPE.body color={'red2'}>
If you purchase an arbitrary token, <strong>you may be unable to sell it back.</strong>
</TYPE.body>
{tokens.map(token => {
return <TokenWarningCard key={token.address} token={token} />
})}
<RowBetween>
<div>
<label style={{ cursor: 'pointer', userSelect: 'none' }}>
<input
type="checkbox"
className="understand-checkbox"
checked={understandChecked}
onChange={toggleUnderstand}
/>{' '}
I understand
</label>
</div>
<ButtonError
disabled={!understandChecked}
error={true}
width={'140px'}
padding="0.5rem 1rem"
className="token-dismiss-button"
style={{
borderRadius: '10px'
}}
onClick={() => {
onConfirm()
}}
>
<TYPE.body color="white">Continue</TYPE.body>
</ButtonError>
</RowBetween>
</AutoColumn>
</WarningContainer>
</Modal>
)
}

View File

@@ -0,0 +1,197 @@
import { ChainId } from '@uniswap/sdk'
import React, { useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import Modal from '../Modal'
import { ExternalLink } from '../../theme'
import { Text } from 'rebass'
import { CloseIcon, Spinner } from '../../theme/components'
import { RowBetween } from '../Row'
import { AlertTriangle, ArrowUpCircle } from 'react-feather'
import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import Circle from '../../assets/images/blue-loader.svg'
import { getEtherscanLink } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
const Wrapper = styled.div`
width: 100%;
`
const Section = styled(AutoColumn)`
padding: 24px;
`
const BottomSection = styled(Section)`
background-color: ${({ theme }) => theme.bg2};
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
`
const ConfirmedIcon = styled(ColumnCenter)`
padding: 60px 0;
`
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
height: ${({ size }) => size};
width: ${({ size }) => size};
`
function ConfirmationPendingContent({ onDismiss, pendingText }: { onDismiss: () => void; pendingText: string }) {
return (
<Wrapper>
<Section>
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
<ConfirmedIcon>
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20}>
Waiting For Confirmation
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
{pendingText}
</Text>
</AutoColumn>
<Text fontSize={12} color="#565A69" textAlign="center">
Confirm this transaction in your wallet
</Text>
</AutoColumn>
</Section>
</Wrapper>
)
}
function TransactionSubmittedContent({
onDismiss,
chainId,
hash
}: {
onDismiss: () => void
hash: string | undefined
chainId: ChainId
}) {
const theme = useContext(ThemeContext)
return (
<Wrapper>
<Section>
<RowBetween>
<div />
<CloseIcon onClick={onDismiss} />
</RowBetween>
<ConfirmedIcon>
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20}>
Transaction Submitted
</Text>
{chainId && hash && (
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
View on Etherscan
</Text>
</ExternalLink>
)}
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
<Text fontWeight={500} fontSize={20}>
Close
</Text>
</ButtonPrimary>
</AutoColumn>
</Section>
</Wrapper>
)
}
export function ConfirmationModalContent({
title,
bottomContent,
onDismiss,
topContent
}: {
title: string
onDismiss: () => void
topContent: () => React.ReactNode
bottomContent: () => React.ReactNode
}) {
return (
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
{topContent()}
</Section>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
)
}
export function TransactionErrorContent({ message, onDismiss }: { message: string; onDismiss: () => void }) {
const theme = useContext(ThemeContext)
return (
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
Error
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
<AutoColumn style={{ marginTop: 20, padding: '2rem 0' }} gap="24px" justify="center">
<AlertTriangle color={theme.red1} style={{ strokeWidth: 1.5 }} size={64} />
<Text fontWeight={500} fontSize={16} color={theme.red1} style={{ textAlign: 'center', width: '85%' }}>
{message}
</Text>
</AutoColumn>
</Section>
<BottomSection gap="12px">
<ButtonPrimary onClick={onDismiss}>Dismiss</ButtonPrimary>
</BottomSection>
</Wrapper>
)
}
interface ConfirmationModalProps {
isOpen: boolean
onDismiss: () => void
hash: string | undefined
content: () => React.ReactNode
attemptingTxn: boolean
pendingText: string
}
export default function TransactionConfirmationModal({
isOpen,
onDismiss,
attemptingTxn,
hash,
pendingText,
content
}: ConfirmationModalProps) {
const { chainId } = useActiveWeb3React()
if (!chainId) return null
// confirmation screen
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
{attemptingTxn ? (
<ConfirmationPendingContent onDismiss={onDismiss} pendingText={pendingText} />
) : hash ? (
<TransactionSubmittedContent chainId={chainId} hash={hash} onDismiss={onDismiss} />
) : (
content()
)}
</Modal>
)
}

View File

@@ -104,48 +104,44 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
let slippageError: SlippageError
let slippageError: SlippageError | undefined
if (slippageInput !== '' && !slippageInputIsValid) {
slippageError = SlippageError.InvalidInput
} else if (slippageInputIsValid && rawSlippage < 50) {
slippageError = SlippageError.RiskyLow
} else if (slippageInputIsValid && rawSlippage > 500) {
slippageError = SlippageError.RiskyHigh
} else {
slippageError = undefined
}
let deadlineError: DeadlineError
let deadlineError: DeadlineError | undefined
if (deadlineInput !== '' && !deadlineInputIsValid) {
deadlineError = DeadlineError.InvalidInput
} else {
deadlineError = undefined
}
function parseCustomSlippage(event) {
setSlippageInput(event.target.value)
function parseCustomSlippage(value: string) {
setSlippageInput(value)
let valueAsIntFromRoundedFloat: number
try {
valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(event.target.value) * 100).toString())
const valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(value) * 100).toString())
if (!Number.isNaN(valueAsIntFromRoundedFloat) && valueAsIntFromRoundedFloat < 5000) {
setRawSlippage(valueAsIntFromRoundedFloat)
}
} catch {}
if (
typeof valueAsIntFromRoundedFloat === 'number' &&
!Number.isNaN(valueAsIntFromRoundedFloat) &&
valueAsIntFromRoundedFloat < 5000
) {
setRawSlippage(valueAsIntFromRoundedFloat)
}
}
function parseCustomDeadline(event) {
setDeadlineInput(event.target.value)
function parseCustomDeadline(value: string) {
setDeadlineInput(value)
let valueAsInt: number
try {
valueAsInt = Number.parseInt(event.target.value) * 60
const valueAsInt: number = Number.parseInt(value) * 60
if (!Number.isNaN(valueAsInt) && valueAsInt > 0) {
setDeadline(valueAsInt)
}
} catch {}
if (typeof valueAsInt === 'number' && !Number.isNaN(valueAsInt) && valueAsInt > 0) {
setDeadline(valueAsInt)
}
}
return (
@@ -195,14 +191,15 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
</span>
</SlippageEmojiContainer>
) : null}
{/* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 */}
<Input
ref={inputRef}
ref={inputRef as any}
placeholder={(rawSlippage / 100).toFixed(2)}
value={slippageInput}
onBlur={() => {
parseCustomSlippage({ target: { value: (rawSlippage / 100).toFixed(2) } })
parseCustomSlippage((rawSlippage / 100).toFixed(2))
}}
onChange={parseCustomSlippage}
onChange={e => parseCustomSlippage(e.target.value)}
color={!slippageInputIsValid ? 'red' : ''}
/>
%
@@ -238,11 +235,11 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<Input
color={!!deadlineError ? 'red' : undefined}
onBlur={() => {
parseCustomDeadline({ target: { value: (deadline / 60).toString() } })
parseCustomDeadline((deadline / 60).toString())
}}
placeholder={(deadline / 60).toString()}
value={deadlineInput}
onChange={parseCustomDeadline}
onChange={e => parseCustomDeadline(e.target.value)}
/>
</OptionCustom>
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>

View File

@@ -1,71 +0,0 @@
import React, { useCallback, useContext, useState } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather'
import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import useInterval from '../../hooks/useInterval'
import { useRemovePopup } from '../../state/application/hooks'
import { TYPE } from '../../theme'
import { ExternalLink } from '../../theme/components'
import { getEtherscanLink } from '../../utils'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
const Fader = styled.div<{ count: number }>`
position: absolute;
bottom: 0px;
left: 0px;
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
height: 2px;
background-color: ${({ theme }) => theme.bg3};
transition: width 100ms linear;
`
const delay = 100
export default function TxnPopup({
hash,
success,
summary,
popKey
}: {
hash: string
success?: boolean
summary?: string
popKey?: string
}) {
const { chainId } = useActiveWeb3React()
const [count, setCount] = useState(1)
const [isRunning, setIsRunning] = useState(true)
const removePopup = useRemovePopup()
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
useInterval(
() => {
count > 150 ? removeThisPopup() : setCount(count + 1)
},
isRunning ? delay : null
)
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
const theme = useContext(ThemeContext)
return (
<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 ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn>
<Fader count={count} />
</AutoRow>
)
}

View File

@@ -73,7 +73,7 @@ const SubHeader = styled.div`
font-size: 12px;
`
const IconWrapper = styled.div<{ size?: number }>`
const IconWrapper = styled.div<{ size?: number | null }>`
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;
justify-content: center;
@@ -90,7 +90,7 @@ const IconWrapper = styled.div<{ size?: number }>`
export default function Option({
link = null,
clickable = true,
size = null,
size,
onClick = null,
color,
header,

View File

@@ -86,7 +86,7 @@ export default function PendingView({
<ErrorButton
onClick={() => {
setPendingError(false)
tryActivation(connector)
connector && tryActivation(connector)
}}
>
Try Again

View File

@@ -17,6 +17,7 @@ import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, fortmatic, portis } from '../../connectors'
import { OVERLAY_READY } from '../../connectors/Fortmatic'
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
import { AbstractConnector } from '@web3-react/abstract-connector'
const CloseIcon = styled.div`
position: absolute;
@@ -128,7 +129,7 @@ export default function WalletModal({
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
const [pendingWallet, setPendingWallet] = useState()
const [pendingWallet, setPendingWallet] = useState<AbstractConnector | undefined>()
const [pendingError, setPendingError] = useState<boolean>()
@@ -161,7 +162,7 @@ export default function WalletModal({
}
}, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious])
const tryActivation = async connector => {
const tryActivation = async (connector: AbstractConnector | undefined) => {
let name = ''
Object.keys(SUPPORTED_WALLETS).map(key => {
if (connector === SUPPORTED_WALLETS[key].connector) {
@@ -183,13 +184,14 @@ export default function WalletModal({
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
} else {
setPendingError(true)
}
})
connector &&
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
} else {
setPendingError(true)
}
})
}
// close wallet modal if fortmatic modal is active
@@ -349,9 +351,7 @@ export default function WalletModal({
{walletView !== WALLET_VIEWS.PENDING && (
<Blurb>
<span>New to Ethereum? &nbsp;</span>{' '}
<ExternalLink href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
Learn more about wallets
</ExternalLink>
<ExternalLink href="https://ethereum.org/wallets/">Learn more about wallets</ExternalLink>
</Blurb>
)}
</ContentWrapper>
@@ -360,7 +360,7 @@ export default function WalletModal({
}
return (
<Modal isOpen={walletModalOpen} onDismiss={toggleWalletModal} minHeight={null} maxHeight={90}>
<Modal isOpen={walletModalOpen} onDismiss={toggleWalletModal} minHeight={false} maxHeight={90}>
<Wrapper>{getModalContent()}</Wrapper>
</Modal>
)

View File

@@ -19,7 +19,7 @@ const Message = styled.h2`
color: ${({ theme }) => theme.secondary1};
`
export default function Web3ReactManager({ children }) {
export default function Web3ReactManager({ children }: { children: JSX.Element }) {
const { t } = useTranslation()
const { active } = useWeb3React()
const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName)

View File

@@ -1,28 +1,29 @@
import React, { useMemo } from 'react'
import styled, { css } from 'styled-components'
import { useTranslation } from 'react-i18next'
import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
import { AbstractConnector } from '@web3-react/abstract-connector'
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
import { darken, lighten } from 'polished'
import React, { useMemo } from 'react'
import { Activity } from 'react-feather'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import PortisIcon from '../../assets/images/portisIcon.png'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors'
import { NetworkContextName } from '../../constants'
import useENSName from '../../hooks/useENSName'
import { useHasSocks } from '../../hooks/useSocksBalance'
import { useWalletModalToggle } from '../../state/application/hooks'
import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks'
import { TransactionDetails } from '../../state/transactions/reducer'
import { shortenAddress } from '../../utils'
import { ButtonSecondary } from '../Button'
import Identicon from '../Identicon'
import PortisIcon from '../../assets/images/portisIcon.png'
import WalletModal from '../WalletModal'
import { ButtonSecondary } from '../Button'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import Loader from '../Loader'
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'
import Loader from '../Loader'
import WalletModal from '../WalletModal'
const IconWrapper = styled.div<{ size?: number }>`
${({ theme }) => theme.flexColumnNoWrap};
@@ -118,104 +119,114 @@ const NetworkIcon = styled(Activity)`
`
// we want the latest one to come first, so return negative if a is after b
function newTranscationsFirst(a: TransactionDetails, b: TransactionDetails) {
function newTransactionsFirst(a: TransactionDetails, b: TransactionDetails) {
return b.addedTime - a.addedTime
}
function recentTransactionsOnly(a: TransactionDetails) {
return new Date().getTime() - a.addedTime < 86_400_000
}
const SOCK = (
<span role="img" aria-label="has socks emoji" style={{ marginTop: -4, marginBottom: -4 }}>
🧦
</span>
)
export default function Web3Status() {
const { t } = useTranslation()
const { active, account, connector, error } = useWeb3React()
const contextNetwork = useWeb3React(NetworkContextName)
// eslint-disable-next-line react/prop-types
function StatusIcon({ connector }: { connector: AbstractConnector }) {
if (connector === injected) {
return <Identicon />
} else if (connector === walletconnect) {
return (
<IconWrapper size={16}>
<img src={WalletConnectIcon} alt={''} />
</IconWrapper>
)
} else if (connector === walletlink) {
return (
<IconWrapper size={16}>
<img src={CoinbaseWalletIcon} alt={''} />
</IconWrapper>
)
} else if (connector === fortmatic) {
return (
<IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} />
</IconWrapper>
)
} else if (connector === portis) {
return (
<IconWrapper size={16}>
<img src={PortisIcon} alt={''} />
</IconWrapper>
)
}
return null
}
const { ENSName } = useENSName(account)
function Web3StatusInner() {
const { t } = useTranslation()
const { account, connector, error } = useWeb3React()
const { ENSName } = useENSName(account ?? undefined)
const allTransactions = useAllTransactions()
const sortedRecentTransactions = useMemo(() => {
const txs = Object.values(allTransactions)
return txs.filter(recentTransactionsOnly).sort(newTranscationsFirst)
return txs.filter(isTransactionRecent).sort(newTransactionsFirst)
}, [allTransactions])
const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash)
const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash)
const hasPendingTransactions = !!pending.length
const hasSocks = useHasSocks()
const toggleWalletModal = useWalletModalToggle()
// handle the logo we want to show with the account
function getStatusIcon() {
if (connector === injected) {
return <Identicon />
} else if (connector === walletconnect) {
return (
<IconWrapper size={16}>
<img src={WalletConnectIcon} alt={''} />
</IconWrapper>
)
} else if (connector === walletlink) {
return (
<IconWrapper size={16}>
<img src={CoinbaseWalletIcon} alt={''} />
</IconWrapper>
)
} else if (connector === fortmatic) {
return (
<IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} />
</IconWrapper>
)
} else if (connector === portis) {
return (
<IconWrapper size={16}>
<img src={PortisIcon} alt={''} />
</IconWrapper>
)
}
if (account) {
return (
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
{hasPendingTransactions ? (
<RowBetween>
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
</RowBetween>
) : (
<>
{hasSocks ? SOCK : null}
<Text>{ENSName || shortenAddress(account)}</Text>
</>
)}
{!hasPendingTransactions && connector && <StatusIcon connector={connector} />}
</Web3StatusConnected>
)
} else if (error) {
return (
<Web3StatusError onClick={toggleWalletModal}>
<NetworkIcon />
<Text>{error instanceof UnsupportedChainIdError ? 'Wrong Network' : 'Error'}</Text>
</Web3StatusError>
)
} else {
return (
<Web3StatusConnect id="connect-wallet" onClick={toggleWalletModal} faded={!account}>
<Text>{t('Connect to a wallet')}</Text>
</Web3StatusConnect>
)
}
}
function getWeb3Status() {
if (account) {
return (
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
{hasPendingTransactions ? (
<RowBetween>
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
</RowBetween>
) : (
<>
{hasSocks ? SOCK : null}
<Text>{ENSName || shortenAddress(account)}</Text>
</>
)}
{!hasPendingTransactions && getStatusIcon()}
</Web3StatusConnected>
)
} else if (error) {
return (
<Web3StatusError onClick={toggleWalletModal}>
<NetworkIcon />
<Text>{error instanceof UnsupportedChainIdError ? 'Wrong Network' : 'Error'}</Text>
</Web3StatusError>
)
} else {
return (
<Web3StatusConnect id="connect-wallet" onClick={toggleWalletModal} faded={!account}>
<Text>{t('Connect to a wallet')}</Text>
</Web3StatusConnect>
)
}
}
export default function Web3Status() {
const { active, account } = useWeb3React()
const contextNetwork = useWeb3React(NetworkContextName)
const { ENSName } = useENSName(account ?? undefined)
const allTransactions = useAllTransactions()
const sortedRecentTransactions = useMemo(() => {
const txs = Object.values(allTransactions)
return txs.filter(isTransactionRecent).sort(newTransactionsFirst)
}, [allTransactions])
const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash)
const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash)
if (!contextNetwork.active && !active) {
return null
@@ -223,8 +234,8 @@ export default function Web3Status() {
return (
<>
{getWeb3Status()}
<WalletModal ENSName={ENSName} pendingTransactions={pending} confirmedTransactions={confirmed} />
<Web3StatusInner />
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
</>
)
}

View File

@@ -3,7 +3,7 @@ import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
// fires a GA pageview every time the route changes
export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps) {
export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps): null {
useEffect(() => {
ReactGA.pageview(`${pathname}${search}`)
}, [pathname, search])

View File

@@ -73,23 +73,27 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
const [allowedSlippage] = useUserSlippageTolerance()
const showRoute = trade?.route?.path?.length > 2
const showRoute = Boolean(trade && trade.route.path.length > 2)
return (
<AutoColumn gap="md">
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
{showRoute && (
{trade && (
<>
<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>
<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>

View File

@@ -1,6 +1,6 @@
import React from 'react'
import styled from 'styled-components'
import useLast from '../../hooks/useLast'
import { useLastTruthy } from '../../hooks/useLast'
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
@@ -20,11 +20,11 @@ const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
`
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
const lastTrade = useLast(trade)
const lastTrade = useLastTruthy(trade)
return (
<AdvancedDetailsFooter show={Boolean(trade)}>
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade} />
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade ?? undefined} />
</AdvancedDetailsFooter>
)
}

View File

@@ -0,0 +1,109 @@
import { currencyEquals, Trade } from '@uniswap/sdk'
import React, { useCallback, useMemo } from 'react'
import TransactionConfirmationModal, {
ConfirmationModalContent,
TransactionErrorContent
} from '../TransactionConfirmationModal'
import SwapModalFooter from './SwapModalFooter'
import SwapModalHeader from './SwapModalHeader'
/**
* Returns true if the trade requires a confirmation of details before we can submit it
* @param tradeA trade A
* @param tradeB trade B
*/
function tradeMeaningfullyDiffers(tradeA: Trade, tradeB: Trade): boolean {
return (
tradeA.tradeType !== tradeB.tradeType ||
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
!tradeA.inputAmount.equalTo(tradeB.inputAmount) ||
!currencyEquals(tradeA.outputAmount.currency, tradeB.outputAmount.currency) ||
!tradeA.outputAmount.equalTo(tradeB.outputAmount)
)
}
export default function ConfirmSwapModal({
trade,
originalTrade,
onAcceptChanges,
allowedSlippage,
onConfirm,
onDismiss,
recipient,
swapErrorMessage,
isOpen,
attemptingTxn,
txHash
}: {
isOpen: boolean
trade: Trade | undefined
originalTrade: Trade | undefined
attemptingTxn: boolean
txHash: string | undefined
recipient: string | null
allowedSlippage: number
onAcceptChanges: () => void
onConfirm: () => void
swapErrorMessage: string | undefined
onDismiss: () => void
}) {
const showAcceptChanges = useMemo(
() => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
[originalTrade, trade]
)
const modalHeader = useCallback(() => {
return trade ? (
<SwapModalHeader
trade={trade}
allowedSlippage={allowedSlippage}
recipient={recipient}
showAcceptChanges={showAcceptChanges}
onAcceptChanges={onAcceptChanges}
/>
) : null
}, [allowedSlippage, onAcceptChanges, recipient, showAcceptChanges, trade])
const modalBottom = useCallback(() => {
return trade ? (
<SwapModalFooter
onConfirm={onConfirm}
trade={trade}
disabledConfirm={showAcceptChanges}
swapErrorMessage={swapErrorMessage}
allowedSlippage={allowedSlippage}
/>
) : null
}, [allowedSlippage, onConfirm, showAcceptChanges, swapErrorMessage, trade])
// text to show while loading
const pendingText = `Swapping ${trade?.inputAmount?.toSignificant(6)} ${
trade?.inputAmount?.currency?.symbol
} for ${trade?.outputAmount?.toSignificant(6)} ${trade?.outputAmount?.currency?.symbol}`
const confirmationContent = useCallback(
() =>
swapErrorMessage ? (
<TransactionErrorContent onDismiss={onDismiss} message={swapErrorMessage} />
) : (
<ConfirmationModalContent
title="Confirm Swap"
onDismiss={onDismiss}
topContent={modalHeader}
bottomContent={modalBottom}
/>
),
[onDismiss, modalBottom, modalHeader, swapErrorMessage]
)
return (
<TransactionConfirmationModal
isOpen={isOpen}
onDismiss={onDismiss}
attemptingTxn={attemptingTxn}
hash={txHash}
content={confirmationContent}
pendingText={pendingText}
/>
)
}

View File

@@ -4,10 +4,13 @@ import { ONE_BIPS } from '../../constants'
import { warningSeverity } from '../../utils/prices'
import { ErrorText } from './styleds'
/**
* Formatted version of price impact text with warning colors
*/
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return (
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
{priceImpact ? (priceImpact.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact.toFixed(2)}%`) : '-'}
</ErrorText>
)
}

View File

@@ -1,46 +1,44 @@
import { CurrencyAmount, Percent, Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Trade, TradeType } from '@uniswap/sdk'
import React, { useContext, useMemo, useState } from 'react'
import { Repeat } from 'react-feather'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions'
import { TYPE } from '../../theme'
import { formatExecutionPrice } from '../../utils/prices'
import {
computeSlippageAdjustedAmounts,
computeTradePriceBreakdown,
formatExecutionPrice,
warningSeverity
} from '../../utils/prices'
import { ButtonError } from '../Button'
import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import FormattedPriceImpact from './FormattedPriceImpact'
import { StyledBalanceMaxMini } from './styleds'
import { StyledBalanceMaxMini, SwapCallbackError } from './styleds'
export default function SwapModalFooter({
trade,
showInverted,
setShowInverted,
severity,
slippageAdjustedAmounts,
onSwap,
parsedAmounts,
realizedLPFee,
priceImpactWithoutFee,
confirmText
onConfirm,
allowedSlippage,
swapErrorMessage,
disabledConfirm
}: {
trade?: Trade
showInverted: boolean
setShowInverted: (inverted: boolean) => void
severity: number
slippageAdjustedAmounts?: { [field in Field]?: CurrencyAmount }
onSwap: () => any
parsedAmounts?: { [field in Field]?: CurrencyAmount }
realizedLPFee?: CurrencyAmount
priceImpactWithoutFee?: Percent
confirmText: string
trade: Trade
allowedSlippage: number
onConfirm: () => void
swapErrorMessage: string | undefined
disabledConfirm: boolean
}) {
const [showInverted, setShowInverted] = useState<boolean>(false)
const theme = useContext(ThemeContext)
if (!trade) {
return null
}
const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [
allowedSlippage,
trade
])
const { priceImpactWithoutFee, realizedLPFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade])
const severity = warningSeverity(priceImpactWithoutFee)
return (
<>
@@ -71,23 +69,21 @@ export default function SwapModalFooter({
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'}
{trade.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'}
</TYPE.black>
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
</RowFixed>
<RowFixed>
<TYPE.black fontSize={14}>
{trade?.tradeType === TradeType.EXACT_INPUT
{trade.tradeType === TradeType.EXACT_INPUT
? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-'
: slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'}
</TYPE.black>
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
<TYPE.black fontSize={14} marginLeft={'4px'}>
{trade?.tradeType === TradeType.EXACT_INPUT
? parsedAmounts[Field.OUTPUT]?.currency?.symbol
: parsedAmounts[Field.INPUT]?.currency?.symbol}
</TYPE.black>
)}
<TYPE.black fontSize={14} marginLeft={'4px'}>
{trade.tradeType === TradeType.EXACT_INPUT
? trade.outputAmount.currency.symbol
: trade.inputAmount.currency.symbol}
</TYPE.black>
</RowFixed>
</RowBetween>
<RowBetween>
@@ -107,17 +103,25 @@ export default function SwapModalFooter({
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed>
<TYPE.black fontSize={14}>
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.currency?.symbol : '-'}
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade.inputAmount.currency.symbol : '-'}
</TYPE.black>
</RowBetween>
</AutoColumn>
<AutoRow>
<ButtonError onClick={onSwap} error={severity > 2} style={{ margin: '10px 0 0 0' }} id="confirm-swap-or-send">
<ButtonError
onClick={onConfirm}
disabled={disabledConfirm}
error={severity > 2}
style={{ margin: '10px 0 0 0' }}
id="confirm-swap-or-send"
>
<Text fontSize={20} fontWeight={500}>
{confirmText}
{severity > 2 ? 'Swap Anyway' : 'Confirm Swap'}
</Text>
</ButtonError>
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
</AutoRow>
</>
)

View File

@@ -1,66 +1,107 @@
import { Currency, CurrencyAmount } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { ArrowDown } from 'react-feather'
import { Trade, TradeType } from '@uniswap/sdk'
import React, { useContext, useMemo } from 'react'
import { ArrowDown, AlertTriangle } from 'react-feather'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions'
import { TYPE } from '../../theme'
import { ButtonPrimary } from '../Button'
import { isAddress, shortenAddress } from '../../utils'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import CurrencyLogo from '../CurrencyLogo'
import { TruncatedText } from './styleds'
import { RowBetween, RowFixed } from '../Row'
import { TruncatedText, SwapShowAcceptChanges } from './styleds'
export default function SwapModalHeader({
currencies,
formattedAmounts,
slippageAdjustedAmounts,
priceImpactSeverity,
independentField,
recipient
trade,
allowedSlippage,
recipient,
showAcceptChanges,
onAcceptChanges
}: {
currencies: { [field in Field]?: Currency }
formattedAmounts: { [field in Field]?: string }
slippageAdjustedAmounts: { [field in Field]?: CurrencyAmount }
priceImpactSeverity: number
independentField: Field
trade: Trade
allowedSlippage: number
recipient: string | null
showAcceptChanges: boolean
onAcceptChanges: () => void
}) {
const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [
trade,
allowedSlippage
])
const { priceImpactWithoutFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade])
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
const theme = useContext(ThemeContext)
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500}>
{formattedAmounts[Field.INPUT]}
</TruncatedText>
<RowFixed gap="4px">
<CurrencyLogo currency={currencies[Field.INPUT]} size={'24px'} />
<RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.inputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
<TruncatedText
fontSize={24}
fontWeight={500}
color={showAcceptChanges && trade.tradeType === TradeType.EXACT_OUTPUT ? theme.primary1 : ''}
>
{trade.inputAmount.toSignificant(6)}
</TruncatedText>
</RowFixed>
<RowFixed gap={'0px'}>
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencies[Field.INPUT]?.symbol}
{trade.inputAmount.currency.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowFixed>
<ArrowDown size="16" color={theme.text2} />
<ArrowDown size="16" color={theme.text2} style={{ marginLeft: '4px', minWidth: '16px' }} />
</RowFixed>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
{formattedAmounts[Field.OUTPUT]}
</TruncatedText>
<RowFixed gap="4px">
<CurrencyLogo currency={currencies[Field.OUTPUT]} size={'24px'} />
<RowFixed gap={'0px'}>
<CurrencyLogo currency={trade.outputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
<TruncatedText
fontSize={24}
fontWeight={500}
color={
priceImpactSeverity > 2
? theme.red1
: showAcceptChanges && trade.tradeType === TradeType.EXACT_INPUT
? theme.primary1
: ''
}
>
{trade.outputAmount.toSignificant(6)}
</TruncatedText>
</RowFixed>
<RowFixed gap={'0px'}>
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{currencies[Field.OUTPUT]?.symbol}
{trade.outputAmount.currency.symbol}
</Text>
</RowFixed>
</RowBetween>
{showAcceptChanges ? (
<SwapShowAcceptChanges justify="flex-start" gap={'0px'}>
<RowBetween>
<RowFixed>
<AlertTriangle size={20} style={{ marginRight: '8px', minWidth: 24 }} />
<TYPE.main color={theme.primary1}> Price Updated</TYPE.main>
</RowFixed>
<ButtonPrimary
style={{ padding: '.5rem', width: 'fit-content', fontSize: '0.825rem', borderRadius: '12px' }}
onClick={onAcceptChanges}
>
Accept
</ButtonPrimary>
</RowBetween>
</SwapShowAcceptChanges>
) : null}
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
{independentField === Field.INPUT ? (
{trade.tradeType === TradeType.EXACT_INPUT ? (
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `}
<b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {currencies[Field.OUTPUT]?.symbol}
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {trade.outputAmount.currency.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>
@@ -68,7 +109,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Input is estimated. You will sell at most `}
<b>
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {currencies[Field.INPUT]?.symbol}
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {trade.inputAmount.currency.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { Currency, Price } from '@uniswap/sdk'
import { Price } from '@uniswap/sdk'
import { useContext } from 'react'
import { Repeat } from 'react-feather'
import { Text } from 'rebass'
@@ -8,27 +8,19 @@ import { StyledBalanceMaxMini } from './styleds'
interface TradePriceProps {
price?: Price
inputCurrency?: Currency
outputCurrency?: Currency
showInverted: boolean
setShowInverted: (showInverted: boolean) => void
}
export default function TradePrice({
price,
inputCurrency,
outputCurrency,
showInverted,
setShowInverted
}: TradePriceProps) {
export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
const theme = useContext(ThemeContext)
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
const show = Boolean(inputCurrency && outputCurrency)
const show = Boolean(price?.baseCurrency && price?.quoteCurrency)
const label = showInverted
? `${outputCurrency?.symbol} per ${inputCurrency?.symbol}`
: `${inputCurrency?.symbol} per ${outputCurrency?.symbol}`
? `${price?.quoteCurrency?.symbol} per ${price?.baseCurrency?.symbol}`
: `${price?.baseCurrency?.symbol} per ${price?.quoteCurrency?.symbol}`
return (
<Text

View File

@@ -1,7 +1,11 @@
// gathers additional user consent for a high price impact
import { Percent } from '@uniswap/sdk'
import { ALLOWED_PRICE_IMPACT_HIGH, PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN } from '../../constants'
/**
* Given the price impact, get user confirmation.
*
* @param priceImpactWithoutFee price impact of the trade without the fee.
*/
export default function confirmPriceImpactWithoutFee(priceImpactWithoutFee: Percent): boolean {
if (!priceImpactWithoutFee.lessThan(PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN)) {
return (

View File

@@ -1,8 +1,9 @@
import { transparentize } from 'polished'
import React from 'react'
import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components'
import { AutoColumn } from '../Column'
import { Text } from 'rebass'
import NumericalInput from '../NumericalInput'
import { AutoColumn } from '../Column'
export const Wrapper = styled.div`
position: relative;
@@ -29,8 +30,7 @@ export const SectionBreak = styled.div`
`
export const BottomGrouping = styled.div`
margin-top: 12px;
position: relative;
margin-top: 1rem;
`
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
@@ -44,21 +44,6 @@ export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
: theme.green1};
`
export const InputGroup = styled(AutoColumn)`
position: relative;
padding: 40px 0 20px 0;
`
export const StyledNumerical = styled(NumericalInput)`
text-align: center;
font-size: 48px;
font-weight: 500px;
width: 100%;
::placeholder {
color: ${({ theme }) => theme.text4};
}
`
export const StyledBalanceMaxMini = styled.button`
height: 22px;
width: 22px;
@@ -112,3 +97,51 @@ export const Dots = styled.span`
}
}
`
const SwapCallbackErrorInner = styled.div`
background-color: ${({ theme }) => transparentize(0.9, theme.red1)};
border-radius: 1rem;
display: flex;
align-items: center;
font-size: 0.825rem;
width: 100%;
padding: 3rem 1.25rem 1rem 1rem;
margin-top: -2rem;
color: ${({ theme }) => theme.red1};
z-index: -1;
p {
padding: 0;
margin: 0;
font-weight: 500;
}
`
const SwapCallbackErrorInnerAlertTriangle = styled.div`
background-color: ${({ theme }) => transparentize(0.9, theme.red1)};
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
border-radius: 12px;
min-width: 48px;
height: 48px;
`
export function SwapCallbackError({ error }: { error: string }) {
return (
<SwapCallbackErrorInner>
<SwapCallbackErrorInnerAlertTriangle>
<AlertTriangle size={24} />
</SwapCallbackErrorInnerAlertTriangle>
<p>{error}</p>
</SwapCallbackErrorInner>
)
}
export const SwapShowAcceptChanges = styled(AutoColumn)`
background-color: ${({ theme }) => transparentize(0.9, theme.primary1)};
color: ${({ theme }) => theme.primary1};
padding: 0.5rem;
border-radius: 12px;
margin-top: 8px;
`

View File

@@ -16,6 +16,7 @@ export class FortmaticConnector extends FortmaticConnectorCore {
async activate() {
if (!this.fortmatic) {
const { default: Fortmatic } = await import('fortmatic')
const { apiKey, chainId } = this as any
if (chainId in CHAIN_ID_NETWORK_ARGUMENT) {
this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains])

View File

@@ -22,19 +22,83 @@ class RequestError extends Error {
}
}
interface BatchItem {
request: { jsonrpc: '2.0'; id: number; method: string; params: unknown }
resolve: (result: any) => void
reject: (error: Error) => void
}
class MiniRpcProvider implements AsyncSendable {
public readonly isMetaMask: false = false
public readonly chainId: number
public readonly url: string
public readonly host: string
public readonly path: string
public readonly batchWaitTimeMs: number
constructor(chainId: number, url: string) {
private nextId = 1
private batchTimeoutId: ReturnType<typeof setTimeout> | null = null
private batch: BatchItem[] = []
constructor(chainId: number, url: string, batchWaitTimeMs?: number) {
this.chainId = chainId
this.url = url
const parsed = new URL(url)
this.host = parsed.host
this.path = parsed.pathname
// how long to wait to batch calls
this.batchWaitTimeMs = batchWaitTimeMs ?? 50
}
public readonly clearBatch = async () => {
console.debug('Clearing batch', this.batch)
const batch = this.batch
this.batch = []
this.batchTimeoutId = null
let response: Response
try {
response = await fetch(this.url, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify(batch.map(item => item.request))
})
} catch (error) {
batch.forEach(({ reject }) => reject(new Error('Failed to send batch call')))
return
}
if (!response.ok) {
batch.forEach(({ reject }) => reject(new RequestError(`${response.status}: ${response.statusText}`, -32000)))
return
}
let json
try {
json = await response.json()
} catch (error) {
batch.forEach(({ reject }) => reject(new Error('Failed to parse JSON response')))
return
}
const byKey = batch.reduce<{ [id: number]: BatchItem }>((memo, current) => {
memo[current.request.id] = current
return memo
}, {})
for (const result of json) {
const {
resolve,
reject,
request: { method }
} = byKey[result.id]
if (resolve && reject) {
if ('error' in result) {
reject(new RequestError(result?.error?.message, result?.error?.code, result?.error?.data))
} else if ('result' in result) {
resolve(result.result)
} else {
reject(new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, result))
}
}
}
}
public readonly sendAsync = (
@@ -53,24 +117,23 @@ class MiniRpcProvider implements AsyncSendable {
if (typeof method !== 'string') {
return this.request(method.method, method.params)
}
const response = await fetch(this.url, {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method,
params
if (method === 'eth_chainId') {
return `0x${this.chainId.toString(16)}`
}
const promise = new Promise((resolve, reject) => {
this.batch.push({
request: {
jsonrpc: '2.0',
id: this.nextId++,
method,
params
},
resolve,
reject
})
})
if (!response.ok) throw new RequestError(`${response.status}: ${response.statusText}`, -32000)
const body = await response.json()
if ('error' in body) {
throw new RequestError(body?.error?.message, body?.error?.code, body?.error?.data)
} else if ('result' in body) {
return body.result
} else {
throw new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, body)
}
this.batchTimeoutId = this.batchTimeoutId ?? setTimeout(this.clearBatch, this.batchWaitTimeMs)
return promise
}
}
@@ -89,6 +152,10 @@ export class NetworkConnector extends AbstractConnector {
}, {})
}
public get provider(): MiniRpcProvider {
return this.providers[this.currentChainId]
}
public async activate(): Promise<ConnectorUpdate> {
return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }
}

View File

@@ -1,3 +1,4 @@
import { Web3Provider } from '@ethersproject/providers'
import { InjectedConnector } from '@web3-react/injected-connector'
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
@@ -6,19 +7,25 @@ import { PortisConnector } from '@web3-react/portis-connector'
import { FortmaticConnector } from './Fortmatic'
import { NetworkConnector } from './NetworkConnector'
const POLLING_INTERVAL = 10000
const NETWORK_URL = process.env.REACT_APP_NETWORK_URL
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
export const NETWORK_CHAIN_ID: number = parseInt(process.env.REACT_APP_CHAIN_ID ?? '1')
if (typeof NETWORK_URL === 'undefined') {
throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`)
}
export const network = new NetworkConnector({
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL }
urls: { [NETWORK_CHAIN_ID]: NETWORK_URL }
})
let networkLibrary: Web3Provider | undefined
export function getNetworkLibrary(): Web3Provider {
return (networkLibrary = networkLibrary ?? new Web3Provider(network.provider as any))
}
export const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42]
})
@@ -28,7 +35,7 @@ export const walletconnect = new WalletConnectConnector({
rpc: { 1: NETWORK_URL },
bridge: 'https://bridge.walletconnect.org',
qrcode: true,
pollingInterval: POLLING_INTERVAL
pollingInterval: 15000
})
// mainnet only

View File

@@ -1,4 +0,0 @@
{
"extends": "../../tsconfig.strict.json",
"include": ["**/*"]
}

View File

@@ -0,0 +1,816 @@
[
{
"inputs": [
{
"internalType": "contract ENS",
"name": "_ens",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "uint256",
"name": "contentType",
"type": "uint256"
}
],
"name": "ABIChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "address",
"name": "a",
"type": "address"
}
],
"name": "AddrChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "coinType",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bytes",
"name": "newAddress",
"type": "bytes"
}
],
"name": "AddressChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "target",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "isAuthorised",
"type": "bool"
}
],
"name": "AuthorisationChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bytes",
"name": "hash",
"type": "bytes"
}
],
"name": "ContenthashChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"indexed": false,
"internalType": "uint16",
"name": "resource",
"type": "uint16"
},
{
"indexed": false,
"internalType": "bytes",
"name": "record",
"type": "bytes"
}
],
"name": "DNSRecordChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bytes",
"name": "name",
"type": "bytes"
},
{
"indexed": false,
"internalType": "uint16",
"name": "resource",
"type": "uint16"
}
],
"name": "DNSRecordDeleted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "DNSZoneCleared",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "bytes4",
"name": "interfaceID",
"type": "bytes4"
},
{
"indexed": false,
"internalType": "address",
"name": "implementer",
"type": "address"
}
],
"name": "InterfaceChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
}
],
"name": "NameChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "x",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "y",
"type": "bytes32"
}
],
"name": "PubkeyChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "string",
"name": "indexedKey",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "key",
"type": "string"
}
],
"name": "TextChanged",
"type": "event"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "contentTypes",
"type": "uint256"
}
],
"name": "ABI",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "addr",
"outputs": [
{
"internalType": "address payable",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "authorisations",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "clearDNSZone",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "contenthash",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "name",
"type": "bytes32"
},
{
"internalType": "uint16",
"name": "resource",
"type": "uint16"
}
],
"name": "dnsRecord",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "name",
"type": "bytes32"
}
],
"name": "hasDNSRecords",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes4",
"name": "interfaceID",
"type": "bytes4"
}
],
"name": "interfaceImplementer",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "pubkey",
"outputs": [
{
"internalType": "bytes32",
"name": "x",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "y",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "contentType",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "setABI",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "coinType",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "a",
"type": "bytes"
}
],
"name": "setAddr",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "address",
"name": "a",
"type": "address"
}
],
"name": "setAddr",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "address",
"name": "target",
"type": "address"
},
{
"internalType": "bool",
"name": "isAuthorised",
"type": "bool"
}
],
"name": "setAuthorisation",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "hash",
"type": "bytes"
}
],
"name": "setContenthash",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "setDNSRecords",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes4",
"name": "interfaceID",
"type": "bytes4"
},
{
"internalType": "address",
"name": "implementer",
"type": "address"
}
],
"name": "setInterface",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "string",
"name": "name",
"type": "string"
}
],
"name": "setName",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "x",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "y",
"type": "bytes32"
}
],
"name": "setPubkey",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "string",
"name": "key",
"type": "string"
},
{
"internalType": "string",
"name": "value",
"type": "string"
}
],
"name": "setText",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceID",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "pure",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "string",
"name": "key",
"type": "string"
}
],
"name": "text",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

View File

@@ -0,0 +1,422 @@
[
{
"inputs": [
{
"internalType": "contract ENS",
"name": "_old",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": true,
"internalType": "bytes32",
"name": "label",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "NewOwner",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "address",
"name": "resolver",
"type": "address"
}
],
"name": "NewResolver",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint64",
"name": "ttl",
"type": "uint64"
}
],
"name": "NewTTL",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "Transfer",
"type": "event"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "old",
"outputs": [
{
"internalType": "contract ENS",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "recordExists",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "resolver",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "resolver",
"type": "address"
},
{
"internalType": "uint64",
"name": "ttl",
"type": "uint64"
}
],
"name": "setRecord",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "address",
"name": "resolver",
"type": "address"
}
],
"name": "setResolver",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "label",
"type": "bytes32"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "setSubnodeOwner",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "label",
"type": "bytes32"
},
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "resolver",
"type": "address"
},
{
"internalType": "uint64",
"name": "ttl",
"type": "uint64"
}
],
"name": "setSubnodeRecord",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
},
{
"internalType": "uint64",
"name": "ttl",
"type": "uint64"
}
],
"name": "setTTL",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "ttl",
"outputs": [
{
"internalType": "uint64",
"name": "",
"type": "uint64"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

View File

@@ -1,7 +1,7 @@
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
import { AbstractConnector } from '@web3-react/abstract-connector'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
@@ -10,6 +10,13 @@ type ChainTokenList = {
readonly [chainId in ChainId]: Token[]
}
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 const AMPL = new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth')
const WETH_ONLY: ChainTokenList = {
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
@@ -24,6 +31,16 @@ export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR]
}
/**
* Some tokens can only be swapped via certain pairs, so we override the list of bases that are considered for these
* tokens.
*/
export const CUSTOM_BASES: { [chainId in ChainId]?: { [tokenAddress: string]: Token[] } } = {
[ChainId.MAINNET]: {
[AMPL.address]: [DAI, WETH[ChainId.MAINNET]]
}
}
// used for display in the default list when adding liquidity
export const SUGGESTED_BASES: ChainTokenList = {
...WETH_ONLY,
@@ -47,7 +64,19 @@ export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] }
]
}
const TESTNET_CAPABLE_WALLETS = {
export interface WalletInfo {
connector?: AbstractConnector
name: string
iconName: string
description: string
href: string | null
color: string
primary?: true
mobile?: true
mobileOnly?: true
}
export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
INJECTED: {
connector: injected,
name: 'Injected',
@@ -64,62 +93,53 @@ const TESTNET_CAPABLE_WALLETS = {
description: 'Easy-to-use browser extension.',
href: null,
color: '#E8831D'
},
WALLET_CONNECT: {
connector: walletconnect,
name: 'WalletConnect',
iconName: 'walletConnectIcon.svg',
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
href: null,
color: '#4196FC',
mobile: true
},
WALLET_LINK: {
connector: walletlink,
name: 'Coinbase Wallet',
iconName: 'coinbaseWalletIcon.svg',
description: 'Use Coinbase Wallet app on mobile device',
href: null,
color: '#315CF5'
},
COINBASE_LINK: {
name: 'Open in Coinbase Wallet',
iconName: 'coinbaseWalletIcon.svg',
description: 'Open in Coinbase Wallet app.',
href: 'https://go.cb-w.com/mtUDhEZPy1',
color: '#315CF5',
mobile: true,
mobileOnly: true
},
FORTMATIC: {
connector: fortmatic,
name: 'Fortmatic',
iconName: 'fortmaticIcon.png',
description: 'Login using Fortmatic hosted wallet',
href: null,
color: '#6748FF',
mobile: true
},
Portis: {
connector: portis,
name: 'Portis',
iconName: 'portisIcon.png',
description: 'Login using Portis hosted wallet',
href: null,
color: '#4A6C9B',
mobile: true
}
}
export const SUPPORTED_WALLETS =
process.env.REACT_APP_CHAIN_ID !== '1'
? TESTNET_CAPABLE_WALLETS
: {
...TESTNET_CAPABLE_WALLETS,
...{
WALLET_CONNECT: {
connector: walletconnect,
name: 'WalletConnect',
iconName: 'walletConnectIcon.svg',
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
href: null,
color: '#4196FC',
mobile: true
},
WALLET_LINK: {
connector: walletlink,
name: 'Coinbase Wallet',
iconName: 'coinbaseWalletIcon.svg',
description: 'Use Coinbase Wallet app on mobile device',
href: null,
color: '#315CF5'
},
COINBASE_LINK: {
name: 'Open in Coinbase Wallet',
iconName: 'coinbaseWalletIcon.svg',
description: 'Open in Coinbase Wallet app.',
href: 'https://go.cb-w.com/mtUDhEZPy1',
color: '#315CF5',
mobile: true,
mobileOnly: true
},
FORTMATIC: {
connector: fortmatic,
name: 'Fortmatic',
iconName: 'fortmaticIcon.png',
description: 'Login using Fortmatic hosted wallet',
href: null,
color: '#6748FF',
mobile: true
},
Portis: {
connector: portis,
name: 'Portis',
iconName: 'portisIcon.png',
description: 'Login using Portis hosted wallet',
href: null,
color: '#4A6C9B',
mobile: true
}
}
}
export const NetworkContextName = 'NETWORK'
// default allowed slippage, in bips

20
src/constants/lists.ts Normal file
View File

@@ -0,0 +1,20 @@
// the Uniswap Default token list lives here
export const DEFAULT_TOKEN_LIST_URL = 'tokens.uniswap.eth'
export const DEFAULT_LIST_OF_LISTS: string[] = [
DEFAULT_TOKEN_LIST_URL,
't2crtokens.eth', // kleros
'tokens.1inch.eth', // 1inch
'synths.snx.eth',
'tokenlist.dharma.eth',
'defi.cmc.eth',
'erc20.cmc.eth',
'stablecoin.cmc.eth',
'tokenlist.zerion.eth',
'tokenlist.aave.eth',
'https://www.coingecko.com/tokens_list/uniswap/defi_100/v_0_0_0.json',
'https://app.tryroll.com/tokens.json',
'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json',
'https://defiprime.com/defiprime.tokenlist.json',
'https://umaproject.org/uma.tokenlist.json'
]

View File

@@ -1,36 +0,0 @@
import { ChainId, Token, WETH } from '@uniswap/sdk'
import KOVAN_TOKENS from './kovan'
import MAINNET_TOKENS from './mainnet'
import RINKEBY_TOKENS from './rinkeby'
import ROPSTEN_TOKENS from './ropsten'
type AllTokens = Readonly<{ [chainId in ChainId]: Readonly<{ [tokenAddress: string]: Token }> }>
export const ALL_TOKENS: AllTokens = [
// WETH on all chains
...Object.values(WETH),
// chain-specific tokens
...MAINNET_TOKENS,
...RINKEBY_TOKENS,
...KOVAN_TOKENS,
...ROPSTEN_TOKENS
]
// put into an object
.reduce<AllTokens>(
(tokenMap, token) => {
if (tokenMap[token.chainId][token.address] !== undefined) throw Error('Duplicate tokens.')
return {
...tokenMap,
[token.chainId]: {
...tokenMap[token.chainId],
[token.address]: token
}
}
},
{
[ChainId.MAINNET]: {},
[ChainId.RINKEBY]: {},
[ChainId.GÖRLI]: {},
[ChainId.ROPSTEN]: {},
[ChainId.KOVAN]: {}
}
)

View File

@@ -1,6 +0,0 @@
import { Token, ChainId } from '@uniswap/sdk'
export default [
new Token(ChainId.KOVAN, '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.KOVAN, '0xAaF64BFCC32d0F15873a02163e7E500671a4ffcD', 18, 'MKR', 'Maker')
]

View File

@@ -1,134 +0,0 @@
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'),
new Token(ChainId.MAINNET, '0x737F98AC8cA59f2C68aD658E3C3d8C8963E40a4c', 18, 'AMN', 'Amon'),
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++'),
new Token(ChainId.MAINNET, '0x56d811088235F11C8920698a204A5010a788f4b3', 18, 'BZRX', 'bZx Protocol Token'),
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
COMP,
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
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'),
new Token(
ChainId.MAINNET,
'0xc719d010B63E5bbF2C0551872CD5316ED26AcD83',
18,
'DIP',
'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'),
new Token(ChainId.MAINNET, '0x419D0d8BdD9aF5e606Ae2232ed285Aff190E711b', 8, 'FUN', 'FunFair'),
new Token(ChainId.MAINNET, '0x4a57E687b9126435a9B19E4A802113e266AdeBde', 18, 'FXC', 'Flexacoin'),
new Token(ChainId.MAINNET, '0x543Ff227F64Aa17eA132Bf9886cAb5DB55DCAddf', 18, 'GEN', 'DAOstack'),
new Token(ChainId.MAINNET, '0x6810e776880C02933D47DB1b9fc05908e5386b96', 18, 'GNO', 'Gnosis Token'),
new Token(ChainId.MAINNET, '0x12B19D3e2ccc14Da04FAe33e63652ce469b3F2FD', 12, 'GRID', 'GRID Token'),
new Token(ChainId.MAINNET, '0x0000000000b3F879cb30FE243b4Dfee438691c04', 2, 'GST2', 'Gastoken.io'),
new Token(ChainId.MAINNET, '0xF1290473E210b2108A85237fbCd7b6eb42Cc654F', 18, 'HEDG', 'HedgeTrade'),
new Token(ChainId.MAINNET, '0x6c6EE5e31d828De241282B9606C8e98Ea48526E2', 18, 'HOT', 'HoloToken'),
new Token(ChainId.MAINNET, '0x493C57C4763932315A328269E1ADaD09653B9081', 18, 'iDAI', 'Fulcrum DAI iToken'),
new Token(ChainId.MAINNET, '0x14094949152EDDBFcd073717200DA82fEd8dC960', 18, 'iSAI', 'Fulcrum SAI iToken '),
new Token(ChainId.MAINNET, '0x6fB3e0A217407EFFf7Ca062D46c26E5d60a14d69', 18, 'IOTX', 'IoTeX Network'),
new Token(ChainId.MAINNET, '0x4Cd988AfBad37289BAAf53C13e98E2BD46aAEa8c', 18, 'KEY', 'KEY'),
new Token(ChainId.MAINNET, '0xdd974D5C2e2928deA5F71b9825b8b646686BD200', 18, 'KNC', 'Kyber Network Crystal'),
new Token(ChainId.MAINNET, '0x514910771AF9Ca656af840dff83E8264EcF986CA', 18, 'LINK', 'ChainLink Token'),
new Token(ChainId.MAINNET, '0xBBbbCA6A901c926F240b89EacB641d8Aec7AEafD', 18, 'LRC', 'LoopringCoin V2'),
new Token(ChainId.MAINNET, '0x80fB784B7eD66730e8b1DBd9820aFD29931aab03', 18, 'LEND', 'EthLend Token'),
new Token(ChainId.MAINNET, '0xA4e8C3Ec456107eA67d3075bF9e3DF3A75823DB0', 18, 'LOOM', 'LoomToken'),
new Token(ChainId.MAINNET, '0x58b6A8A3302369DAEc383334672404Ee733aB239', 18, 'LPT', 'Livepeer Token'),
new Token(ChainId.MAINNET, '0xD29F0b5b3F50b07Fe9a9511F7d86F4f4bAc3f8c4', 18, 'LQD', 'Liquidity.Network Token'),
new Token(ChainId.MAINNET, '0x0F5D2fB29fb7d3CFeE444a200298f468908cC942', 18, 'MANA', 'Decentraland MANA'),
new Token(ChainId.MAINNET, '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0', 18, 'MATIC', 'Matic Token'),
new Token(ChainId.MAINNET, '0x8888889213DD4dA823EbDD1e235b09590633C150', 18, 'MBC', 'Marblecoin'),
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'),
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'),
new Token(ChainId.MAINNET, '0x4575f41308EC1483f3d399aa9a2826d74Da13Deb', 18, 'OXT', 'Orchid'),
new Token(ChainId.MAINNET, '0xD56daC73A4d6766464b38ec6D91eB45Ce7457c44', 18, 'PAN', 'Panvala pan'),
new Token(ChainId.MAINNET, '0x8E870D67F660D95d5be530380D0eC0bd388289E1', 18, 'PAX', 'PAX'),
new Token(ChainId.MAINNET, '0x45804880De22913dAFE09f4980848ECE6EcbAf78', 18, 'PAXG', 'Paxos Gold'),
new Token(ChainId.MAINNET, '0x93ED3FBe21207Ec2E8f2d3c3de6e058Cb73Bc04d', 18, 'PNK', 'Pinakion'),
new Token(ChainId.MAINNET, '0x6758B7d441a9739b98552B373703d8d3d14f9e62', 18, 'POA20', 'POA ERC20 on Foundation'),
new Token(ChainId.MAINNET, '0x687BfC3E73f6af55F0CccA8450114D107E781a0e', 18, 'QCH', 'QChi'),
new Token(ChainId.MAINNET, '0x4a220E6096B25EADb88358cb44068A3248254675', 18, 'QNT', 'Quant'),
new Token(ChainId.MAINNET, '0x99ea4dB9EE77ACD40B119BD1dC4E33e1C070b80d', 18, 'QSP', 'Quantstamp Token'),
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network Token'),
new Token(ChainId.MAINNET, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
new Token(ChainId.MAINNET, '0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf', 8, 'renBCH', 'renBCH'),
new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC'),
new Token(ChainId.MAINNET, '0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2', 8, 'renZEC', 'renZEC'),
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, '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'),
new Token(ChainId.MAINNET, '0x4156D3342D5c385a87D264F90653733592000581', 8, 'SALT', 'Salt'),
new Token(ChainId.MAINNET, '0x7C5A0CE9267ED19B22F8cae653F198e3E8daf098', 18, 'SAN', 'SANtiment network token'),
new Token(ChainId.MAINNET, '0x5e74C9036fb86BD7eCdcb084a0673EFc32eA31cb', 18, 'sETH', 'Synth sETH'),
new Token(ChainId.MAINNET, '0x3A9FfF453d50D4Ac52A6890647b823379ba36B9E', 18, 'SHUF', 'Shuffle.Monster V3'),
new Token(ChainId.MAINNET, '0x744d70FDBE2Ba4CF95131626614a1763DF805B9E', 18, 'SNT', 'Status Network Token'),
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
new Token(ChainId.MAINNET, '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'),
new Token(ChainId.MAINNET, '0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9', 18, 'SXP', 'Swipe'),
new Token(ChainId.MAINNET, '0x00006100F7090010005F1bd7aE6122c3C2CF0090', 18, 'TAUD', 'TrueAUD'),
new Token(ChainId.MAINNET, '0x00000100F2A2bd000715001920eB70D229700085', 18, 'TCAD', 'TrueCAD'),
new Token(ChainId.MAINNET, '0x00000000441378008EA67F4284A57932B1c000a5', 18, 'TGBP', 'TrueGBP'),
new Token(ChainId.MAINNET, '0x0000852600CEB001E08e00bC008be620d60031F2', 18, 'THKD', 'TrueHKD'),
new Token(ChainId.MAINNET, '0xaAAf91D9b90dF800Df4F55c205fd6989c977E73a', 8, 'TKN', 'Monolith TKN'),
new Token(ChainId.MAINNET, '0x0Ba45A8b5d5575935B8158a88C631E9F9C95a2e5', 18, 'TRB', 'Tellor Tributes'),
new Token(ChainId.MAINNET, '0xCb94be6f13A1182E4A4B6140cb7bf2025d28e41B', 6, 'TRST', 'Trustcoin'),
new Token(ChainId.MAINNET, '0x2C537E5624e4af88A7ae4060C022609376C8D0EB', 6, 'TRYB', 'BiLira'),
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'),
USDC,
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
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'),
new Token(ChainId.MAINNET, '0xB4272071eCAdd69d933AdcD19cA99fe80664fc08', 18, 'XCHF', 'CryptoFranc'),
new Token(ChainId.MAINNET, '0x0f7F961648aE6Db43C75663aC7E5414Eb79b5704', 18, 'XIO', 'XIO Network'),
new Token(ChainId.MAINNET, '0xE41d2489571d322189246DaFA5ebDe1F4699F498', 18, 'ZRX', '0x Protocol Token')
]

View File

@@ -1,6 +0,0 @@
import { Token, ChainId } from '@uniswap/sdk'
export default [
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.RINKEBY, '0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85', 18, 'MKR', 'Maker')
]

View File

@@ -1,3 +0,0 @@
import { Token, ChainId } from '@uniswap/sdk'
export default [new Token(ChainId.ROPSTEN, '0xaD6D458402F60fD3Bd25163575031ACDce07538D', 18, 'DAI', 'Dai Stablecoin')]

View File

@@ -1,4 +0,0 @@
{
"extends": "../../tsconfig.strict.json",
"include": ["**/*"]
}

View File

@@ -123,14 +123,16 @@ export function useV1Trade(
pairs = [inputPair, outputPair]
}
const route = inputCurrency && pairs && pairs.length > 0 && new Route(pairs, inputCurrency)
const route = inputCurrency && pairs && pairs.length > 0 && new Route(pairs, inputCurrency, outputCurrency)
let v1Trade: Trade | undefined
try {
v1Trade =
route && exactAmount
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
: undefined
} catch {}
} catch (error) {
console.debug('Failed to create V1 trade', error)
}
return v1Trade
}
@@ -165,6 +167,8 @@ export function isTradeBetter(
tradeB: Trade | undefined,
minimumDelta: Percent = ZERO_PERCENT
): boolean | undefined {
if (tradeA && !tradeB) return false
if (tradeB && !tradeA) return true
if (!tradeA || !tradeB) return undefined
if (

View File

@@ -1,4 +0,0 @@
{
"extends": "../../tsconfig.strict.json",
"include": ["**/*"]
}

8
src/ethereum.d.ts vendored
View File

@@ -1,8 +0,0 @@
interface Window {
ethereum?: {
isMetaMask?: true
on?: (...args: any[]) => void
removeListener?: (...args: any[]) => void
}
web3?: {}
}

View File

@@ -1,7 +1,7 @@
import { parseBytes32String } from '@ethersproject/strings'
import { ChainId, Currency, ETHER, Token } from '@uniswap/sdk'
import { Currency, ETHER, Token, currencyEquals } from '@uniswap/sdk'
import { useMemo } from 'react'
import { ALL_TOKENS } from '../constants/tokens'
import { useSelectedTokenList } from '../state/lists/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useUserAddedTokens } from '../state/user/hooks'
import { isAddress } from '../utils'
@@ -12,6 +12,7 @@ import { useBytes32TokenContract, useTokenContract } from './useContract'
export function useAllTokens(): { [address: string]: Token } {
const { chainId } = useActiveWeb3React()
const userAddedTokens = useUserAddedTokens()
const allTokens = useSelectedTokenList()
return useMemo(() => {
if (!chainId) return {}
@@ -25,10 +26,16 @@ export function useAllTokens(): { [address: string]: Token } {
},
// must make a copy because reduce modifies the map, and we do not
// want to make a copy in every iteration
{ ...ALL_TOKENS[chainId as ChainId] }
{ ...allTokens[chainId] }
)
)
}, [userAddedTokens, chainId])
}, [chainId, userAddedTokens, allTokens])
}
// Check if currency is included in custom list from user storage
export function useIsUserAddedToken(currency: Currency): boolean {
const userAddedTokens = useUserAddedTokens()
return !!userAddedTokens.find(token => currencyEquals(currency, token))
}
// parse a name or symbol from a token response

View File

@@ -2,7 +2,7 @@ import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useMemo } from 'react'
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
import { BASES_TO_CHECK_TRADES_AGAINST, CUSTOM_BASES } from '../constants'
import { PairState, usePairs } from '../data/Reserves'
import { wrappedCurrency } from '../utils/wrappedCurrency'
@@ -17,18 +17,46 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
? [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
: [undefined, 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 basePairs: [Token, Token][] = useMemo(
() =>
flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase])).filter(
([t0, t1]) => t0.address !== t1.address
),
[bases]
)
const allPairCombinations: [Token, Token][] = useMemo(
() =>
tokenA && tokenB
? [
// the direct pair
[tokenA, tokenB],
// token A against all bases
...bases.map((base): [Token, Token] => [tokenA, base]),
// token B against all bases
...bases.map((base): [Token, Token] => [tokenB, base]),
// each base against all bases
...basePairs
]
.filter((tokens): tokens is [Token, Token] => Boolean(tokens[0] && tokens[1]))
.filter(([t0, t1]) => t0.address !== t1.address)
.filter(([tokenA, tokenB]) => {
if (!chainId) return true
const customBases = CUSTOM_BASES[chainId]
if (!customBases) return true
const customBasesA: Token[] | undefined = customBases[tokenA.address]
const customBasesB: Token[] | undefined = customBases[tokenB.address]
if (!customBasesA && !customBasesB) return true
if (customBasesA && !customBasesA.find(base => tokenB.equals(base))) return false
if (customBasesB && !customBasesB.find(base => tokenA.equals(base))) return false
return true
})
: [],
[tokenA, tokenB, bases, basePairs, chainId]
)
const allPairs = usePairs(allPairCombinations)
@@ -55,7 +83,6 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
*/
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
return useMemo(() => {
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
return (

View File

@@ -1,8 +0,0 @@
interface Window {
ethereum?: {
isMetaMask?: true
on?: (...args: any[]) => void
removeListener?: (...args: any[]) => void
}
web3?: {}
}

View File

@@ -72,25 +72,16 @@ export function useInactiveListener(suppress = false) {
}
}
const handleNetworkChanged = () => {
// eat errors
activate(injected, undefined, true).catch(error => {
console.error('Failed to activate after networks changed', error)
})
}
ethereum.on('chainChanged', handleChainChanged)
ethereum.on('networkChanged', handleNetworkChanged)
ethereum.on('accountsChanged', handleAccountsChanged)
return () => {
if (ethereum.removeListener) {
ethereum.removeListener('chainChanged', handleChainChanged)
ethereum.removeListener('networkChanged', handleNetworkChanged)
ethereum.removeListener('accountsChanged', handleAccountsChanged)
}
}
}
return
return undefined
}, [active, error, suppress, activate])
}

View File

@@ -1,4 +0,0 @@
{
"extends": "../../tsconfig.strict.json",
"include": ["**/*"]
}

View File

@@ -2,18 +2,20 @@ import { Contract } from '@ethersproject/contracts'
import { ChainId, WETH } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useMemo } from 'react'
import ENS_ABI from '../constants/abis/ens-registrar.json'
import ENS_PUBLIC_RESOLVER_ABI from '../constants/abis/ens-public-resolver.json'
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
import ERC20_ABI from '../constants/abis/erc20.json'
import WETH_ABI from '../constants/abis/weth.json'
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
import WETH_ABI from '../constants/abis/weth.json'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
import { getContract } from '../utils'
import { useActiveWeb3React } from './index'
// returns null on errors
function useContract(address?: string, ABI?: any, withSignerIfPossible = true): Contract | null {
function useContract(address: string | undefined, ABI: any, withSignerIfPossible = true): Contract | null {
const { library, account } = useActiveWeb3React()
return useMemo(() => {
@@ -49,6 +51,26 @@ export function useWETHContract(withSignerIfPossible?: boolean): Contract | null
return useContract(chainId ? WETH[chainId].address : undefined, WETH_ABI, withSignerIfPossible)
}
export function useENSRegistrarContract(withSignerIfPossible?: boolean): Contract | null {
const { chainId } = useActiveWeb3React()
let address: string | undefined
if (chainId) {
switch (chainId) {
case ChainId.MAINNET:
case ChainId.GÖRLI:
case ChainId.ROPSTEN:
case ChainId.RINKEBY:
address = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'
break
}
}
return useContract(address, ENS_ABI, withSignerIfPossible)
}
export function useENSResolverContract(address: string | undefined, withSignerIfPossible?: boolean): Contract | null {
return useContract(address, ENS_PUBLIC_RESOLVER_ABI, withSignerIfPossible)
}
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
}

Some files were not shown because too many files have changed in this diff Show More