Compare commits

...

27 Commits

Author SHA1 Message Date
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
Moody Salem
19b1e9e399 feat(weth): support WETH across the site and use sdk 3.0 (#947)
* first pass of sdk 3.0

* second pass using weth

* kill unused pool popup

* get it compiling again

* first pass of sdk 3.0

* switch to currencies

* get it compiling after the big move merge

* restore margin

* clean up add liquidity more

* fix a bunch of bugs

* todo trade on v1

* show eth in currency list

* allow selecting eth in the swap page

* fix unit tests for swap page

* test lint errors

* fix failing integration tests

* fix another couple of failing unit tests

* handle selecting currency b when no currency a

* improve the import pool page

* clean up add liquidity for invalid pairs

* bold

* first pass at swap arguments for v1, some unit tests

* fix some bugs in add liquidity, burn hook

* fix last of ts errors in remove liquidity

* support wrapping/unwrapping weth

* kill a bunch of code including the dummy pairs

* required pair prop in the position card

* tests for the v1 swap arguments

* do not say estimated on the wrap ui

* show ETH instead of WETH in the pool summaries

* small size socks

* fix lint error

* in burn, use currencies from the URL

* fix some integration tests

* both contain weth

* receive eth/weth link

* fix empty row

* show wrapped only if one currency is weth

* currency selects in the remove liquidity page
2020-07-20 06:48:42 -05:00
Moody Salem
6287b95b92 fix(#961): change send copy 2020-07-17 09:05:20 -05:00
Ian Lapham
4e8a6e2a4c change copy on confirm view (#969) 2020-07-16 11:59:37 -04:00
143 changed files with 6785 additions and 4934 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

@@ -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

@@ -32,13 +32,19 @@ describe('Add Liquidity', () => {
)
})
it('redirects /add/WETH-token to /add/ETH/token', () => {
it('redirects /add/WETH-token to /add/WETH-address/token', () => {
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should('contain', '/add/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should(
'contain',
'/add/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
)
})
it('redirects /add/token-WETH to /add/token/ETH', () => {
it('redirects /add/token-WETH to /add/token/WETH-address', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.url().should('contain', '/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
cy.url().should(
'contain',
'/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/0xc778417E063141139Fce010982780140Aa0cD5Ab'
)
})
})

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,14 +1,34 @@
describe('Remove Liquidity', () => {
it('redirects', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.url().should(
'contain',
'/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'
)
})
it('eth remove', () => {
cy.visit('/remove/ETH/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
it('eth remove swap order', () => {
cy.visit('/remove/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'MKR')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
})
it('loads the two correct tokens', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
it('does not crash if ETH is duplicated', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
})
it('token not in storage is loaded', () => {

View File

@@ -0,0 +1,19 @@
describe('Warning', () => {
beforeEach(() => {
cy.clearLocalStorage()
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').click()
cy.get('.token-warning-container').should('not.be.visible')
})
it('Check supression persists across sessions.', () => {
cy.get('.token-warning-container').should('be.visible')
cy.get('.token-dismiss-button').click()
cy.reload()
cy.get('.token-warning-container').should('not.be.visible')
})
})

View File

@@ -73,9 +73,9 @@ Cypress.Commands.overwrite('visit', (original, url, options) => {
...options,
onBeforeLoad(win) {
options && options.onBeforeLoad && options.onBeforeLoad(win)
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
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,15 +4,7 @@
"homepage": ".",
"private": true,
"devDependencies": {
"@ethersproject/address": "^5.0.0-beta.134",
"@ethersproject/bignumber": "^5.0.0-beta.138",
"@ethersproject/constants": "^5.0.0-beta.133",
"@ethersproject/contracts": "^5.0.0-beta.151",
"@ethersproject/experimental": "^5.0.0-beta.141",
"@ethersproject/providers": "5.0.0-beta.162",
"@ethersproject/strings": "^5.0.0-beta.136",
"@ethersproject/units": "^5.0.0-beta.132",
"@ethersproject/wallet": "^5.0.0-beta.141",
"@ethersproject/experimental": "^5.0.1",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
@@ -27,11 +19,12 @@
"@types/react-router-dom": "^5.0.0",
"@types/react-window": "^1.8.2",
"@types/rebass": "^4.0.5",
"@types/styled-components": "^4.2.0",
"@types/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": "^2.0.5",
"@uniswap/sdk": "3.0.3-beta.1",
"@uniswap/token-lists": "^1.0.0-beta.11",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@web3-react/core": "^6.0.9",
@@ -40,14 +33,16 @@
"@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",
"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",
@@ -56,7 +51,6 @@
"lodash.flatmap": "^4.5.0",
"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",
@@ -76,8 +70,10 @@
"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

@@ -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()
@@ -382,7 +382,6 @@ export default function AccountDetails({
</AccountControl>
</>
)}
{/* {formatConnectorName()} */}
</AccountGroupingRow>
</InfoCard>
</YourAccount>

View File

@@ -27,6 +27,8 @@ const Base = styled(RebassButton)<{
flex-wrap: nowrap;
align-items: center;
cursor: pointer;
position: relative;
z-index: 1;
&:disabled {
cursor: auto;
}

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

@@ -1,12 +1,11 @@
import { Pair, Token } from '@uniswap/sdk'
import { Currency, Pair } from '@uniswap/sdk'
import React, { useState, useContext, useCallback } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { darken } from 'polished'
import { Field } from '../../state/swap/actions'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import TokenSearchModal from '../SearchModal/TokenSearchModal'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import { useCurrencyBalance } from '../../state/wallet/hooks'
import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { RowBetween } from '../Row'
import { TYPE, CursorPointer } from '../../theme'
import { Input as NumericalInput } from '../NumericalInput'
@@ -117,40 +116,36 @@ const StyledBalanceMax = styled.button`
interface CurrencyInputPanelProps {
value: string
field: string
onUserInput: (field: string, val: string) => void
onUserInput: (value: string) => void
onMax?: () => void
showMaxButton: boolean
label?: string
onTokenSelection?: (tokenAddress: string) => void
token?: Token | null
disableTokenSelect?: boolean
onCurrencySelect?: (currency: Currency) => void
currency?: Currency | null
disableCurrencySelect?: boolean
hideBalance?: boolean
isExchange?: boolean
pair?: Pair | null
hideInput?: boolean
showSendWithSwap?: boolean
otherSelectedTokenAddress?: string | null
otherCurrency?: Currency | null
id: string
showCommonBases?: boolean
}
export default function CurrencyInputPanel({
value,
field,
onUserInput,
onMax,
showMaxButton,
label = 'Input',
onTokenSelection = null,
token = null,
disableTokenSelect = false,
onCurrencySelect = null,
currency = null,
disableCurrencySelect = false,
hideBalance = false,
isExchange = false,
pair = null, // used for double token logo
hideInput = false,
showSendWithSwap = false,
otherSelectedTokenAddress = null,
otherCurrency = null,
id,
showCommonBases
}: CurrencyInputPanelProps) {
@@ -158,7 +153,7 @@ export default function CurrencyInputPanel({
const [modalOpen, setModalOpen] = useState(false)
const { account } = useActiveWeb3React()
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
const selectedCurrencyBalance = useCurrencyBalance(account, currency)
const theme = useContext(ThemeContext)
const handleDismissSearch = useCallback(() => {
@@ -183,8 +178,8 @@ export default function CurrencyInputPanel({
fontSize={14}
style={{ display: 'inline' }}
>
{!hideBalance && !!token && userTokenBalance
? 'Balance: ' + userTokenBalance?.toSignificant(6)
{!hideBalance && !!currency && selectedCurrencyBalance
? 'Balance: ' + selectedCurrencyBalance?.toSignificant(6)
: ' -'}
</TYPE.body>
</CursorPointer>
@@ -192,63 +187,62 @@ export default function CurrencyInputPanel({
</RowBetween>
</LabelRow>
)}
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableTokenSelect}>
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableCurrencySelect}>
{!hideInput && (
<>
<NumericalInput
className="token-amount-input"
value={value}
onUserInput={val => {
onUserInput(field, val)
onUserInput(val)
}}
/>
{account && !!token?.address && showMaxButton && label !== 'To' && (
{account && currency && showMaxButton && label !== 'To' && (
<StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>
)}
</>
)}
<CurrencySelect
selected={!!token}
selected={!!currency}
className="open-currency-select-button"
onClick={() => {
if (!disableTokenSelect) {
if (!disableCurrencySelect) {
setModalOpen(true)
}
}}
>
<Aligner>
{isExchange ? (
<DoubleLogo a0={pair?.token0.address} a1={pair?.token1.address} size={24} margin={true} />
) : token?.address ? (
<TokenLogo address={token?.address} size={'24px'} />
{pair ? (
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
) : currency ? (
<CurrencyLogo currency={currency} size={'24px'} />
) : null}
{isExchange ? (
{pair ? (
<StyledTokenName className="pair-name-container">
{pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName>
) : (
<StyledTokenName className="token-symbol-container" active={Boolean(token && token.symbol)}>
{(token && token.symbol && token.symbol.length > 20
? token.symbol.slice(0, 4) +
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
{(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) +
'...' +
token.symbol.slice(token.symbol.length - 5, token.symbol.length)
: token?.symbol) || t('selectToken')}
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: currency?.symbol) || t('selectToken')}
</StyledTokenName>
)}
{!disableTokenSelect && <StyledDropDown selected={!!token?.address} />}
{!disableCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner>
</CurrencySelect>
</InputRow>
</Container>
{!disableTokenSelect && (
<TokenSearchModal
{!disableCurrencySelect && (
<CurrencySearchModal
isOpen={modalOpen}
onDismiss={handleDismissSearch}
onTokenSelect={onTokenSelection}
onCurrencySelect={onCurrencySelect}
showSendWithSwap={showSendWithSwap}
hiddenToken={token?.address}
otherSelectedTokenAddress={otherSelectedTokenAddress}
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
hiddenCurrency={currency}
otherSelectedCurrency={otherCurrency}
showCommonBases={showCommonBases}
/>
)}

View File

@@ -0,0 +1,94 @@
import { Currency, ETHER, Token } from '@uniswap/sdk'
import React, { useState } from 'react'
import styled from 'styled-components'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
import { WrappedTokenInfo } from '../../state/lists/hooks'
import uriToHttp from '../../utils/uriToHttp'
const getTokenLogoURL = address =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
const BAD_URIS: { [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};
height: ${({ size }) => size};
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
border-radius: 24px;
`
export default function CurrencyLogo({
currency,
size = '24px',
...rest
}: {
currency?: Currency
size?: string
style?: React.CSSProperties
}) {
const [, refresh] = useState<number>(0)
if (currency === ETHER) {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
}
if (currency instanceof Token) {
let uri: string | undefined
if (currency instanceof WrappedTokenInfo) {
if (currency.logoURI && !BAD_URIS[currency.logoURI]) {
uri = uriToHttp(currency.logoURI).filter(s => !BAD_URIS[s])[0]
}
}
if (!uri) {
const defaultUri = getTokenLogoURL(currency.address)
if (!BAD_URIS[defaultUri]) {
uri = defaultUri
}
}
if (uri) {
return (
<Image
{...rest}
alt={`${currency.name} Logo`}
src={uri}
size={size}
onError={() => {
if (currency instanceof Token) {
BAD_URIS[uri] = true
}
refresh(i => i + 1)
}}
/>
)
}
}
return (
<Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking">
🤔
</span>
</Emoji>
)
}

View File

@@ -1,34 +1,40 @@
import { Currency } from '@uniswap/sdk'
import React from 'react'
import styled from 'styled-components'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>`
const Wrapper = styled.div<{ margin: boolean; sizeraw: number }>`
position: relative;
display: flex;
flex-direction: row;
margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'};
`
interface DoubleTokenLogoProps {
interface DoubleCurrencyLogoProps {
margin?: boolean
size?: number
a0?: string
a1?: string
currency0?: Currency
currency1?: Currency
}
const HigherLogo = styled(TokenLogo)`
const HigherLogo = styled(CurrencyLogo)`
z-index: 2;
`
const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>`
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
position: absolute;
left: ${({ sizeraw }) => (sizeraw / 2).toString() + 'px'};
`
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) {
export default function DoubleCurrencyLogo({
currency0,
currency1,
size = 16,
margin = false
}: DoubleCurrencyLogoProps) {
return (
<TokenWrapper sizeraw={size} margin={margin}>
{a0 && <HigherLogo address={a0} size={size.toString() + 'px'} />}
{a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
</TokenWrapper>
<Wrapper sizeraw={size} margin={margin}>
{currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
{currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
</Wrapper>
)
}

View File

@@ -4,6 +4,7 @@ import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import { MouseoverTooltip } from '../Tooltip'
const VersionLabel = styled.span<{ enabled: boolean }>`
padding: 0.35rem 0.6rem;
@@ -61,10 +62,15 @@ export default function VersionSwitch() {
[versionSwitchAvailable]
)
return (
const toggle = (
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
</VersionToggle>
)
return versionSwitchAvailable ? (
toggle
) : (
<MouseoverTooltip text="This page is only compatible with Uniswap V2.">{toggle}</MouseoverTooltip>
)
}

View File

@@ -1,4 +1,4 @@
import { ChainId, WETH } from '@uniswap/sdk'
import { ChainId } from '@uniswap/sdk'
import React from 'react'
import { isMobile } from 'react-device-detect'
import { Text } from 'rebass'
@@ -11,7 +11,7 @@ import Wordmark from '../../assets/svg/wordmark.svg'
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
import { useActiveWeb3React } from '../../hooks'
import { useDarkModeManager } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { useETHBalances } from '../../state/wallet/hooks'
import { YellowCard } from '../Card'
import Settings from '../Settings'
@@ -137,7 +137,7 @@ const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
export default function Header() {
const { account, chainId } = useActiveWeb3React()
const userEthBalance = useTokenBalanceTreatingWETHasETH(account, WETH[chainId])
const userEthBalance = useETHBalances([account])[account]
const [isDark] = useDarkModeManager()
return (

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'
})`
@@ -55,6 +39,8 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
padding: 0px;
width: 50vw;
align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')};
max-width: 420px;
${({ maxHeight }) =>
maxHeight &&
@@ -102,7 +88,7 @@ export default function Modal({
initialFocusRef = null,
children
}: ModalProps) {
const transitions = useTransition(isOpen, null, {
const fadeTransition = useTransition(isOpen, null, {
config: { duration: 200 },
from: { opacity: 0 },
enter: { opacity: 1 },
@@ -115,80 +101,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

@@ -66,20 +66,6 @@ export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
)
}
export function CreatePoolTabs() {
return (
<Tabs>
<RowBetween style={{ padding: '1rem' }}>
<HistoryLink to="/pool">
<StyledArrowLeft />
</HistoryLink>
<ActiveText>Create Pool</ActiveText>
<QuestionHelper text={'Use this interface to create a new pool.'} />
</RowBetween>
</Tabs>
)
}
export function FindPoolTabs() {
return (
<Tabs>

View File

@@ -0,0 +1,72 @@
import { TokenList, Version } from '@uniswap/token-lists'
import React, { useCallback, useContext } from 'react'
import { AlertCircle, Info } from 'react-feather'
import { useDispatch } from 'react-redux'
import { ThemeContext } from 'styled-components'
import { AppDispatch } from '../../state'
import { useRemovePopup } from '../../state/application/hooks'
import { acceptListUpdate } from '../../state/lists/actions'
import { TYPE } from '../../theme'
import { ButtonPrimary, ButtonSecondary } from '../Button'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
function versionLabel(version: Version): string {
return `v${version.major}.${version.minor}.${version.patch}`
}
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 theme = useContext(ThemeContext)
const updateList = useCallback(() => {
if (auto) return
dispatch(acceptListUpdate(listUrl))
removeThisPopup()
}, [auto, dispatch, listUrl, removeThisPopup])
return (
<AutoRow>
<div style={{ paddingRight: 16 }}>
{auto ? <Info color={theme.text2} size={24} /> : <AlertCircle color={theme.red1} size={24} />}{' '}
</div>
<AutoColumn style={{ flex: '1' }} gap="8px">
{auto ? (
<TYPE.body fontWeight={500}>
The token list &quot;{oldList.name}&quot; has been updated to{' '}
<strong>{versionLabel(newList.version)}</strong>.
</TYPE.body>
) : (
<>
<div>
A token list update is available for the list &quot;{oldList.name}&quot; ({versionLabel(oldList.version)}{' '}
to {versionLabel(newList.version)}).
</div>
<AutoRow>
<div style={{ flexGrow: 1, marginRight: 6 }}>
<ButtonPrimary onClick={updateList}>Update list</ButtonPrimary>
</div>
<div style={{ flexGrow: 1 }}>
<ButtonSecondary onClick={removeThisPopup}>Dismiss</ButtonSecondary>
</div>
</AutoRow>
</>
)}
</AutoColumn>
</AutoRow>
)
}

View File

@@ -0,0 +1,86 @@
import React, { useCallback, useContext, useState } from 'react'
import { X } from 'react-feather'
import styled, { ThemeContext } from 'styled-components'
import useInterval from '../../hooks/useInterval'
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;
z-index: 2;
overflow: hidden;
${({ theme }) => theme.mediaWidth.upToSmall`
min-width: 290px;
`}
`
const DELAY = 100
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;
`
export default function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
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 theme = useContext(ThemeContext)
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
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} />
}
return (
<Popup onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<StyledClose color={theme.text2} onClick={() => removePopup(popKey)} />
{popupContent}
<Fader count={count} />
</Popup>
)
}

View File

@@ -0,0 +1,39 @@
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>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn>
</RowNoFlex>
)
}

View File

@@ -1,27 +1,8 @@
import { ChainId, Pair, Token } from '@uniswap/sdk'
import React, { useContext, useMemo } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { useMediaLayout } from 'use-media'
import { X } from 'react-feather'
import { PopupContent } from '../../state/application/actions'
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
import { ExternalLink } from '../../theme'
import React from 'react'
import styled from 'styled-components'
import { useActivePopups } from '../../state/application/hooks'
import { AutoColumn } from '../Column'
import DoubleTokenLogo from '../DoubleLogo'
import Row from '../Row'
import TxnPopup from '../TxnPopup'
import { Text } from 'rebass'
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;
@@ -29,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`
@@ -44,8 +30,8 @@ const MobilePopupInner = styled.div`
`
const FixedPopupColumn = styled(AutoColumn)`
position: absolute;
top: 112px;
position: fixed;
top: 64px;
right: 1rem;
max-width: 355px !important;
width: 100%;
@@ -55,112 +41,27 @@ const FixedPopupColumn = styled(AutoColumn)`
`};
`
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 PoolPopup({
token0,
token1
}: {
token0: { address?: string; symbol?: string }
token1: { address?: string; symbol?: string }
}) {
const pairAddress: string | null = useMemo(() => {
if (!token0 || !token1) return null
// just mock it out
return Pair.getAddress(
new Token(ChainId.MAINNET, token0.address, 18),
new Token(ChainId.MAINNET, token1.address, 18)
)
}, [token0, token1])
return (
<AutoColumn gap={'10px'}>
<Text fontSize={20} fontWeight={500}>
Pool Imported
</Text>
<Row>
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
<Text fontSize={16} fontWeight={500}>
UNI {token0?.symbol} / {token1?.symbol}
</Text>
</Row>
{pairAddress ? (
<ExternalLink href={`https://uniswap.info/pair/${pairAddress}`}>View on Uniswap Info.</ExternalLink>
) : null}
</AutoColumn>
)
}
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
if ('txn' in content) {
const {
txn: { hash, success, summary }
} = content
return <TxnPopup popKey={popKey} hash={hash} success={success} summary={summary} />
} else if ('poolAdded' in content) {
const {
poolAdded: { token0, token1 }
} = content
return <PoolPopup token0={token0} token1={token1} />
}
}
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} />
))}
</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} />
))}
</MobilePopupInner>
</MobilePopupWrapper>
)
</>
)
}

View File

@@ -7,7 +7,7 @@ import { AutoColumn } from '../Column'
import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed } from '../Row'
import { FixedHeightRow, HoverCard } from './index'
import DoubleTokenLogo from '../DoubleLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { useActiveWeb3React } from '../../hooks'
import { ThemeContext } from 'styled-components'
@@ -26,7 +26,7 @@ function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
<AutoColumn gap="12px">
<FixedHeightRow>
<RowFixed>
<DoubleTokenLogo a0={token.address} margin={true} size={20} />
<DoubleCurrencyLogo currency0={token} margin={true} size={20} />
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
</Text>

View File

@@ -1,23 +1,24 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { JSBI, Pair, Percent } from '@uniswap/sdk'
import { darken } from 'polished'
import React, { useState } from 'react'
import { ChevronDown, ChevronUp } from 'react-feather'
import { Link } from 'react-router-dom'
import { Percent, Pair, JSBI } from '@uniswap/sdk'
import { Text } from 'rebass'
import styled from 'styled-components'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { useTotalSupply } from '../../data/TotalSupply'
import { currencyId } from '../../pages/AddLiquidity/currencyId'
import { useTokenBalance } from '../../state/wallet/hooks'
import { ExternalLink } from '../../theme'
import { currencyId } from '../../utils/currencyId'
import { unwrappedToken } from '../../utils/wrappedCurrency'
import { ButtonSecondary } from '../Button'
import Card, { GreyCard } from '../Card'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import { Text } from 'rebass'
import { ExternalLink } from '../../theme'
import { AutoColumn } from '../Column'
import { ChevronDown, ChevronUp } from 'react-feather'
import { ButtonSecondary } from '../Button'
import { RowBetween, RowFixed, AutoRow } from '../Row'
import CurrencyLogo from '../CurrencyLogo'
import DoubleCurrencyLogo from '../DoubleLogo'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import { Dots } from '../swap/styleds'
export const FixedHeightRow = styled(RowBetween)`
@@ -32,20 +33,21 @@ export const HoverCard = styled(Card)`
`
interface PositionCardProps {
pair: Pair | undefined | null
pair: Pair
showUnwrapped?: boolean
border?: string
}
export function MinimalPositionCard({ pair, border }: PositionCardProps) {
export function MinimalPositionCard({ pair, showUnwrapped = false, border }: PositionCardProps) {
const { account } = useActiveWeb3React()
const token0 = pair?.token0
const token1 = pair?.token1
const currency0 = showUnwrapped ? pair.token0 : unwrappedToken(pair.token0)
const currency1 = showUnwrapped ? pair.token1 : unwrappedToken(pair.token1)
const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken)
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
const [token0Deposited, token1Deposited] =
!!pair &&
@@ -54,8 +56,8 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
? [
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false)
]
: [undefined, undefined]
@@ -73,9 +75,9 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
</FixedHeightRow>
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}>
{token0?.symbol}/{token1?.symbol}
{currency0.symbol}/{currency1.symbol}
</Text>
</RowFixed>
<RowFixed>
@@ -87,7 +89,7 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
<AutoColumn gap="4px">
<FixedHeightRow>
<Text color="#888D9B" fontSize={16} fontWeight={500}>
{token0?.symbol}:
{currency0.symbol}:
</Text>
{token0Deposited ? (
<RowFixed>
@@ -101,7 +103,7 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
</FixedHeightRow>
<FixedHeightRow>
<Text color="#888D9B" fontSize={16} fontWeight={500}>
{token1?.symbol}:
{currency1.symbol}:
</Text>
{token1Deposited ? (
<RowFixed>
@@ -124,13 +126,13 @@ export function MinimalPositionCard({ pair, border }: PositionCardProps) {
export default function FullPositionCard({ pair, border }: PositionCardProps) {
const { account } = useActiveWeb3React()
const token0 = pair?.token0
const token1 = pair?.token1
const currency0 = unwrappedToken(pair.token0)
const currency1 = unwrappedToken(pair.token1)
const [showMore, setShowMore] = useState(false)
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken)
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
const poolTokenPercentage =
!!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
@@ -144,8 +146,8 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
? [
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false),
pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false)
]
: [undefined, undefined]
@@ -154,9 +156,9 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<AutoColumn gap="12px">
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
<RowFixed>
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
<DoubleCurrencyLogo currency0={currency0} currency1={currency1} margin={true} size={20} />
<Text fontWeight={500} fontSize={20}>
{!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`}
{!currency0 || !currency1 ? <Dots>Loading</Dots> : `${currency0.symbol}/${currency1.symbol}`}
</Text>
</RowFixed>
<RowFixed>
@@ -172,7 +174,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {token0?.symbol}:
Pooled {currency0.symbol}:
</Text>
</RowFixed>
{token0Deposited ? (
@@ -180,7 +182,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token0Deposited?.toSignificant(6)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={currency0} />
</RowFixed>
) : (
'-'
@@ -190,7 +192,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<FixedHeightRow>
<RowFixed>
<Text fontSize={16} fontWeight={500}>
Pooled {token1?.symbol}:
Pooled {currency1.symbol}:
</Text>
</RowFixed>
{token1Deposited ? (
@@ -198,7 +200,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{token1Deposited?.toSignificant(6)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={currency1} />
</RowFixed>
) : (
'-'
@@ -222,15 +224,15 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
</FixedHeightRow>
<AutoRow justify="center" marginTop={'10px'}>
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
<ExternalLink href={`https://uniswap.info/pair/${pair.liquidityToken.address}`}>
View pool information
</ExternalLink>
</AutoRow>
<RowBetween marginTop="10px">
<ButtonSecondary as={Link} to={`/add/${currencyId(token0)}/${currencyId(token1)}`} width="48%">
<ButtonSecondary as={Link} to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`} width="48%">
Add
</ButtonSecondary>
<ButtonSecondary as={Link} width="48%" to={`/remove/${token0?.address}-${token1?.address}`}>
<ButtonSecondary as={Link} width="48%" to={`/remove/${currencyId(currency0)}/${currencyId(currency1)}`}>
Remove
</ButtonSecondary>
</RowBetween>

View File

@@ -5,14 +5,13 @@ const Row = styled(Box)<{ align?: string; padding?: string; border?: string; bor
width: 100%;
display: flex;
padding: 0;
align-items: center;
align-items: ${({ align }) => align && align};
align-items: ${({ align }) => (align ? align : 'center')};
padding: ${({ padding }) => padding};
border: ${({ border }) => border};
border-radius: ${({ borderRadius }) => borderRadius};
`
export const RowBetween = styled(Row)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>`
export const RowBetween = styled(Row)`
justify-content: space-between;
`

View File

@@ -1,13 +1,13 @@
import React from 'react'
import { Text } from 'rebass'
import { ChainId, Token } from '@uniswap/sdk'
import { ChainId, Currency, currencyEquals, ETHER, Token } from '@uniswap/sdk'
import styled from 'styled-components'
import { SUGGESTED_BASES } from '../../constants'
import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper'
import { AutoRow } from '../Row'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
const BaseWrapper = styled.div<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
@@ -28,11 +28,11 @@ const BaseWrapper = styled.div<{ disable?: boolean }>`
export default function CommonBases({
chainId,
onSelect,
selectedTokenAddress
selectedCurrency
}: {
chainId: ChainId
selectedTokenAddress: string
onSelect: (tokenAddress: string) => void
chainId?: ChainId
selectedCurrency?: Currency
onSelect: (currency: Currency) => void
}) {
return (
<AutoColumn gap="md">
@@ -43,14 +43,20 @@ export default function CommonBases({
<QuestionHelper text="These tokens are commonly paired with other tokens." />
</AutoRow>
<AutoRow gap="4px">
{(SUGGESTED_BASES[chainId as ChainId] ?? []).map((token: Token) => {
<BaseWrapper
onClick={() => !currencyEquals(selectedCurrency, ETHER) && onSelect(ETHER)}
disable={selectedCurrency === ETHER}
>
<CurrencyLogo currency={ETHER} style={{ marginRight: 8 }} />
<Text fontWeight={500} fontSize={16}>
ETH
</Text>
</BaseWrapper>
{(chainId ? SUGGESTED_BASES[chainId] : []).map((token: Token) => {
const selected = selectedCurrency instanceof Token && selectedCurrency.address === token.address
return (
<BaseWrapper
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
disable={selectedTokenAddress === token.address}
key={token.address}
>
<TokenLogo address={token.address} style={{ marginRight: 8 }} />
<BaseWrapper onClick={() => !selected && onSelect(token)} disable={selected} key={token.address}>
<CurrencyLogo currency={token} style={{ marginRight: 8 }} />
<Text fontWeight={500} fontSize={16}>
{token.symbol}
</Text>

View File

@@ -1,72 +1,74 @@
import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
import { Currency, CurrencyAmount, currencyEquals, ETHER, JSBI, Token } from '@uniswap/sdk'
import React, { CSSProperties, memo, useContext, useMemo } from 'react'
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 } from '../../hooks/Tokens'
import { useDefaultTokenList } from '../../state/lists/hooks'
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useETHBalances } from '../../state/wallet/hooks'
import { LinkStyledButton, TYPE } from '../../theme'
import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import { RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
import CurrencyLogo from '../CurrencyLogo'
import { FadedSpan, MenuItem } from './styleds'
import Loader from '../Loader'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
import { isDefaultToken } from '../../utils'
export default function TokenList({
tokens,
allTokenBalances,
selectedToken,
onTokenSelect,
otherToken,
showSendWithSwap,
otherSelectedText
function currencyKey(currency: Currency): string {
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
}
export default function CurrencyList({
currencies,
allBalances,
selectedCurrency,
onCurrencySelect,
otherCurrency,
showSendWithSwap
}: {
tokens: Token[]
selectedToken: string
allTokenBalances: { [tokenAddress: string]: TokenAmount }
onTokenSelect: (tokenAddress: string) => void
otherToken: string
currencies: Currency[]
selectedCurrency: Currency
allBalances: { [tokenAddress: string]: CurrencyAmount }
onCurrencySelect: (currency: Currency) => void
otherCurrency: Currency
showSendWithSwap?: boolean
otherSelectedText: string
}) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const allTokens = useAllTokens()
const defaultTokens = useDefaultTokenList()
const addToken = useAddUserToken()
const removeToken = useRemoveUserAddedToken()
const ETHBalance = useETHBalances([account])[account]
const TokenRow = useMemo(() => {
return memo(function TokenRow({ index, style }: { index: number; style: CSSProperties }) {
const token = tokens[index]
const { address, symbol } = token
const isDefault = isDefaultToken(token)
const customAdded = isCustomAddedToken(allTokens, token)
const balance = allTokenBalances[address]
const 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(defaultTokens, currency)
const customAdded = Boolean(!isDefault && currency instanceof Token && allTokens[currency.address])
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 otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
return (
<MenuItem
style={style}
key={address}
className={`token-item-${address}`}
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
disabled={selectedToken && selectedToken === address}
selected={otherToken === address}
className={`token-item-${key}`}
onClick={() => (isSelected ? null : onCurrencySelect(currency))}
disabled={isSelected}
selected={otherSelected}
>
<RowFixed>
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
<CurrencyLogo currency={currency} size={'24px'} style={{ marginRight: '14px' }} />
<Column>
<Text fontWeight={500}>
{symbol}
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
</Text>
<Text fontWeight={500}>{currency.symbol}</Text>
<FadedSpan>
{customAdded ? (
<TYPE.main fontWeight={500}>
@@ -74,7 +76,7 @@ export default function TokenList({
<LinkStyledButton
onClick={event => {
event.stopPropagation()
removeToken(chainId, address)
if (currency instanceof Token) removeToken(chainId, currency.address)
}}
>
(Remove)
@@ -87,7 +89,7 @@ export default function TokenList({
<LinkStyledButton
onClick={event => {
event.stopPropagation()
addToken(token)
if (currency instanceof Token) addToken(currency)
}}
>
(Add)
@@ -122,35 +124,32 @@ export default function TokenList({
)
})
}, [
ETHBalance,
account,
addToken,
allTokenBalances,
allBalances,
allTokens,
chainId,
onTokenSelect,
otherSelectedText,
otherToken,
currencies,
defaultTokens,
onCurrencySelect,
otherCurrency,
removeToken,
selectedToken,
selectedCurrency,
showSendWithSwap,
theme.primary1,
tokens
theme.primary1
])
if (tokens.length === 0) {
return <ModalInfo>{t('noToken')}</ModalInfo>
}
return (
<FixedSizeList
width="100%"
height={500}
itemCount={tokens.length}
itemCount={currencies.length + 1}
itemSize={56}
style={{ flex: '1' }}
itemKey={index => tokens[index].address}
itemKey={index => currencyKey(currencies[index])}
>
{TokenRow}
{CurrencyRow}
</FixedSizeList>
)
}

View File

@@ -1,4 +1,4 @@
import { Token } from '@uniswap/sdk'
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'
@@ -8,7 +8,7 @@ import Card from '../../components/Card'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useToken } from '../../hooks/Tokens'
import useInterval from '../../hooks/useInterval'
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { useAllTokenBalances, useTokenBalance } from '../../state/wallet/hooks'
import { CloseIcon, LinkStyledButton } from '../../theme'
import { isAddress } from '../../utils'
import Column from '../Column'
@@ -20,30 +20,28 @@ import CommonBases from './CommonBases'
import { filterTokens } from './filtering'
import { useTokenComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds'
import TokenList from './TokenList'
import CurrencyList from './CurrencyList'
import SortButton from './SortButton'
interface TokenSearchModalProps {
interface CurrencySearchModalProps {
isOpen?: boolean
onDismiss?: () => void
hiddenToken?: string
hiddenCurrency?: Currency
showSendWithSwap?: boolean
onTokenSelect?: (address: string) => void
otherSelectedTokenAddress?: string
otherSelectedText?: string
onCurrencySelect?: (currency: Currency) => void
otherSelectedCurrency?: Currency
showCommonBases?: boolean
}
export default function TokenSearchModal({
export default function CurrencySearchModal({
isOpen,
onDismiss,
onTokenSelect,
hiddenToken,
onCurrencySelect,
hiddenCurrency,
showSendWithSwap,
otherSelectedTokenAddress,
otherSelectedText,
otherSelectedCurrency,
showCommonBases = false
}: TokenSearchModalProps) {
}: CurrencySearchModalProps) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
@@ -55,8 +53,8 @@ export default function TokenSearchModal({
// if the current input is an address, and we don't have the token in context, try to fetch it and import
const searchToken = useToken(searchQuery)
const searchTokenBalance = useTokenBalanceTreatingWETHasETH(account, searchToken)
const allTokenBalances_ = useAllTokenBalancesTreatingWETHasETH()
const searchTokenBalance = useTokenBalance(account, searchToken)
const allTokenBalances_ = useAllTokenBalances()
const allTokenBalances = searchToken
? {
[searchToken.address]: searchTokenBalance
@@ -87,12 +85,12 @@ export default function TokenSearchModal({
]
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
const handleTokenSelect = useCallback(
(address: string) => {
onTokenSelect(address)
const handleCurrencySelect = useCallback(
(currency: Currency) => {
onCurrencySelect(currency)
onDismiss()
},
[onDismiss, onTokenSelect]
[onDismiss, onCurrencySelect]
)
// clear the input on open
@@ -129,11 +127,11 @@ export default function TokenSearchModal({
filteredSortedTokens[0].symbol.toLowerCase() === searchQuery.trim().toLowerCase() ||
filteredSortedTokens.length === 1
) {
handleTokenSelect(filteredSortedTokens[0].address)
handleCurrencySelect(filteredSortedTokens[0])
}
}
},
[filteredSortedTokens, handleTokenSelect, searchQuery]
[filteredSortedTokens, handleCurrencySelect, searchQuery]
)
return (
@@ -174,7 +172,7 @@ export default function TokenSearchModal({
/>
</Tooltip>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={handleTokenSelect} selectedTokenAddress={hiddenToken} />
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={hiddenCurrency} />
)}
<RowBetween>
<Text fontSize={14} fontWeight={500}>
@@ -184,13 +182,12 @@ export default function TokenSearchModal({
</RowBetween>
</PaddedColumn>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
<TokenList
tokens={filteredSortedTokens}
allTokenBalances={allTokenBalances}
onTokenSelect={handleTokenSelect}
otherSelectedText={otherSelectedText}
otherToken={otherSelectedTokenAddress}
selectedToken={hiddenToken}
<CurrencyList
currencies={filteredSortedTokens}
allBalances={allTokenBalances}
onCurrencySelect={handleCurrencySelect}
otherCurrency={otherSelectedCurrency}
selectedCurrency={hiddenCurrency}
showSendWithSwap={showSendWithSwap}
/>
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />

View File

@@ -1,5 +1,5 @@
import { isAddress } from '../../utils'
import { Pair, Token } from '@uniswap/sdk'
import { Token } from '@uniswap/sdk'
export function filterTokens(tokens: Token[], search: string): Token[] {
if (search.length === 0) return tokens
@@ -34,27 +34,3 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
return matchesSearch(symbol) || matchesSearch(name)
})
}
export function filterPairs(pairs: Pair[], search: string): Pair[] {
if (search.trim().length === 0) return pairs
const addressSearch = isAddress(search)
if (addressSearch) {
return pairs.filter(p => {
return (
p.token0.address === addressSearch ||
p.token1.address === addressSearch ||
p.liquidityToken.address === addressSearch
)
})
}
const lowerSearch = search.toLowerCase()
return pairs.filter(pair => {
const pairExpressionA = `${pair.token0.symbol}/${pair.token1.symbol}`.toLowerCase()
if (pairExpressionA.startsWith(lowerSearch)) return true
const pairExpressionB = `${pair.token1.symbol}/${pair.token0.symbol}`.toLowerCase()
if (pairExpressionB.startsWith(lowerSearch)) return true
return filterTokens([pair.token0, pair.token1], search).length > 0
})
}

View File

@@ -1,8 +1,7 @@
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
import { useAllTokenBalances } from '../../state/wallet/hooks'
// compare two token amounts with highest one coming first
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
@@ -16,26 +15,6 @@ function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
return 0
}
// compare two pairs, favoring "pinned" pairs, and falling back to balances
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
const aShouldBePinned =
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
) ?? false
const bShouldBePinned =
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
) ?? false
if (aShouldBePinned && !bShouldBePinned) {
return -1
} else if (!aShouldBePinned && bShouldBePinned) {
return 1
} else {
return balanceComparator(balanceA, balanceB)
}
}
function getTokenComparator(
weth: Token | undefined,
balances: { [tokenAddress: string]: TokenAmount }
@@ -65,7 +44,7 @@ function getTokenComparator(
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
const { chainId } = useActiveWeb3React()
const weth = WETH[chainId]
const balances = useAllTokenBalancesTreatingWETHasETH()
const balances = useAllTokenBalances()
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
return useMemo(() => {
if (inverted) {

View File

@@ -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;

View File

@@ -1,79 +0,0 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { isAddress } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
import { WETH } from '@uniswap/sdk'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
const getTokenLogoURL = address =>
`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};
height: ${({ size }) => size};
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
border-radius: 24px;
`
export default function TokenLogo({
address,
size = '24px',
...rest
}: {
address?: string
size?: string
style?: React.CSSProperties
}) {
const [, refresh] = useState<number>(0)
const { chainId } = useActiveWeb3React()
let path = ''
const validated = isAddress(address)
// hard code to show ETH instead of WETH in UI
if (validated === WETH[chainId].address) {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
} else if (!NO_LOGO_ADDRESSES[address] && validated) {
path = getTokenLogoURL(validated)
} else {
return (
<Emoji {...rest} size={size}>
<span role="img" aria-label="Thinking">
🤔
</span>
</Emoji>
)
}
return (
<Image
{...rest}
// alt={address}
src={path}
size={size}
onError={() => {
NO_LOGO_ADDRESSES[address] = true
refresh(i => i + 1)
}}
/>
)
}

View File

@@ -1,80 +1,54 @@
import { Token } from '@uniswap/sdk'
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 { useDefaultTokenList } from '../../state/lists/hooks'
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 TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
import { AutoRow, RowBetween } from '../Row'
import { AutoColumn } from '../Column'
import { AlertTriangle } from 'react-feather'
import { ButtonError } from '../Button'
import { useTokenWarningDismissal } from '../../state/user/hooks'
const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
position: relative;
background: ${({ theme }) => transparentize(0.6, theme.white)};
padding: 0.75rem;
border-radius: 20px;
`
const WarningContainer = styled.div`
max-width: 420px;
width: 100%;
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;
background: rgba(242, 150, 2, 0.05);
border: 1px solid #f3841e;
box-sizing: border-box;
border-radius: 20px;
margin-bottom: 2rem;
`
const Row = styled.div`
display: flex;
align-items: center;
justify-items: flex-start;
& > * {
margin-right: 6px;
}
const StyledWarningIcon = styled(AlertTriangle)`
stroke: ${({ theme }) => theme.red2};
`
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 defaultTokens = useDefaultTokenList()
const isDefault = isDefaultToken(defaultTokens, token)
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
const tokenName = token?.name?.toLowerCase() ?? ''
const [dismissed, dismissTokenWarning] = useTokenWarningDismissal(chainId, token)
const allTokens = useAllTokens()
const duplicateNameOrSymbol = useMemo(() => {
@@ -89,50 +63,77 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
})
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
if (isDefault || !token || dismissed) return null
if (isDefault || !token) 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>
<TokenLogo address={token.address} />
<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>
<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>
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
<TYPE.blue> (View on Etherscan)</TYPE.blue>
</ExternalLink>
</AutoColumn>
</AutoRow>
</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 } }) {
const { chainId } = useActiveWeb3React()
const [dismissedToken0, dismissToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
const [dismissedToken1, dismissToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
export function TokenWarningCards({ tokens }: { tokens: { [field in Field]?: Token } }) {
return (
<WarningContainer>
{Object.keys(tokens).map(field =>
tokens[field] ? <TokenWarningCard style={{ marginBottom: 14 }} key={field} token={tokens[field]} /> : null
)}
<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 and name any ERC20 token on Ethereum, 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'}>
Similar to Etherscan, this site can load arbitrary tokens via token addresses. Please do your own research
before interacting with any ERC20 token.
</TYPE.body>
{Object.keys(currencies).map(field => {
const dismissed = field === Field.INPUT ? dismissedToken0 : dismissedToken1
return currencies[field] instanceof Token && !dismissed ? (
<TokenWarningCard key={field} token={currencies[field]} />
) : null
})}
<RowBetween>
<div />
<ButtonError
error={true}
width={'140px'}
padding="0.5rem 1rem"
style={{
borderRadius: '10px'
}}
onClick={() => {
dismissToken0 && dismissToken0()
dismissToken1 && dismissToken1()
}}
>
<TYPE.body color="white" className="token-dismiss-button">
I understand
</TYPE.body>
</ButtonError>
<div />
</RowBetween>
</AutoColumn>
</WarningContainer>
)
}

View File

@@ -1,4 +1,4 @@
import React from 'react'
import React, { useCallback, useState } from 'react'
import styled from 'styled-components'
import Popover, { PopoverProps } from '../Popover'
@@ -16,3 +16,16 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
export default function Tooltip({ text, ...rest }: TooltipProps) {
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
}
export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show'>) {
const [show, setShow] = useState(false)
const open = useCallback(() => setShow(true), [setShow])
const close = useCallback(() => setShow(false), [setShow])
return (
<Tooltip {...rest} show={show}>
<div onMouseEnter={open} onMouseLeave={close}>
{children}
</div>
</Tooltip>
)
}

View File

@@ -0,0 +1,195 @@
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>
<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

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

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

@@ -349,9 +349,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>

View File

@@ -126,6 +126,12 @@ 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()
@@ -187,9 +193,10 @@ export default function Web3Status() {
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
</RowBetween>
) : (
<Text>
{hasSocks ? '🧦' : ''} {ENSName || shortenAddress(account)}
</Text>
<>
{hasSocks ? SOCK : null}
<Text>{ENSName || shortenAddress(account)}</Text>
</>
)}
{!hasPendingTransactions && getStatusIcon()}
</Web3StatusConnected>

View File

@@ -31,8 +31,10 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<RowFixed>
<TYPE.black color={theme.text1} fontSize={14}>
{isExactIn
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.token.symbol}` ?? '-'
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.token.symbol}` ?? '-'}
? `${slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4)} ${trade.outputAmount.currency.symbol}` ??
'-'
: `${slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4)} ${trade.inputAmount.currency.symbol}` ??
'-'}
</TYPE.black>
</RowFixed>
</RowBetween>
@@ -54,7 +56,7 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
</RowFixed>
<TYPE.black fontSize={14} color={theme.text1}>
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.token.symbol}` : '-'}
{realizedLPFee ? `${realizedLPFee.toSignificant(4)} ${trade.inputAmount.currency.symbol}` : '-'}
</TYPE.black>
</RowBetween>
</AutoColumn>
@@ -71,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 { Percent, TokenAmount, 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]?: TokenAmount }
onSwap: () => any
parsedAmounts?: { [field in Field]?: TokenAmount }
realizedLPFee?: TokenAmount
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 sent' : '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]?.token?.symbol
: parsedAmounts[Field.INPUT]?.token?.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?.token?.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 { Token, TokenAmount } 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 CurrencyLogo from '../CurrencyLogo'
import { RowBetween, RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { TruncatedText } from './styleds'
import { TruncatedText, SwapShowAcceptChanges } from './styleds'
export default function SwapModalHeader({
tokens,
formattedAmounts,
slippageAdjustedAmounts,
priceImpactSeverity,
independentField,
recipient
trade,
allowedSlippage,
recipient,
showAcceptChanges,
onAcceptChanges
}: {
tokens: { [field in Field]?: Token }
formattedAmounts: { [field in Field]?: string }
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
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">
<TokenLogo address={tokens[Field.INPUT]?.address} 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' }}>
{tokens[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">
<TokenLogo address={tokens[Field.OUTPUT]?.address} 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' }}>
{tokens[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)} {tokens[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)} {tokens[Field.INPUT]?.symbol}
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {trade.inputAmount.currency.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>

View File

@@ -4,7 +4,7 @@ import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components'
import { TYPE } from '../../theme'
import TokenLogo from '../TokenLogo'
import CurrencyLogo from '../CurrencyLogo'
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
const theme = useContext(ThemeContext)
@@ -24,7 +24,7 @@ export default memo(function SwapRoute({ trade }: { trade: Trade }) {
return (
<Fragment key={i}>
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
<TokenLogo address={token.address} size="1.5rem" />
<CurrencyLogo currency={token} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { Price, Token } from '@uniswap/sdk'
import { Currency, Price } from '@uniswap/sdk'
import { useContext } from 'react'
import { Repeat } from 'react-feather'
import { Text } from 'rebass'
@@ -8,21 +8,27 @@ import { StyledBalanceMaxMini } from './styleds'
interface TradePriceProps {
price?: Price
inputToken?: Token
outputToken?: Token
inputCurrency?: Currency
outputCurrency?: Currency
showInverted: boolean
setShowInverted: (showInverted: boolean) => void
}
export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) {
export default function TradePrice({
price,
inputCurrency,
outputCurrency,
showInverted,
setShowInverted
}: TradePriceProps) {
const theme = useContext(ThemeContext)
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
const show = Boolean(inputToken && outputToken)
const show = Boolean(inputCurrency && outputCurrency)
const label = showInverted
? `${outputToken?.symbol} per ${inputToken?.symbol}`
: `${inputToken?.symbol} per ${outputToken?.symbol}`
? `${outputCurrency?.symbol} per ${inputCurrency?.symbol}`
: `${inputCurrency?.symbol} per ${outputCurrency?.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;
@@ -30,7 +31,6 @@ export const SectionBreak = styled.div`
export const BottomGrouping = styled.div`
margin-top: 12px;
position: relative;
`
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

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

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 = (
@@ -46,25 +110,30 @@ class MiniRpcProvider implements AsyncSendable {
.catch(error => callback(error, null))
}
public readonly request = async (method: string, params?: unknown[] | object): Promise<unknown> => {
const response = await fetch(this.url, {
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method,
params
public readonly request = async (
method: string | { method: string; params: unknown[] },
params?: unknown[] | object
): Promise<unknown> => {
if (typeof method !== 'string') {
return this.request(method.method, 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
}
}

View File

@@ -6,7 +6,6 @@ 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
@@ -28,7 +27,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

@@ -0,0 +1,279 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "guy",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "src",
"type": "address"
},
{
"name": "dst",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "wad",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "dst",
"type": "address"
},
{
"name": "wad",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "deposit",
"outputs": [],
"payable": true,
"stateMutability": "payable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
},
{
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": true,
"name": "guy",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": true,
"name": "dst",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "dst",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Deposit",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "src",
"type": "address"
},
{
"indexed": false,
"name": "wad",
"type": "uint256"
}
],
"name": "Withdrawal",
"type": "event"
}
]

View File

@@ -1,7 +1,7 @@
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
import { AbstractConnector } from '@web3-react/abstract-connector'
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
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,
@@ -36,42 +53,30 @@ export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
}
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] } = {
[ChainId.MAINNET]: [
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
'0'
)
),
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
'0'
)
),
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
'0'
)
)
[
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin')
],
[USDC, USDT],
[DAI, USDT]
]
}
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',
@@ -88,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
@@ -166,3 +162,6 @@ export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(
// used to ensure the user doesn't send so much ETH so they end up with <.01
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
// the Uniswap Default token list lives here
export const DEFAULT_TOKEN_LIST_URL = 'https://unpkg.com/@uniswap/default-token-list@latest'

View File

@@ -1,44 +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
]
// remap WETH to ETH
.map(token => {
if (token.equals(WETH[token.chainId])) {
;(token as any).symbol = 'ETH'
;(token as any).name = 'Ether'
}
return token
})
// 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,18 +1,33 @@
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
import { TokenAmount, Pair, Currency } from '@uniswap/sdk'
import { useMemo } from 'react'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { Interface } from '@ethersproject/abi'
import { useActiveWeb3React } from '../hooks'
import { useMultipleContractSingleData } from '../state/multicall/hooks'
import { wrappedCurrency } from '../utils/wrappedCurrency'
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
/*
* if loading, return undefined
* if no pair created yet, return null
* if pair already created (even if 0 reserves), return pair
*/
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
export enum PairState {
LOADING,
NOT_EXISTS,
EXISTS,
INVALID
}
export function usePairs(currencies: [Currency | undefined, Currency | undefined][]): [PairState, Pair | null][] {
const { chainId } = useActiveWeb3React()
const tokens = useMemo(
() =>
currencies.map(([currencyA, currencyB]) => [
wrappedCurrency(currencyA, chainId),
wrappedCurrency(currencyB, chainId)
]),
[chainId, currencies]
)
const pairAddresses = useMemo(
() =>
tokens.map(([tokenA, tokenB]) => {
@@ -29,15 +44,19 @@ export function usePairs(tokens: [Token | undefined, Token | undefined][]): (und
const tokenA = tokens[i][0]
const tokenB = tokens[i][1]
if (loading || !tokenA || !tokenB) return undefined
if (!reserves) return null
if (loading) return [PairState.LOADING, null]
if (!tokenA || !tokenB || tokenA.equals(tokenB)) return [PairState.INVALID, null]
if (!reserves) return [PairState.NOT_EXISTS, null]
const { reserve0, reserve1 } = reserves
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
return [
PairState.EXISTS,
new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
]
})
}, [results, tokens])
}
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
export function usePair(tokenA?: Currency, tokenB?: Currency): [PairState, Pair | null] {
return usePairs([[tokenA, tokenB]])[0]
}

View File

@@ -1,4 +1,20 @@
import { JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
import { AddressZero } from '@ethersproject/constants'
import {
BigintIsh,
Currency,
CurrencyAmount,
currencyEquals,
ETHER,
JSBI,
Pair,
Percent,
Route,
Token,
TokenAmount,
Trade,
TradeType,
WETH
} from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../hooks'
import { useAllTokens } from '../hooks/Tokens'
@@ -6,7 +22,6 @@ import { useV1FactoryContract } from '../hooks/useContract'
import { Version } from '../hooks/useToggledVersion'
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
import { AddressZero } from '@ethersproject/constants'
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
const contract = useV1FactoryContract()
@@ -15,18 +30,25 @@ export function useV1ExchangeAddress(tokenAddress?: string): string | undefined
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
}
class MockV1Pair extends Pair {}
export class MockV1Pair extends Pair {
constructor(etherAmount: BigintIsh, tokenAmount: TokenAmount) {
super(tokenAmount, new TokenAmount(WETH[tokenAmount.token.chainId], etherAmount))
}
}
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
function useMockV1Pair(inputCurrency?: Currency): MockV1Pair | undefined {
const token = inputCurrency instanceof Token ? inputCurrency : undefined
const isWETH = Boolean(token && token.equals(WETH[token.chainId]))
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
const tokenBalance = useTokenBalance(v1PairAddress, token)
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
return tokenBalance && ETHBalance && token
? new MockV1Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
: undefined
return useMemo(
() =>
token && tokenBalance && ETHBalance && inputCurrency ? new MockV1Pair(ETHBalance.raw, tokenBalance) : undefined,
[ETHBalance, inputCurrency, token, tokenBalance]
)
}
// returns all v1 exchange addresses in the user's token list
@@ -41,8 +63,7 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
() =>
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
if (result?.[0] && result[0] !== AddressZero) {
const token = allTokens[args[ix][0]]
memo[result[0]] = token
memo[result[0]] = allTokens[args[ix][0]]
}
return memo
}, {}) ?? {},
@@ -56,13 +77,13 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
const exchanges = useAllTokenV1Exchanges()
const fakeLiquidityTokens = useMemo(
const v1ExchangeLiquidityTokens = useMemo(
() =>
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
[chainId, exchanges]
)
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
const balances = useTokenBalances(account ?? undefined, v1ExchangeLiquidityTokens)
return useMemo(
() =>
@@ -79,39 +100,39 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
*/
export function useV1Trade(
isExactIn?: boolean,
inputToken?: Token,
outputToken?: Token,
exactAmount?: TokenAmount
inputCurrency?: Currency,
outputCurrency?: Currency,
exactAmount?: CurrencyAmount
): Trade | undefined {
const { chainId } = useActiveWeb3React()
// get the mock v1 pairs
const inputPair = useMockV1Pair(inputToken)
const outputPair = useMockV1Pair(outputToken)
const inputPair = useMockV1Pair(inputCurrency)
const outputPair = useMockV1Pair(outputCurrency)
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
const inputIsETH = inputCurrency === ETHER
const outputIsETH = outputCurrency === ETHER
// construct a direct or through ETH v1 route
let pairs: Pair[] = []
if (inputIsWETH && outputPair) {
if (inputIsETH && outputPair) {
pairs = [outputPair]
} else if (outputIsWETH && inputPair) {
} else if (outputIsETH && inputPair) {
pairs = [inputPair]
}
// if neither are WETH, it's token-to-token (if they both exist)
// if neither are ETH, it's token-to-token (if they both exist)
else if (inputPair && outputPair) {
pairs = [inputPair, outputPair]
}
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
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
}
@@ -125,14 +146,13 @@ export function getTradeVersion(trade?: Trade): Version | undefined {
// returns the v1 exchange against which a trade should be executed
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
const tokenAddress: string | undefined = useMemo(() => {
const tradeVersion = getTradeVersion(trade)
const isV1 = tradeVersion === Version.v1
return isV1
? trade &&
WETH[trade.inputAmount.token.chainId] &&
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
? trade.outputAmount.token.address
: trade?.inputAmount?.token?.address
if (!trade) return undefined
const isV1 = getTradeVersion(trade) === Version.v1
if (!isV1) return undefined
return trade.inputAmount instanceof TokenAmount
? trade.inputAmount.token.address
: trade.outputAmount instanceof TokenAmount
? trade.outputAmount.token.address
: undefined
}, [trade])
return useV1ExchangeAddress(tokenAddress)
@@ -140,7 +160,8 @@ export function useV1TradeExchangeAddress(trade: Trade | undefined): string | un
const ZERO_PERCENT = new Percent('0')
const ONE_HUNDRED_PERCENT = new Percent('1')
// returns whether tradeB is better than tradeA by at least a threshold
// returns whether tradeB is better than tradeA by at least a threshold percentage amount
export function isTradeBetter(
tradeA: Trade | undefined,
tradeB: Trade | undefined,
@@ -150,8 +171,8 @@ export function isTradeBetter(
if (
tradeA.tradeType !== tradeB.tradeType ||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
!currencyEquals(tradeB.outputAmount.currency, tradeB.outputAmount.currency)
) {
throw new Error('Trades are not comparable')
}

View File

@@ -1,7 +1,7 @@
import { parseBytes32String } from '@ethersproject/strings'
import { ChainId, Token, WETH } from '@uniswap/sdk'
import { Currency, ETHER, Token } from '@uniswap/sdk'
import { useMemo } from 'react'
import { ALL_TOKENS } from '../constants/tokens'
import { useDefaultTokenList } from '../state/lists/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useUserAddedTokens } from '../state/user/hooks'
import { isAddress } from '../utils'
@@ -12,30 +12,24 @@ import { useBytes32TokenContract, useTokenContract } from './useContract'
export function useAllTokens(): { [address: string]: Token } {
const { chainId } = useActiveWeb3React()
const userAddedTokens = useUserAddedTokens()
const allTokens = useDefaultTokenList()
return useMemo(() => {
if (!chainId) return {}
const tokens = userAddedTokens
// reduce into all ALL_TOKENS filtered by the current chain
.reduce<{ [address: string]: Token }>(
(tokenMap, token) => {
tokenMap[token.address] = token
return tokenMap
},
// 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] }
)
const weth = WETH[chainId as ChainId]
if (weth) {
// we have to replace it as a workaround because if it is automatically
// fetched by address it will cause an invariant when used in constructing
// pairs since we replace the name and symbol with 'ETH' and 'Ether'
tokens[weth.address] = weth
}
return tokens
}, [userAddedTokens, chainId])
return (
userAddedTokens
// reduce into all ALL_TOKENS filtered by the current chain
.reduce<{ [address: string]: Token }>(
(tokenMap, token) => {
tokenMap[token.address] = token
return tokenMap
},
// must make a copy because reduce modifies the map, and we do not
// want to make a copy in every iteration
{ ...allTokens[chainId] }
)
)
}, [chainId, userAddedTokens, allTokens])
}
// parse a name or symbol from a token response
@@ -100,3 +94,9 @@ export function useToken(tokenAddress?: string): Token | undefined | null {
tokenNameBytes32.result
])
}
export function useCurrency(currencyId: string | undefined): Currency | null | undefined {
const isETH = currencyId?.toUpperCase() === 'ETH'
const token = useToken(isETH ? undefined : currencyId)
return isETH ? ETHER : token
}

View File

@@ -1,29 +1,51 @@
import { Pair, Token, TokenAmount, Trade } from '@uniswap/sdk'
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 { usePairs } from '../data/Reserves'
import { BASES_TO_CHECK_TRADES_AGAINST, CUSTOM_BASES } from '../constants'
import { PairState, usePairs } from '../data/Reserves'
import { wrappedCurrency } from '../utils/wrappedCurrency'
import { useActiveWeb3React } from './index'
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
const { chainId } = useActiveWeb3React()
const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
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 [tokenA, tokenB] = chainId
? [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
: [undefined, undefined]
const allPairCombinations: [Token, Token][] = 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]))
]
.filter((tokens): tokens is [Token, Token] => Boolean(tokens[0] && tokens[1]))
.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.findIndex(base => tokenB.equals(base)) === -1) return false
if (customBasesB && customBasesB.findIndex(base => tokenA.equals(base)) === -1) return false
return true
}),
[tokenA, tokenB, bases, chainId]
)
const allPairs = usePairs(allPairCombinations)
@@ -34,9 +56,9 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
Object.values(
allPairs
// filter out invalid pairs
.filter((p): p is Pair => !!p)
.filter((result): result is [PairState.EXISTS, Pair] => Boolean(result[0] === PairState.EXISTS && result[1]))
// filter out duplicated pairs
.reduce<{ [pairAddress: string]: Pair }>((memo, curr) => {
.reduce<{ [pairAddress: string]: Pair }>((memo, [, curr]) => {
memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
return memo
}, {})
@@ -48,27 +70,32 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
/**
* Returns the best trade for the exact amount of tokens in to the given token out
*/
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
return useMemo(() => {
if (amountIn && tokenOut && allowedPairs.length > 0) {
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
return (
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
)
}
return null
}, [allowedPairs, amountIn, tokenOut])
}, [allowedPairs, currencyAmountIn, currencyOut])
}
/**
* Returns the best trade for the token in to the exact amount of token out
*/
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
const allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: CurrencyAmount): Trade | null {
const allowedPairs = useAllCommonPairs(currencyIn, currencyAmountOut?.currency)
return useMemo(() => {
if (tokenIn && amountOut && allowedPairs.length > 0) {
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
return (
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
null
)
}
return null
}, [allowedPairs, tokenIn, amountOut])
}, [allowedPairs, currencyIn, currencyAmountOut])
}

View File

@@ -72,21 +72,12 @@ 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)
}
}

View File

@@ -1,6 +1,6 @@
import { MaxUint256 } from '@ethersproject/constants'
import { TransactionResponse } from '@ethersproject/providers'
import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
import { Trade, TokenAmount, CurrencyAmount, ETHER } from '@uniswap/sdk'
import { useCallback, useMemo } from 'react'
import { ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
@@ -22,19 +22,18 @@ export enum ApprovalState {
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
export function useApproveCallback(
amountToApprove?: TokenAmount,
amountToApprove?: CurrencyAmount,
spender?: string
): [ApprovalState, () => Promise<void>] {
const { account } = useActiveWeb3React()
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
const token = amountToApprove instanceof TokenAmount ? amountToApprove.token : undefined
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(token?.address, spender)
// check the current approval status
const approvalState: ApprovalState = useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
// we treat WETH as ETH which requires no approvals
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
if (amountToApprove.currency === ETHER) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve
if (!currentAllowance) return ApprovalState.UNKNOWN
@@ -46,7 +45,7 @@ export function useApproveCallback(
: ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender])
const tokenContract = useTokenContract(amountToApprove?.token?.address)
const tokenContract = useTokenContract(token?.address)
const addTransaction = useTransactionAdder()
const approve = useCallback(async (): Promise<void> => {
@@ -54,6 +53,10 @@ export function useApproveCallback(
console.error('approve was called unnecessarily')
return
}
if (!token) {
console.error('no token')
return
}
if (!tokenContract) {
console.error('tokenContract is null')
@@ -83,15 +86,15 @@ export function useApproveCallback(
})
.then((response: TransactionResponse) => {
addTransaction(response, {
summary: 'Approve ' + amountToApprove.token.symbol,
approval: { tokenAddress: amountToApprove.token.address, spender: spender }
summary: 'Approve ' + amountToApprove.currency.symbol,
approval: { tokenAddress: token.address, spender: spender }
})
})
.catch((error: Error) => {
console.debug('Failed to approve token', error)
throw error
})
}, [approvalState, tokenContract, spender, amountToApprove, addTransaction])
}, [approvalState, token, tokenContract, amountToApprove, spender, addTransaction])
return [approvalState, approve]
}

View File

@@ -1,25 +0,0 @@
import { useCallback, useEffect } from 'react'
// modified from https://usehooks.com/useKeyPress/
export default function useBodyKeyDown(targetKey: string, onKeyDown: () => void, suppressOnKeyDown = false) {
const downHandler = useCallback(
event => {
const {
target: { tagName },
key
} = event
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
event.preventDefault()
onKeyDown()
}
},
[targetKey, onKeyDown, suppressOnKeyDown]
)
useEffect(() => {
window.addEventListener('keydown', downHandler)
return () => {
window.removeEventListener('keydown', downHandler)
}
}, [downHandler])
}

View File

@@ -1,10 +1,11 @@
import { Contract } from '@ethersproject/contracts'
import { ChainId } from '@uniswap/sdk'
import { ChainId, WETH } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useMemo } from 'react'
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
import 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 { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
@@ -43,6 +44,11 @@ export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: b
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
}
export function useWETHContract(withSignerIfPossible?: boolean): Contract | null {
const { chainId } = useActiveWeb3React()
return useContract(chainId ? WETH[chainId].address : undefined, WETH_ABI, withSignerIfPossible)
}
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
}

View File

@@ -1,15 +1,23 @@
import { useCallback, useEffect, useState } from 'react'
const VISIBILITY_STATE_SUPPORTED = 'visibilityState' in document
function isWindowVisible() {
return !VISIBILITY_STATE_SUPPORTED || document.visibilityState !== 'hidden'
}
/**
* Returns whether the window is currently visible to the user.
*/
export default function useIsWindowVisible(): boolean {
const [focused, setFocused] = useState<boolean>(true)
const [focused, setFocused] = useState<boolean>(isWindowVisible())
const listener = useCallback(() => {
setFocused(document.visibilityState !== 'hidden')
setFocused(isWindowVisible())
}, [setFocused])
useEffect(() => {
if (!VISIBILITY_STATE_SUPPORTED) return
document.addEventListener('visibilitychange', listener)
return () => {
document.removeEventListener('visibilitychange', listener)

View File

@@ -1,13 +1,33 @@
import { useEffect, useState } from 'react'
/**
* Returns the last value of type T that passes a filter function
* @param value changing value
* @param filterFn function that determines whether a given value should be considered for the last value
*/
export default function useLast<T>(
value: T | undefined | null,
filterFn?: (value: T | null | undefined) => boolean
): T | null | undefined {
const [last, setLast] = useState<T | null | undefined>(filterFn && filterFn(value) ? value : undefined)
useEffect(() => {
setLast(last => {
const shouldUse: boolean = filterFn ? filterFn(value) : true
if (shouldUse) return value
return last
})
}, [filterFn, value])
return last
}
function isDefined<T>(x: T | null | undefined): x is T {
return x !== null && x !== undefined
}
/**
* Returns the last truthy value of type T
* @param value changing value
*/
export default function useLast<T>(value: T | undefined | null): T | null | undefined {
const [last, setLast] = useState<T | null | undefined>(value)
useEffect(() => {
setLast(last => value ?? last)
}, [value])
return last
export function useLastTruthy<T>(value: T | undefined | null): T | null | undefined {
return useLast(value, isDefined)
}

View File

@@ -1,6 +1,6 @@
import { JSBI } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useSingleCallResult } from '../state/multicall/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useActiveWeb3React } from './index'
import { useSocksController } from './useContract'
@@ -8,7 +8,7 @@ export default function useSocksBalance(): JSBI | undefined {
const { account } = useActiveWeb3React()
const socksContract = useSocksController()
const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], { blocksPerFetch: 100 })
const { result } = useSingleCallResult(socksContract, 'balanceOf', [account ?? undefined], NEVER_RELOAD)
const data = result?.[0]
return data ? JSBI.BigInt(data.toString()) : undefined
}

View File

@@ -1,59 +1,107 @@
import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { Trade, TradeType, WETH } from '@uniswap/sdk'
import { JSBI, Percent, Router, SwapParameters, Trade, TradeType } from '@uniswap/sdk'
import { useMemo } from 'react'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
import { useTokenAllowance } from '../data/Allowances'
import { BIPS_BASE, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../constants'
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
import { Field } from '../state/swap/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin, getRouterContract, shortenAddress, isAddress } from '../utils'
import { computeSlippageAdjustedAmounts } from '../utils/prices'
import { calculateGasMargin, getRouterContract, isAddress, shortenAddress } from '../utils'
import isZero from '../utils/isZero'
import v1SwapArguments from '../utils/v1SwapArguments'
import { useActiveWeb3React } from './index'
import { useV1ExchangeContract } from './useContract'
import useENS from './useENS'
import { Version } from './useToggledVersion'
enum SwapType {
EXACT_TOKENS_FOR_TOKENS,
EXACT_TOKENS_FOR_ETH,
EXACT_ETH_FOR_TOKENS,
TOKENS_FOR_EXACT_TOKENS,
TOKENS_FOR_EXACT_ETH,
ETH_FOR_EXACT_TOKENS,
V1_EXACT_ETH_FOR_TOKENS,
V1_EXACT_TOKENS_FOR_ETH,
V1_EXACT_TOKENS_FOR_TOKENS,
V1_ETH_FOR_EXACT_TOKENS,
V1_TOKENS_FOR_EXACT_ETH,
V1_TOKENS_FOR_EXACT_TOKENS
export enum SwapCallbackState {
INVALID,
LOADING,
VALID
}
function getSwapType(trade: Trade | undefined): SwapType | undefined {
if (!trade) return undefined
const chainId = trade.inputAmount.token.chainId
const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const isV1 = getTradeVersion(trade) === Version.v1
if (isExactIn) {
if (inputWETH) {
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
} else {
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
interface SwapCall {
contract: Contract
parameters: SwapParameters
}
interface SuccessfulCall {
call: SwapCall
gasEstimate: BigNumber
}
interface FailedCall {
call: SwapCall
error: Error
}
type EstimatedSwapCall = SuccessfulCall | FailedCall
/**
* Returns the swap calls that can be used to make the trade
* @param trade trade to execute
* @param allowedSlippage user allowed slippage
* @param deadline the deadline for the trade
* @param recipientAddressOrName
*/
function useSwapCallArguments(
trade: Trade | undefined, // trade to execute, required
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
return useMemo(() => {
const tradeVersion = getTradeVersion(trade)
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return []
const contract: Contract | null =
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
if (!contract) {
return []
}
} else {
if (inputWETH) {
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
} else if (outputWETH) {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
} else {
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
const swapMethods = []
switch (tradeVersion) {
case Version.v2:
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push(
Router.swapCallParameters(trade, {
feeOnTransfer: true,
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
}
break
case Version.v1:
swapMethods.push(
v1SwapArguments(trade, {
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
recipient,
ttl: deadline
})
)
break
}
}
return swapMethods.map(parameters => ({ parameters, contract }))
}, [account, allowedSlippage, chainId, deadline, library, recipient, trade, v1Exchange])
}
// returns a function that will execute a swap, if the parameters are all valid
@@ -63,222 +111,103 @@ export function useSwapCallback(
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
): null | (() => Promise<string>) {
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: string | null } {
const { account, chainId, library } = useActiveWeb3React()
const swapCalls = useSwapCallArguments(trade, allowedSlippage, deadline, recipientAddressOrName)
const addTransaction = useTransactionAdder()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const tradeVersion = getTradeVersion(trade)
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
const inputAllowance = useTokenAllowance(
trade?.inputAmount?.token,
account ?? undefined,
tradeVersion === Version.v1 ? v1Exchange?.address : ROUTER_ADDRESS
)
return useMemo(() => {
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
// will always be defined
const {
[Field.INPUT]: slippageAdjustedInput,
[Field.OUTPUT]: slippageAdjustedOutput
} = computeSlippageAdjustedAmounts(trade, allowedSlippage)
if (!slippageAdjustedInput || !slippageAdjustedOutput) return null
// no allowance
if (
!trade.inputAmount.token.equals(WETH[chainId]) &&
(!inputAllowance || slippageAdjustedInput.greaterThan(inputAllowance))
) {
return null
if (!trade || !library || !account || !chainId) {
return { state: SwapCallbackState.INVALID, callback: null, error: 'Missing dependencies' }
}
if (!recipient) {
if (recipientAddressOrName !== null) {
return { state: SwapCallbackState.INVALID, callback: null, error: 'Invalid recipient' }
} else {
return { state: SwapCallbackState.LOADING, callback: null, error: null }
}
}
return async function onSwap() {
const contract: Contract | null =
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
if (!contract) {
throw new Error('Failed to get a swap contract')
}
const tradeVersion = getTradeVersion(trade)
const path = trade.route.path.map(t => t.address)
return {
state: SwapCallbackState.VALID,
callback: async function onSwap(): Promise<string> {
const estimatedCalls: EstimatedSwapCall[] = await Promise.all(
swapCalls.map(call => {
const {
parameters: { methodName, args, value },
contract
} = call
const options = !value || isZero(value) ? {} : { value }
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
return contract.estimateGas[methodName](...args, options)
.then(gasEstimate => {
return {
call,
gasEstimate
}
})
.catch(gasError => {
console.debug('Gas estimate failed, trying eth_call to extract error', call)
const swapType = getSwapType(trade)
// let estimate: Function, method: Function,
let methodNames: string[],
args: Array<string | string[] | number>,
value: BigNumber | null = null
switch (swapType) {
case SwapType.EXACT_TOKENS_FOR_TOKENS:
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.TOKENS_FOR_EXACT_TOKENS:
methodNames = ['swapTokensForExactTokens']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.EXACT_ETH_FOR_TOKENS:
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
methodNames = ['swapTokensForExactETH']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.EXACT_TOKENS_FOR_ETH:
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
path,
recipient,
deadlineFromNow
]
break
case SwapType.ETH_FOR_EXACT_TOKENS:
methodNames = ['swapETHForExactTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_ETH_FOR_TOKENS:
methodNames = ['ethToTokenTransferInput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_EXACT_TOKENS_FOR_TOKENS:
methodNames = ['tokenToTokenTransferInput']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
1,
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
case SwapType.V1_EXACT_TOKENS_FOR_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_ETH_FOR_EXACT_TOKENS:
methodNames = ['ethToTokenTransferOutput']
args = [slippageAdjustedOutput.raw.toString(), deadlineFromNow, recipient]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.V1_TOKENS_FOR_EXACT_ETH:
methodNames = ['tokenToEthTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
deadlineFromNow,
recipient
]
break
case SwapType.V1_TOKENS_FOR_EXACT_TOKENS:
methodNames = ['tokenToTokenTransferOutput']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
MaxUint256.toString(),
deadlineFromNow,
recipient,
trade.outputAmount.token.address
]
break
default:
throw new Error(`Unhandled swap type: ${swapType}`)
}
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
contract.estimateGas[methodName](...args, value ? { value } : {})
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
return undefined
})
return contract.callStatic[methodName](...args, options)
.then(result => {
console.debug('Unexpected successful call after failed estimate gas', call, gasError, result)
return { call, error: new Error('Unexpected issue with estimating the gas. Please try again.') }
})
.catch(callError => {
console.debug('Call threw error', call, callError)
let errorMessage: string
switch (callError.reason) {
case 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT':
case 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT':
errorMessage =
'This transaction will not succeed either due to price movement or fee on transfer. Try increasing your slippage tolerance.'
break
default:
errorMessage = `The transaction cannot succeed due to error: ${callError.reason}. This is probably an issue with one of the tokens you are swapping.`
}
return { call, error: new Error(errorMessage) }
})
})
})
)
)
// we expect failures from left to right, so throw if we see failures
// from right to left
for (let i = 0; i < safeGasEstimates.length - 1; i++) {
// if the FoT method fails, but the regular method does not, we should not
// use the regular method. this probably means something is wrong with the fot token.
if (BigNumber.isBigNumber(safeGasEstimates[i]) && !BigNumber.isBigNumber(safeGasEstimates[i + 1])) {
throw new Error(
'An error occurred. Please try raising your slippage. If that does not work, contact support.'
)
}
}
// a successful estimation is a bignumber gas estimate and the next call is also a bignumber gas estimate
const successfulEstimation = estimatedCalls.find(
(el, ix, list): el is SuccessfulCall =>
'gasEstimate' in el && (ix === list.length - 1 || 'gasEstimate' in list[ix + 1])
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
if (!successfulEstimation) {
const errorCalls = estimatedCalls.filter((call): call is FailedCall => 'error' in call)
if (errorCalls.length > 0) throw errorCalls[errorCalls.length - 1].error
throw new Error('Unexpected error. Please contact support: none of the calls threw an error')
}
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
// if only 1 method exists, either:
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
// b) the token is FoT and the user specified an exact output, which is not allowed
if (methodNames.length === 1) {
throw Error(
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.`
)
}
// if 2 methods exists, either:
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
// b) the token is FoT and is taking more than the specified slippage
else if (methodNames.length === 2) {
throw Error(
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.`
)
} else {
throw Error('This transaction would fail. Please contact support.')
}
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
const {
call: {
contract,
parameters: { methodName, args, value }
},
gasEstimate
} = successfulEstimation
return contract[methodName](...args, {
gasLimit: safeGasEstimate,
...(value ? { value } : {})
gasLimit: calculateGasMargin(gasEstimate),
...(value && !isZero(value) ? { value, from: account } : { from: account })
})
.then((response: any) => {
const inputSymbol = trade.inputAmount.token.symbol
const outputSymbol = trade.outputAmount.token.symbol
const inputAmount = slippageAdjustedInput.toSignificant(3)
const outputAmount = slippageAdjustedOutput.toSignificant(3)
const inputSymbol = trade.inputAmount.currency.symbol
const outputSymbol = trade.outputAmount.currency.symbol
const inputAmount = trade.inputAmount.toSignificant(3)
const outputAmount = trade.outputAmount.toSignificant(3)
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
const withRecipient =
@@ -291,7 +220,7 @@ export function useSwapCallback(
}`
const withVersion =
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${(tradeVersion as any).toUpperCase()}`
addTransaction(response, {
summary: withVersion
@@ -302,28 +231,15 @@ export function useSwapCallback(
.catch((error: any) => {
// if the user rejected the tx, pass this along
if (error?.code === 4001) {
throw error
}
// otherwise, the error was unexpected and we need to convey that
else {
throw new Error('Transaction rejected.')
} else {
// otherwise, the error was unexpected and we need to convey that
console.error(`Swap failed`, error, methodName, args, value)
throw Error('An error occurred while swapping. Please contact support.')
throw new Error(`Swap failed: ${error.message}`)
}
})
}
},
error: null
}
}, [
trade,
recipient,
library,
account,
tradeVersion,
chainId,
allowedSlippage,
inputAllowance,
v1Exchange,
deadline,
recipientAddressOrName,
addTransaction
])
}, [trade, library, account, chainId, recipient, recipientAddressOrName, swapCalls, addTransaction])
}

View File

@@ -0,0 +1,75 @@
import { Currency, currencyEquals, ETHER, WETH } from '@uniswap/sdk'
import { useMemo } from 'react'
import { tryParseAmount } from '../state/swap/hooks'
import { useTransactionAdder } from '../state/transactions/hooks'
import { useCurrencyBalance } from '../state/wallet/hooks'
import { useActiveWeb3React } from './index'
import { useWETHContract } from './useContract'
export enum WrapType {
NOT_APPLICABLE,
WRAP,
UNWRAP
}
const NOT_APPLICABLE = { wrapType: WrapType.NOT_APPLICABLE }
/**
* Given the selected input and output currency, return a wrap callback
* @param inputCurrency the selected input currency
* @param outputCurrency the selected output currency
* @param typedValue the user input value
*/
export default function useWrapCallback(
inputCurrency: Currency | undefined,
outputCurrency: Currency | undefined,
typedValue: string | undefined
): { wrapType: WrapType; execute?: undefined | (() => Promise<void>); inputError?: string } {
const { chainId, account } = useActiveWeb3React()
const wethContract = useWETHContract()
const balance = useCurrencyBalance(account ?? undefined, inputCurrency)
// we can always parse the amount typed as the input currency, since wrapping is 1:1
const inputAmount = useMemo(() => tryParseAmount(typedValue, inputCurrency), [inputCurrency, typedValue])
const addTransaction = useTransactionAdder()
return useMemo(() => {
if (!wethContract || !chainId || !inputCurrency || !outputCurrency) return NOT_APPLICABLE
const sufficientBalance = inputAmount && balance && !balance.lessThan(inputAmount)
if (inputCurrency === ETHER && currencyEquals(WETH[chainId], outputCurrency)) {
return {
wrapType: WrapType.WRAP,
execute:
sufficientBalance && inputAmount
? async () => {
try {
const txReceipt = await wethContract.deposit({ value: `0x${inputAmount.raw.toString(16)}` })
addTransaction(txReceipt, { summary: `Wrap ${inputAmount.toSignificant(6)} ETH to WETH` })
} catch (error) {
console.error('Could not deposit', error)
}
}
: undefined,
inputError: sufficientBalance ? undefined : 'Insufficient ETH balance'
}
} else if (currencyEquals(WETH[chainId], inputCurrency) && outputCurrency === ETHER) {
return {
wrapType: WrapType.UNWRAP,
execute:
sufficientBalance && inputAmount
? async () => {
try {
const txReceipt = await wethContract.withdraw(`0x${inputAmount.raw.toString(16)}`)
addTransaction(txReceipt, { summary: `Unwrap ${inputAmount.toSignificant(6)} WETH to ETH` })
} catch (error) {
console.error('Could not withdraw', error)
}
}
: undefined,
inputError: sufficientBalance ? undefined : 'Insufficient WETH balance'
}
} else {
return NOT_APPLICABLE
}
}, [wethContract, chainId, inputCurrency, outputCurrency, inputAmount, balance, addTransaction])
}

View File

@@ -12,12 +12,17 @@ import App from './pages/App'
import store from './state'
import ApplicationUpdater from './state/application/updater'
import TransactionUpdater from './state/transactions/updater'
import ListsUpdater from './state/lists/updater'
import UserUpdater from './state/user/updater'
import MulticallUpdater from './state/multicall/updater'
import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
if ('ethereum' in window) {
;(window.ethereum as any).autoRefreshOnNetworkChange = false
}
function getLibrary(provider: any): Web3Provider {
const library = new Web3Provider(provider)
library.pollingInterval = 15000
@@ -44,6 +49,7 @@ window.addEventListener('error', error => {
function Updaters() {
return (
<>
<ListsUpdater />
<UserUpdater />
<ApplicationUpdater />
<TransactionUpdater />

View File

@@ -1,52 +1,56 @@
import { Fraction, Percent, Token, TokenAmount } from '@uniswap/sdk'
import { Currency, CurrencyAmount, Fraction, Percent } from '@uniswap/sdk'
import React from 'react'
import { Text } from 'rebass'
import { ButtonPrimary } from '../../components/Button'
import { RowBetween, RowFixed } from '../../components/Row'
import TokenLogo from '../../components/TokenLogo'
import CurrencyLogo from '../../components/CurrencyLogo'
import { Field } from '../../state/mint/actions'
import { TYPE } from '../../theme'
export function ConfirmAddModalBottom({
noLiquidity,
price,
tokens,
currencies,
parsedAmounts,
poolTokenPercentage,
onAdd
}: {
noLiquidity?: boolean
price?: Fraction
tokens: { [field in Field]?: Token }
parsedAmounts: { [field in Field]?: TokenAmount }
currencies: { [field in Field]?: Currency }
parsedAmounts: { [field in Field]?: CurrencyAmount }
poolTokenPercentage?: Percent
onAdd: () => void
}) {
return (
<>
<RowBetween>
<TYPE.body>{tokens[Field.TOKEN_A]?.symbol} Deposited</TYPE.body>
<TYPE.body>{currencies[Field.CURRENCY_A]?.symbol} Deposited</TYPE.body>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}</TYPE.body>
<CurrencyLogo currency={currencies[Field.CURRENCY_A]} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
<RowBetween>
<TYPE.body>{tokens[Field.TOKEN_B]?.symbol} Deposited</TYPE.body>
<TYPE.body>{currencies[Field.CURRENCY_B]?.symbol} Deposited</TYPE.body>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}</TYPE.body>
<CurrencyLogo currency={currencies[Field.CURRENCY_B]} style={{ marginRight: '8px' }} />
<TYPE.body>{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}</TYPE.body>
</RowFixed>
</RowBetween>
<RowBetween>
<TYPE.body>Rates</TYPE.body>
<TYPE.body>
{`1 ${tokens[Field.TOKEN_A]?.symbol} = ${price?.toSignificant(4)} ${tokens[Field.TOKEN_B]?.symbol}`}
{`1 ${currencies[Field.CURRENCY_A]?.symbol} = ${price?.toSignificant(4)} ${
currencies[Field.CURRENCY_B]?.symbol
}`}
</TYPE.body>
</RowBetween>
<RowBetween style={{ justifyContent: 'flex-end' }}>
<TYPE.body>
{`1 ${tokens[Field.TOKEN_B]?.symbol} = ${price?.invert().toSignificant(4)} ${tokens[Field.TOKEN_A]?.symbol}`}
{`1 ${currencies[Field.CURRENCY_B]?.symbol} = ${price?.invert().toSignificant(4)} ${
currencies[Field.CURRENCY_A]?.symbol
}`}
</TYPE.body>
</RowBetween>
<RowBetween>

View File

@@ -1,4 +1,4 @@
import { Fraction, Percent, Token } from '@uniswap/sdk'
import { Currency, Percent, Price } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
@@ -8,31 +8,31 @@ import { ONE_BIPS } from '../../constants'
import { Field } from '../../state/mint/actions'
import { TYPE } from '../../theme'
export const PoolPriceBar = ({
tokens,
export function PoolPriceBar({
currencies,
noLiquidity,
poolTokenPercentage,
price
}: {
tokens: { [field in Field]?: Token }
currencies: { [field in Field]?: Currency }
noLiquidity?: boolean
poolTokenPercentage?: Percent
price?: Fraction
}) => {
price?: Price
}) {
const theme = useContext(ThemeContext)
return (
<AutoColumn gap="md">
<AutoRow justify="space-around" gap="4px">
<AutoColumn justify="center">
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
<TYPE.black>{price?.toSignificant(6) ?? '-'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{tokens[Field.TOKEN_B]?.symbol} per {tokens[Field.TOKEN_A]?.symbol}
{currencies[Field.CURRENCY_B]?.symbol} per {currencies[Field.CURRENCY_A]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">
<TYPE.black>{price?.invert().toSignificant(6) ?? '0'}</TYPE.black>
<TYPE.black>{price?.invert()?.toSignificant(6) ?? '-'}</TYPE.black>
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
{tokens[Field.TOKEN_A]?.symbol} per {tokens[Field.TOKEN_B]?.symbol}
{currencies[Field.CURRENCY_A]?.symbol} per {currencies[Field.CURRENCY_B]?.symbol}
</Text>
</AutoColumn>
<AutoColumn justify="center">

View File

@@ -1,13 +0,0 @@
import { Token, ChainId, WETH } from '@uniswap/sdk'
export function currencyId(...args: [ChainId | undefined, string] | [Token]): string {
if (args.length === 2) {
const [chainId, tokenAddress] = args
return chainId && tokenAddress === WETH[chainId].address ? 'ETH' : tokenAddress
} else if (args.length === 1) {
const [token] = args
return currencyId(token.chainId, token.address)
} else {
throw new Error('unexpected call signature')
}
}

View File

@@ -1,6 +1,6 @@
import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers'
import { ChainId, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Currency, currencyEquals, ETHER, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useState } from 'react'
import { Plus } from 'react-feather'
import ReactGA from 'react-ga'
@@ -10,16 +10,17 @@ import { ThemeContext } from 'styled-components'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
import { BlueCard, GreyCard, LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleLogo from '../../components/DoubleLogo'
import DoubleCurrencyLogo from '../../components/DoubleLogo'
import { AddRemoveTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row, { RowBetween, RowFlat } from '../../components/Row'
import { ROUTER_ADDRESS } from '../../constants'
import { PairState } from '../../data/Reserves'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { useCurrency } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/mint/actions'
@@ -30,18 +31,13 @@ import { useIsExpertMode, useUserDeadline, useUserSlippageTolerance } from '../.
import { TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { wrappedCurrency } from '../../utils/wrappedCurrency'
import AppBody from '../AppBody'
import { Dots, Wrapper } from '../Pool/styleds'
import { ConfirmAddModalBottom } from './ConfirmAddModalBottom'
import { currencyId } from './currencyId'
import { currencyId } from '../../utils/currencyId'
import { PoolPriceBar } from './PoolPriceBar'
function useTokenByCurrencyId(chainId: ChainId | undefined, currencyId: string | undefined): Token | undefined {
const isETH = currencyId?.toUpperCase() === 'ETH'
const token = useToken(isETH ? undefined : currencyId)
return isETH && chainId ? WETH[chainId] : token ?? undefined
}
export default function AddLiquidity({
match: {
params: { currencyIdA, currencyIdB }
@@ -51,11 +47,16 @@ export default function AddLiquidity({
const { account, chainId, library } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const tokenA = useTokenByCurrencyId(chainId, currencyIdA)
const tokenB = useTokenByCurrencyId(chainId, currencyIdB)
const currencyA = useCurrency(currencyIdA)
const currencyB = useCurrency(currencyIdB)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(currencyA, WETH[chainId])) ||
(currencyB && currencyEquals(currencyB, WETH[chainId])))
)
const toggleWalletModal = useWalletModalToggle() // toggle wallet when disconnected
const expertMode = useIsExpertMode()
@@ -63,30 +64,18 @@ export default function AddLiquidity({
const { independentField, typedValue, otherTypedValue } = useMintState()
const {
dependentField,
tokens,
currencies,
pair,
tokenBalances,
pairState,
currencyBalances,
parsedAmounts,
price,
noLiquidity,
liquidityMinted,
poolTokenPercentage,
error
} = useDerivedMintInfo(tokenA ?? undefined, tokenB ?? undefined)
const { onUserInput } = useMintActionHandlers(noLiquidity)
const handleTokenAInput = useCallback(
(field: string, value: string) => {
return onUserInput(Field.TOKEN_A, value)
},
[onUserInput]
)
const handleTokenBInput = useCallback(
(field: string, value: string) => {
return onUserInput(Field.TOKEN_B, value)
},
[onUserInput]
)
} = useDerivedMintInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onFieldAInput, onFieldBInput } = useMintActionHandlers(noLiquidity)
const isValid = !error
@@ -106,14 +95,17 @@ export default function AddLiquidity({
}
// get the max amounts user can add
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce((accumulator, field) => {
return {
...accumulator,
[field]: maxAmountSpend(tokenBalances[field])
}
}, {})
const maxAmounts: { [field in Field]?: TokenAmount } = [Field.CURRENCY_A, Field.CURRENCY_B].reduce(
(accumulator, field) => {
return {
...accumulator,
[field]: maxAmountSpend(currencyBalances[field])
}
},
{}
)
const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.TOKEN_A, Field.TOKEN_B].reduce(
const atMaxAmounts: { [field in Field]?: TokenAmount } = [Field.CURRENCY_A, Field.CURRENCY_B].reduce(
(accumulator, field) => {
return {
...accumulator,
@@ -124,8 +116,8 @@ export default function AddLiquidity({
)
// check whether the user has approved the router on the tokens
const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.TOKEN_A], ROUTER_ADDRESS)
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.TOKEN_B], ROUTER_ADDRESS)
const [approvalA, approveACallback] = useApproveCallback(parsedAmounts[Field.CURRENCY_A], ROUTER_ADDRESS)
const [approvalB, approveBCallback] = useApproveCallback(parsedAmounts[Field.CURRENCY_B], ROUTER_ADDRESS)
const addTransaction = useTransactionAdder()
@@ -133,14 +125,14 @@ export default function AddLiquidity({
if (!chainId || !library || !account) return
const router = getRouterContract(chainId, library, account)
const { [Field.TOKEN_A]: parsedAmountA, [Field.TOKEN_B]: parsedAmountB } = parsedAmounts
if (!parsedAmountA || !parsedAmountB || !tokenA || !tokenB) {
const { [Field.CURRENCY_A]: parsedAmountA, [Field.CURRENCY_B]: parsedAmountB } = parsedAmounts
if (!parsedAmountA || !parsedAmountB || !currencyA || !currencyB) {
return
}
const amountsMin = {
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmountB, noLiquidity ? 0 : allowedSlippage)[0]
[Field.CURRENCY_A]: calculateSlippageAmount(parsedAmountA, noLiquidity ? 0 : allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(parsedAmountB, noLiquidity ? 0 : allowedSlippage)[0]
}
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
@@ -149,15 +141,15 @@ export default function AddLiquidity({
method: (...args: any) => Promise<TransactionResponse>,
args: Array<string | string[] | number>,
value: BigNumber | null
if (tokenA.equals(WETH[chainId]) || tokenB.equals(WETH[chainId])) {
const tokenBIsETH = tokenB.equals(WETH[chainId])
if (currencyA === ETHER || currencyB === ETHER) {
const tokenBIsETH = currencyB === ETHER
estimate = router.estimateGas.addLiquidityETH
method = router.addLiquidityETH
args = [
(tokenBIsETH ? tokenA : tokenB).address, // token
wrappedCurrency(tokenBIsETH ? currencyA : currencyB, chainId)?.address ?? '', // token
(tokenBIsETH ? parsedAmountA : parsedAmountB).raw.toString(), // token desired
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(), // token min
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(), // eth min
amountsMin[tokenBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(), // token min
amountsMin[tokenBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(), // eth min
account,
deadlineFromNow
]
@@ -166,12 +158,12 @@ export default function AddLiquidity({
estimate = router.estimateGas.addLiquidity
method = router.addLiquidity
args = [
tokenA.address,
tokenB.address,
wrappedCurrency(currencyA, chainId)?.address ?? '',
wrappedCurrency(currencyB, chainId)?.address ?? '',
parsedAmountA.raw.toString(),
parsedAmountB.raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow
]
@@ -190,13 +182,13 @@ export default function AddLiquidity({
addTransaction(response, {
summary:
'Add ' +
parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_A]?.symbol +
currencies[Field.CURRENCY_A]?.symbol +
' and ' +
parsedAmounts[Field.TOKEN_B]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_B]?.symbol
currencies[Field.CURRENCY_B]?.symbol
})
setTxHash(response.hash)
@@ -204,7 +196,7 @@ export default function AddLiquidity({
ReactGA.event({
category: 'Liquidity',
action: 'Add',
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
label: [currencies[Field.CURRENCY_A]?.symbol, currencies[Field.CURRENCY_B]?.symbol].join('/')
})
})
)
@@ -223,9 +215,13 @@ export default function AddLiquidity({
<LightCard mt="20px" borderRadius="20px">
<RowFlat>
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
{tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol}
{currencies[Field.CURRENCY_A]?.symbol + '/' + currencies[Field.CURRENCY_B]?.symbol}
</Text>
<DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} />
<DoubleCurrencyLogo
currency0={currencies[Field.CURRENCY_A]}
currency1={currencies[Field.CURRENCY_B]}
size={30}
/>
</RowFlat>
</LightCard>
</AutoColumn>
@@ -235,11 +231,15 @@ export default function AddLiquidity({
<Text fontSize="48px" fontWeight={500} lineHeight="42px" marginRight={10}>
{liquidityMinted?.toSignificant(6)}
</Text>
<DoubleLogo a0={tokens[Field.TOKEN_A]?.address} a1={tokens[Field.TOKEN_B]?.address} size={30} />
<DoubleCurrencyLogo
currency0={currencies[Field.CURRENCY_A]}
currency1={currencies[Field.CURRENCY_B]}
size={30}
/>
</RowFlat>
<Row>
<Text fontSize="24px">
{tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol + ' Pool Tokens'}
{currencies[Field.CURRENCY_A]?.symbol + '/' + currencies[Field.CURRENCY_B]?.symbol + ' Pool Tokens'}
</Text>
</Row>
<TYPE.italic fontSize={12} textAlign="left" padding={'8px 0 0 0 '}>
@@ -254,7 +254,7 @@ export default function AddLiquidity({
return (
<ConfirmAddModalBottom
price={price}
tokens={tokens}
currencies={currencies}
parsedAmounts={parsedAmounts}
noLiquidity={noLiquidity}
onAdd={onAdd}
@@ -263,60 +263,65 @@ export default function AddLiquidity({
)
}
const pendingText = `Supplying ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${
tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
const pendingText = `Supplying ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencies[Field.CURRENCY_A]?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencies[Field.CURRENCY_B]?.symbol}`
const handleTokenASelect = useCallback(
(tokenAddress: string) => {
const [tokenAId, tokenBId] = [
currencyId(chainId, tokenAddress),
tokenB ? currencyId(chainId, tokenB.address) : undefined
]
if (tokenAId === tokenBId) {
history.push(`/add/${tokenAId}/${tokenA ? currencyId(chainId, tokenA.address) : ''}`)
const handleCurrencyASelect = useCallback(
(currencyA: Currency) => {
const newCurrencyIdA = currencyId(currencyA)
if (newCurrencyIdA === currencyIdB) {
history.push(`/add/${currencyIdB}/${currencyIdA}`)
} else {
history.push(`/add/${tokenAId}/${tokenBId}`)
history.push(`/add/${newCurrencyIdA}/${currencyIdB}`)
}
},
[chainId, tokenB, history, tokenA]
[currencyIdB, history, currencyIdA]
)
const handleTokenBSelect = useCallback(
(tokenAddress: string) => {
const [tokenAId, tokenBId] = [
tokenA ? currencyId(chainId, tokenA.address) : undefined,
currencyId(chainId, tokenAddress)
]
if (tokenAId === tokenBId) {
history.push(`/add/${tokenB ? currencyId(chainId, tokenB.address) : ''}/${tokenAId}`)
const handleCurrencyBSelect = useCallback(
(currencyB: Currency) => {
const newCurrencyIdB = currencyId(currencyB)
if (currencyIdA === newCurrencyIdB) {
if (currencyIdB) {
history.push(`/add/${currencyIdB}/${newCurrencyIdB}`)
} else {
history.push(`/add/${newCurrencyIdB}`)
}
} else {
history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${currencyId(chainId, tokenAddress)}`)
history.push(`/add/${currencyIdA ? currencyIdA : 'ETH'}/${newCurrencyIdB}`)
}
},
[tokenA, chainId, history, tokenB, currencyIdA]
[currencyIdA, history, currencyIdB]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onFieldAInput('')
}
setTxHash('')
}, [onFieldAInput, txHash])
return (
<>
<AppBody>
<AddRemoveTabs adding={true} />
<Wrapper>
<ConfirmationModal
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.TOKEN_A, '')
}
setTxHash('')
}}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash}
topContent={() => modalHeader()}
bottomContent={modalBottom}
content={() => (
<ConfirmationModalContent
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
title={noLiquidity ? 'You are creating a pool' : 'You will receive'}
/>
<AutoColumn gap="20px">
{noLiquidity && (
@@ -337,16 +342,14 @@ export default function AddLiquidity({
</ColumnCenter>
)}
<CurrencyInputPanel
field={Field.TOKEN_A}
value={formattedAmounts[Field.TOKEN_A]}
onUserInput={handleTokenAInput}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onFieldAInput}
onMax={() => {
onUserInput(Field.TOKEN_A, maxAmounts[Field.TOKEN_A]?.toExact() ?? '')
onFieldAInput(maxAmounts[Field.CURRENCY_A]?.toExact() ?? '')
}}
onTokenSelection={handleTokenASelect}
showMaxButton={!atMaxAmounts[Field.TOKEN_A]}
token={tokens[Field.TOKEN_A]}
pair={pair}
onCurrencySelect={handleCurrencyASelect}
showMaxButton={!atMaxAmounts[Field.CURRENCY_A]}
currency={currencies[Field.CURRENCY_A]}
id="add-liquidity-input-tokena"
showCommonBases
/>
@@ -354,20 +357,18 @@ export default function AddLiquidity({
<Plus size="16" color={theme.text2} />
</ColumnCenter>
<CurrencyInputPanel
field={Field.TOKEN_B}
value={formattedAmounts[Field.TOKEN_B]}
onUserInput={handleTokenBInput}
onTokenSelection={handleTokenBSelect}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onFieldBInput}
onCurrencySelect={handleCurrencyBSelect}
onMax={() => {
onUserInput(Field.TOKEN_B, maxAmounts[Field.TOKEN_B]?.toExact() ?? '')
onFieldBInput(maxAmounts[Field.CURRENCY_B]?.toExact() ?? '')
}}
showMaxButton={!atMaxAmounts[Field.TOKEN_B]}
token={tokens[Field.TOKEN_B]}
pair={pair}
showMaxButton={!atMaxAmounts[Field.CURRENCY_B]}
currency={currencies[Field.CURRENCY_B]}
id="add-liquidity-input-tokenb"
showCommonBases
/>
{tokens[Field.TOKEN_A] && tokens[Field.TOKEN_B] && (
{currencies[Field.CURRENCY_A] && currencies[Field.CURRENCY_B] && pairState !== PairState.INVALID && (
<>
<GreyCard padding="0px" borderRadius={'20px'}>
<RowBetween padding="1rem">
@@ -377,7 +378,7 @@ export default function AddLiquidity({
</RowBetween>{' '}
<LightCard padding="1rem" borderRadius={'20px'}>
<PoolPriceBar
tokens={tokens}
currencies={currencies}
poolTokenPercentage={poolTokenPercentage}
noLiquidity={noLiquidity}
price={price}
@@ -404,9 +405,9 @@ export default function AddLiquidity({
width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'}
>
{approvalA === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
<Dots>Approving {currencies[Field.CURRENCY_A]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.TOKEN_A]?.symbol
'Approve ' + currencies[Field.CURRENCY_A]?.symbol
)}
</ButtonPrimary>
)}
@@ -417,9 +418,9 @@ export default function AddLiquidity({
width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'}
>
{approvalB === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
<Dots>Approving {currencies[Field.CURRENCY_B]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.TOKEN_B]?.symbol
'Approve ' + currencies[Field.CURRENCY_B]?.symbol
)}
</ButtonPrimary>
)}
@@ -430,7 +431,7 @@ export default function AddLiquidity({
expertMode ? onAdd() : setShowConfirm(true)
}}
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={20} fontWeight={500}>
{error ?? 'Supply'}
@@ -442,9 +443,9 @@ export default function AddLiquidity({
</Wrapper>
</AppBody>
{pair && !noLiquidity ? (
{pair && !noLiquidity && pairState !== PairState.INVALID ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard pair={pair} />
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>

View File

@@ -1,4 +1,3 @@
import { WETH } from '@uniswap/sdk'
import React from 'react'
import { Redirect, RouteComponentProps } from 'react-router-dom'
import AddLiquidity from './index'
@@ -7,13 +6,6 @@ export function RedirectToAddLiquidity() {
return <Redirect to="/add/" />
}
function convertToCurrencyIds(address: string): string {
if (Object.values(WETH).some(weth => weth.address === address)) {
return 'ETH'
}
return address
}
const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<{ currencyIdA: string }>) {
const {
@@ -23,7 +15,7 @@ export function RedirectOldAddLiquidityPathStructure(props: RouteComponentProps<
} = props
const match = currencyIdA.match(OLD_PATH_STRUCTURE)
if (match?.length) {
return <Redirect to={`/add/${convertToCurrencyIds(match[1])}/${convertToCurrencyIds(match[2])}`} />
return <Redirect to={`/add/${match[1]}/${match[2]}`} />
}
return <AddLiquidity {...props} />

View File

@@ -18,6 +18,7 @@ import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
import Pool from './Pool'
import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity'
import { RedirectOldRemoveLiquidityPathStructure } from './RemoveLiquidity/redirects'
import Swap from './Swap'
import { RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
@@ -79,10 +80,11 @@ export default function App() {
<Route exact path="/add" component={AddLiquidity} />
<Route exact path="/add/:currencyIdA" component={RedirectOldAddLiquidityPathStructure} />
<Route exact path="/add/:currencyIdA/:currencyIdB" component={RedirectDuplicateTokenIds} />
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
<Route exact strict path="/remove/:tokens" component={RedirectOldRemoveLiquidityPathStructure} />
<Route exact strict path="/remove/:currencyIdA/:currencyIdB" component={RemoveLiquidity} />
<Route exact strict path="/migrate/v1" component={MigrateV1} />
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Web3ReactManager>

View File

@@ -1,7 +1,7 @@
import React from 'react'
import styled from 'styled-components'
export const BodyWrapper = styled.div`
export const BodyWrapper = styled.div<{ disabled?: boolean }>`
position: relative;
max-width: 420px;
width: 100%;
@@ -10,11 +10,13 @@ export const BodyWrapper = styled.div`
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 30px;
padding: 1rem;
opacity: ${({ disabled }) => (disabled ? '0.4' : '1')};
pointer-events: ${({ disabled }) => disabled && 'none'};
`
/**
* The styled container element that wraps the content of most pages and the tabs.
*/
export default function AppBody({ children }: { children: React.ReactNode }) {
return <BodyWrapper>{children}</BodyWrapper>
export default function AppBody({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) {
return <BodyWrapper disabled={disabled}>{children}</BodyWrapper>
}

View File

@@ -1,17 +1,20 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { AddressZero } from '@ethersproject/constants'
import { Currency, CurrencyAmount, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import ReactGA from 'react-ga'
import { Redirect, RouteComponentProps } from 'react-router'
import { Text } from 'rebass'
import { ButtonConfirmed } from '../../components/Button'
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
import { LightCard, PinkCard, YellowCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import CurrencyLogo from '../../components/CurrencyLogo'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
import { Dots } from '../../components/swap/styleds'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
import { usePair } from '../../data/Reserves'
import { PairState, usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
@@ -20,29 +23,26 @@ import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useCon
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
import { TYPE, ExternalLink, BackArrow } from '../../theme'
import { isAddress, getEtherscanLink } from '../../utils'
import { BackArrow, ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink, isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
import TokenLogo from '../../components/TokenLogo'
import { AddressZero } from '@ethersproject/constants'
import { Text } from 'rebass'
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
const POOL_CURRENCY_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
const ZERO = JSBI.BigInt(0)
const ONE = JSBI.BigInt(1)
const ZERO_FRACTION = new Fraction(ZERO, ONE)
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
function FormattedPoolCurrencyAmount({ currencyAmount }: { currencyAmount: CurrencyAmount }) {
return (
<>
{tokenAmount.equalTo(JSBI.BigInt(0))
{currencyAmount.equalTo(JSBI.BigInt(0))
? '0'
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
? tokenAmount.toSignificant(4)
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
: currencyAmount.greaterThan(POOL_CURRENCY_AMOUNT_MIN)
? currencyAmount.toSignificant(4)
: `<${POOL_CURRENCY_AMOUNT_MIN.toSignificant(1)}`}
</>
)
}
@@ -56,17 +56,17 @@ export function V1LiquidityInfo({
token: Token
liquidityTokenAmount: TokenAmount
tokenWorth: TokenAmount
ethWorth: Fraction
ethWorth: CurrencyAmount
}) {
const { chainId } = useActiveWeb3React()
return (
<>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="24px" address={token.address} />
<CurrencyLogo size="24px" currency={token} />
<div style={{ marginLeft: '.75rem' }}>
<TYPE.mediumHeader>
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
{<FormattedPoolCurrencyAmount currencyAmount={liquidityTokenAmount} />}{' '}
{token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH
</TYPE.mediumHeader>
</div>
@@ -80,7 +80,7 @@ export function V1LiquidityInfo({
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{tokenWorth.toSignificant(4)}
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token.address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={token} />
</RowFixed>
</RowBetween>
<RowBetween mb="1rem">
@@ -89,9 +89,9 @@ export function V1LiquidityInfo({
</Text>
<RowFixed>
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
{ethWorth.toSignificant(4)}
<FormattedPoolCurrencyAmount currencyAmount={ethWorth} />
</Text>
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={WETH[chainId].address} />
<CurrencyLogo size="20px" style={{ marginLeft: '8px' }} currency={Currency.ETHER} />
</RowFixed>
</RowBetween>
</>
@@ -104,19 +104,19 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
const v2Pair = usePair(WETH[chainId as ChainId], token)
const isFirstLiquidityProvider: boolean = v2Pair === null
const [v2PairState, v2Pair] = usePair(chainId ? WETH[chainId] : undefined, token)
const isFirstLiquidityProvider: boolean = v2PairState === PairState.NOT_EXISTS
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId as ChainId]))
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId]))
const [confirmingMigration, setConfirmingMigration] = useState<boolean>(false)
const [pendingMigrationHash, setPendingMigrationHash] = useState<string | null>(null)
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
const ethWorth: Fraction = exchangeETHBalance
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
: ZERO_FRACTION
const ethWorth: CurrencyAmount = exchangeETHBalance
? CurrencyAmount.ether(exchangeETHBalance.multiply(shareFraction).multiply(WEI_DENOM).quotient)
: CurrencyAmount.ether(ZERO)
const tokenWorth: TokenAmount = exchangeTokenBalance
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
@@ -126,7 +126,7 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
const v1SpotPrice =
exchangeTokenBalance && exchangeETHBalance
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance, WEI_DENOM))
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance.raw, WEI_DENOM))
: null
const priceDifferenceFraction: Fraction | undefined =

View File

@@ -1,5 +1,5 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { JSBI, Token, TokenAmount, WETH, Fraction, Percent } from '@uniswap/sdk'
import { JSBI, Token, TokenAmount, WETH, Fraction, Percent, CurrencyAmount } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import ReactGA from 'react-ga'
import { Redirect, RouteComponentProps } from 'react-router'
@@ -49,9 +49,9 @@ function V1PairRemoval({
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
const ethWorth: Fraction = exchangeETHBalance
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
: ZERO_FRACTION
const ethWorth: CurrencyAmount = exchangeETHBalance
? CurrencyAmount.ether(exchangeETHBalance.multiply(shareFraction).multiply(WEI_DENOM).quotient)
: CurrencyAmount.ether(ZERO)
const tokenWorth: TokenAmount = exchangeTokenBalance
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
@@ -154,7 +154,7 @@ export default function RemoveV1Exchange({
}
return (
<BodyWrapper style={{ padding: 24 }}>
<BodyWrapper style={{ padding: 24 }} id="remove-v1-exchange">
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<BackArrow to="/migrate/v1" />

View File

@@ -6,7 +6,8 @@ import { AutoRow } from '../../components/Row'
import { SearchInput } from '../../components/SearchModal/styleds'
import { useAllTokenV1Exchanges } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useToken, useAllTokens } from '../../hooks/Tokens'
import { useAllTokens, useToken } from '../../hooks/Tokens'
import { useDefaultTokenList } from '../../state/lists/hooks'
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
import { BackArrow, TYPE } from '../../theme'
import { LightCard } from '../../components/Card'
@@ -16,7 +17,7 @@ import V1PositionCard from '../../components/PositionCard/V1'
import QuestionHelper from '../../components/QuestionHelper'
import { Dots } from '../../components/swap/styleds'
import { useAddUserToken } from '../../state/user/hooks'
import { isDefaultToken, isCustomAddedToken } from '../../utils'
import { isDefaultToken } from '../../utils'
export default function MigrateV1() {
const theme = useContext(ThemeContext)
@@ -27,15 +28,15 @@ export default function MigrateV1() {
// automatically add the search token
const token = useToken(tokenSearch)
const isDefault = isDefaultToken(token)
const defaultTokens = useDefaultTokenList()
const isDefault = isDefaultToken(defaultTokens, token)
const allTokens = useAllTokens()
const isCustomAdded = isCustomAddedToken(allTokens, token)
const addToken = useAddUserToken()
useEffect(() => {
if (token && !isDefault && !isCustomAdded) {
if (token && !isDefault && !allTokens[token.address]) {
addToken(token)
}
}, [token, isDefault, isCustomAdded, addToken])
}, [token, isDefault, addToken, allTokens])
// get V1 LP balances
const V1Exchanges = useAllTokenV1Exchanges()

View File

@@ -1,4 +1,4 @@
import React, { useContext } from 'react'
import React, { useContext, useMemo } from 'react'
import { ThemeContext } from 'styled-components'
import { Pair } from '@uniswap/sdk'
import { Link } from 'react-router-dom'
@@ -17,7 +17,7 @@ import { AutoColumn } from '../../components/Column'
import { useActiveWeb3React } from '../../hooks'
import { usePairs } from '../../data/Reserves'
import { useAllDummyPairs } from '../../state/user/hooks'
import { toV2LiquidityToken, useTrackedTokenPairs } from '../../state/user/hooks'
import AppBody from '../AppBody'
import { Dots } from '../../components/swap/styleds'
@@ -26,27 +26,33 @@ export default function Pool() {
const { account } = useActiveWeb3React()
// fetch the user's balances of all tracked V2 LP tokens
const v2DummyPairs = useAllDummyPairs()
const trackedTokenPairs = useTrackedTokenPairs()
const tokenPairsWithLiquidityTokens = useMemo(
() => trackedTokenPairs.map(tokens => ({ liquidityToken: toV2LiquidityToken(tokens), tokens })),
[trackedTokenPairs]
)
const liquidityTokens = useMemo(() => tokenPairsWithLiquidityTokens.map(tpwlt => tpwlt.liquidityToken), [
tokenPairsWithLiquidityTokens
])
const [v2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
account ?? undefined,
v2DummyPairs?.map(p => p.liquidityToken)
liquidityTokens
)
// fetch the reserves for all V2 pools in which the user has a balance
const v2DummyPairsWithABalance = v2DummyPairs.filter(dummyPair =>
v2PairsBalances[dummyPair.liquidityToken.address]?.greaterThan('0')
)
const v2Pairs = usePairs(
v2DummyPairsWithABalance.map(V2DummyPairWithABalance => [
V2DummyPairWithABalance.token0,
V2DummyPairWithABalance.token1
])
)
const v2IsLoading =
fetchingV2PairBalances || v2Pairs?.length < v2DummyPairsWithABalance.length || v2Pairs?.some(V2Pair => !V2Pair)
const allV2PairsWithLiquidity = v2Pairs
.filter((v2Pair): v2Pair is Pair => Boolean(v2Pair))
.map(V2Pair => <FullPositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />)
// fetch the reserves for all V2 pools in which the user has a balance
const liquidityTokensWithBalances = useMemo(
() =>
tokenPairsWithLiquidityTokens.filter(({ liquidityToken }) =>
v2PairsBalances[liquidityToken.address]?.greaterThan('0')
),
[tokenPairsWithLiquidityTokens, v2PairsBalances]
)
const v2Pairs = usePairs(liquidityTokensWithBalances.map(({ tokens }) => tokens))
const v2IsLoading =
fetchingV2PairBalances || v2Pairs?.length < liquidityTokensWithBalances.length || v2Pairs?.some(V2Pair => !V2Pair)
const allV2PairsWithLiquidity = v2Pairs.map(([, pair]) => pair).filter((v2Pair): v2Pair is Pair => Boolean(v2Pair))
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
@@ -82,7 +88,11 @@ export default function Pool() {
</TYPE.body>
</LightCard>
) : allV2PairsWithLiquidity?.length > 0 ? (
<>{allV2PairsWithLiquidity}</>
<>
{allV2PairsWithLiquidity.map(v2Pair => (
<FullPositionCard key={v2Pair.liquidityToken.address} pair={v2Pair} />
))}
</>
) : (
<LightCard padding="40px">
<TYPE.body color={theme.text3} textAlign="center">

View File

@@ -1,22 +1,23 @@
import { JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Currency, ETHER, JSBI, TokenAmount } from '@uniswap/sdk'
import React, { useCallback, useEffect, useState } from 'react'
import { Plus } from 'react-feather'
import { Text } from 'rebass'
import { ButtonDropdownLight } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import CurrencyLogo from '../../components/CurrencyLogo'
import { FindPoolTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row from '../../components/Row'
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
import TokenLogo from '../../components/TokenLogo'
import { usePair } from '../../data/Reserves'
import CurrencySearchModal from '../../components/SearchModal/CurrencySearchModal'
import { PairState, usePair } from '../../data/Reserves'
import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { usePairAdder } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { useTokenBalance } from '../../state/wallet/hooks'
import { StyledInternalLink } from '../../theme'
import { currencyId } from '../../utils/currencyId'
import AppBody from '../AppBody'
import { Dots } from '../Pool/styleds'
enum Fields {
TOKEN0 = 0,
@@ -24,17 +25,15 @@ enum Fields {
}
export default function PoolFinder() {
const { account, chainId } = useActiveWeb3React()
const { account } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN1)
const [token0Address, setToken0Address] = useState<string>(chainId ? WETH[chainId].address : '')
const [token1Address, setToken1Address] = useState<string>()
const token0: Token | null | undefined = useToken(token0Address)
const token1: Token | null | undefined = useToken(token1Address)
const [currency0, setCurrency0] = useState<Currency | null>(ETHER)
const [currency1, setCurrency1] = useState<Currency | null>(null)
const pair: Pair | null | undefined = usePair(token0 ?? undefined, token1 ?? undefined)
const [pairState, pair] = usePair(currency0 ?? undefined, currency1 ?? undefined)
const addPair = usePairAdder()
useEffect(() => {
if (pair) {
@@ -42,16 +41,25 @@ export default function PoolFinder() {
}
}, [pair, addPair])
const newPair: boolean =
pair === null ||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
const validPairNoLiquidity: boolean =
pairState === PairState.NOT_EXISTS ||
Boolean(
pairState === PairState.EXISTS &&
pair &&
JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) &&
JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0))
)
const position: TokenAmount | undefined = useTokenBalanceTreatingWETHasETH(account ?? undefined, pair?.liquidityToken)
const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
const position: TokenAmount | undefined = useTokenBalance(account ?? undefined, pair?.liquidityToken)
const hasPosition = Boolean(position && JSBI.greaterThan(position.raw, JSBI.BigInt(0)))
const handleTokenSelect = useCallback(
(address: string) => {
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
const handleCurrencySelect = useCallback(
(currency: Currency) => {
if (activeField === Fields.TOKEN0) {
setCurrency0(currency)
} else {
setCurrency1(currency)
}
},
[activeField]
)
@@ -60,6 +68,14 @@ export default function PoolFinder() {
setShowSearch(false)
}, [setShowSearch])
const prerequisiteMessage = (
<LightCard padding="45px 10px">
<Text textAlign="center">
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
</Text>
</LightCard>
)
return (
<AppBody>
<FindPoolTabs />
@@ -70,11 +86,11 @@ export default function PoolFinder() {
setActiveField(Fields.TOKEN0)
}}
>
{token0 ? (
{currency0 ? (
<Row>
<TokenLogo address={token0Address} />
<CurrencyLogo currency={currency0} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token0.symbol}
{currency0.symbol}
</Text>
</Row>
) : (
@@ -94,11 +110,11 @@ export default function PoolFinder() {
setActiveField(Fields.TOKEN1)
}}
>
{token1 ? (
{currency1 ? (
<Row>
<TokenLogo address={token1Address} />
<CurrencyLogo currency={currency1} />
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
{token1.symbol}
{currency1.symbol}
</Text>
</Row>
) : (
@@ -108,51 +124,68 @@ export default function PoolFinder() {
)}
</ButtonDropdownLight>
{poolImported && (
{hasPosition && (
<ColumnCenter
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
>
<Text textAlign="center" fontWeight={500} color="">
<Text textAlign="center" fontWeight={500}>
Pool Found!
</Text>
</ColumnCenter>
)}
{position ? (
poolImported ? (
<MinimalPositionCard pair={pair} border="1px solid #CED0D9" />
) : (
{currency0 && currency1 ? (
pairState === PairState.EXISTS ? (
hasPosition && pair ? (
<MinimalPositionCard pair={pair} border="1px solid #CED0D9" />
) : (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">You dont have liquidity in this pool yet.</Text>
<StyledInternalLink to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}>
<Text textAlign="center">Add liquidity.</Text>
</StyledInternalLink>
</AutoColumn>
</LightCard>
)
) : validPairNoLiquidity ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">You dont have liquidity in this pool yet.</Text>
<StyledInternalLink to={`/add/${token0?.address}/${token1?.address}`}>
<Text textAlign="center">Add liquidity.</Text>
<Text textAlign="center">No pool found.</Text>
<StyledInternalLink to={`/add/${currencyId(currency0)}/${currencyId(currency1)}`}>
Create pool.
</StyledInternalLink>
</AutoColumn>
</LightCard>
)
) : newPair ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">No pool found.</Text>
<StyledInternalLink to={`/add/${token0Address}/${token1Address}`}>Create pool?</StyledInternalLink>
</AutoColumn>
</LightCard>
) : pairState === PairState.INVALID ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center" fontWeight={500}>
Invalid pair.
</Text>
</AutoColumn>
</LightCard>
) : pairState === PairState.LOADING ? (
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">
Loading
<Dots />
</Text>
</AutoColumn>
</LightCard>
) : null
) : (
<LightCard padding="45px 10px">
<Text textAlign="center">
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
</Text>
</LightCard>
prerequisiteMessage
)}
</AutoColumn>
<TokenSearchModal
<CurrencySearchModal
isOpen={showSearch}
onTokenSelect={handleTokenSelect}
onCurrencySelect={handleCurrencySelect}
onDismiss={handleSearchDismiss}
showCommonBases
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
hiddenCurrency={(activeField === Fields.TOKEN0 ? currency1 : currency0) ?? undefined}
/>
</AppBody>
)

View File

@@ -1,7 +1,8 @@
import { splitSignature } from '@ethersproject/bytes'
import { Contract } from '@ethersproject/contracts'
import { Percent, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useState } from 'react'
import { TransactionResponse } from '@ethersproject/providers'
import { Currency, currencyEquals, ETHER, Percent, WETH } from '@uniswap/sdk'
import React, { useCallback, useContext, useMemo, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router'
@@ -10,37 +11,50 @@ import { ThemeContext } from 'styled-components'
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button'
import { LightCard } from '../../components/Card'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import TransactionConfirmationModal, { ConfirmationModalContent } from '../../components/TransactionConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import DoubleLogo from '../../components/DoubleLogo'
import DoubleCurrencyLogo from '../../components/DoubleLogo'
import { AddRemoveTabs } from '../../components/NavigationTabs'
import { MinimalPositionCard } from '../../components/PositionCard'
import Row, { RowBetween, RowFixed } from '../../components/Row'
import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo'
import CurrencyLogo from '../../components/CurrencyLogo'
import { ROUTER_ADDRESS } from '../../constants'
import { useActiveWeb3React } from '../../hooks'
import { useCurrency } from '../../hooks/Tokens'
import { usePairContract } from '../../hooks/useContract'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TYPE } from '../../theme'
import { StyledInternalLink, TYPE } from '../../theme'
import { calculateGasMargin, calculateSlippageAmount, getRouterContract } from '../../utils'
import { currencyId } from '../../utils/currencyId'
import { wrappedCurrency } from '../../utils/wrappedCurrency'
import AppBody from '../AppBody'
import { ClickableText, MaxButton, Wrapper } from '../Pool/styleds'
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
import { Dots } from '../../components/swap/styleds'
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
import { useBurnActionHandlers } from '../../state/burn/hooks'
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
import { Field } from '../../state/burn/actions'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
import { BigNumber } from '@ethersproject/bignumber'
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
export default function RemoveLiquidity({
history,
match: {
params: { currencyIdA, currencyIdB }
}
}: RouteComponentProps<{ currencyIdA: string; currencyIdB: string }>) {
const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined]
const { account, chainId, library } = useActiveWeb3React()
const [tokenA, tokenB] = useMemo(() => [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)], [
currencyA,
currencyB,
chainId
])
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
@@ -48,7 +62,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
// burn state
const { independentField, typedValue } = useBurnState()
const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
const { pair, parsedAmounts, error } = useDerivedBurnInfo(currencyA ?? undefined, currencyB ?? undefined)
const { onUserInput: _onUserInput } = useBurnActionHandlers()
const isValid = !error
@@ -70,23 +84,27 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
: parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0),
[Field.LIQUIDITY]:
independentField === Field.LIQUIDITY ? typedValue : parsedAmounts[Field.LIQUIDITY]?.toSignificant(6) ?? '',
[Field.TOKEN_A]:
independentField === Field.TOKEN_A ? typedValue : parsedAmounts[Field.TOKEN_A]?.toSignificant(6) ?? '',
[Field.TOKEN_B]:
independentField === Field.TOKEN_B ? typedValue : parsedAmounts[Field.TOKEN_B]?.toSignificant(6) ?? ''
[Field.CURRENCY_A]:
independentField === Field.CURRENCY_A ? typedValue : parsedAmounts[Field.CURRENCY_A]?.toSignificant(6) ?? '',
[Field.CURRENCY_B]:
independentField === Field.CURRENCY_B ? typedValue : parsedAmounts[Field.CURRENCY_B]?.toSignificant(6) ?? ''
}
const atMaxAmount = parsedAmounts[Field.LIQUIDITY_PERCENT]?.equalTo(new Percent('1'))
// pair contract
const pairContract: Contract = usePairContract(pair?.liquidityToken?.address)
const pairContract: Contract | null = usePairContract(pair?.liquidityToken?.address)
// allowance handling
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number }>(null)
const [signatureData, setSignatureData] = useState<{ v: number; r: string; s: string; deadline: number } | null>(null)
const [approval, approveCallback] = useApproveCallback(parsedAmounts[Field.LIQUIDITY], ROUTER_ADDRESS)
async function onAttemptToApprove() {
if (!pairContract || !pair || !library) throw new Error('missing dependencies')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
// try to gather a signature for permission
const nonce = await pairContract.nonces(account)
const deadlineForSignature: number = Math.ceil(Date.now() / 1000) + deadline
const EIP712Domain = [
@@ -111,7 +129,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
const message = {
owner: account,
spender: ROUTER_ADDRESS,
value: parsedAmounts[Field.LIQUIDITY].raw.toString(),
value: liquidityAmount.raw.toString(),
nonce: nonce.toHexString(),
deadline: deadlineForSignature
}
@@ -153,32 +171,52 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
[_onUserInput]
)
const onLiquidityInput = useCallback((typedValue: string): void => onUserInput(Field.LIQUIDITY, typedValue), [
onUserInput
])
const onCurrencyAInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_A, typedValue), [
onUserInput
])
const onCurrencyBInput = useCallback((typedValue: string): void => onUserInput(Field.CURRENCY_B, typedValue), [
onUserInput
])
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
if (!chainId || !library || !account) throw new Error('missing dependencies')
const { [Field.CURRENCY_A]: currencyAmountA, [Field.CURRENCY_B]: currencyAmountB } = parsedAmounts
if (!currencyAmountA || !currencyAmountB) {
throw new Error('missing currency amounts')
}
const router = getRouterContract(chainId, library, account)
const amountsMin = {
[Field.TOKEN_A]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_A], allowedSlippage)[0],
[Field.TOKEN_B]: calculateSlippageAmount(parsedAmounts[Field.TOKEN_B], allowedSlippage)[0]
[Field.CURRENCY_A]: calculateSlippageAmount(currencyAmountA, allowedSlippage)[0],
[Field.CURRENCY_B]: calculateSlippageAmount(currencyAmountB, allowedSlippage)[0]
}
const tokenBIsETH = tokens[Field.TOKEN_B].equals(WETH[chainId])
const oneTokenIsETH = tokens[Field.TOKEN_A].equals(WETH[chainId]) || tokenBIsETH
if (!currencyA || !currencyB) throw new Error('missing tokens')
const liquidityAmount = parsedAmounts[Field.LIQUIDITY]
if (!liquidityAmount) throw new Error('missing liquidity amount')
const currencyBIsETH = currencyB === ETHER
const oneCurrencyIsETH = currencyA === ETHER || currencyBIsETH
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
if (!tokenA || !tokenB) throw new Error('could not wrap')
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneTokenIsETH) {
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
deadlineFromNow
]
@@ -187,11 +225,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
else {
methodNames = ['removeLiquidity']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
deadlineFromNow
]
@@ -200,13 +238,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
// we have a signataure, use permit versions of remove liquidity
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneTokenIsETH) {
if (oneCurrencyIsETH) {
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].toString(),
amountsMin[tokenBIsETH ? Field.TOKEN_B : Field.TOKEN_A].toString(),
currencyBIsETH ? tokenA.address : tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_A : Field.CURRENCY_B].toString(),
amountsMin[currencyBIsETH ? Field.CURRENCY_B : Field.CURRENCY_A].toString(),
account,
signatureData.deadline,
false,
@@ -219,11 +257,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
else {
methodNames = ['removeLiquidityWithPermit']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
amountsMin[Field.TOKEN_A].toString(),
amountsMin[Field.TOKEN_B].toString(),
tokenA.address,
tokenB.address,
liquidityAmount.raw.toString(),
amountsMin[Field.CURRENCY_A].toString(),
amountsMin[Field.CURRENCY_B].toString(),
account,
signatureData.deadline,
false,
@@ -233,15 +271,16 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
]
}
} else {
console.error('Attempting to confirm without approval or a signature. Please contact support.')
throw new Error('Attempting to confirm without approval or a signature. Please contact support.')
}
const safeGasEstimates = await Promise.all(
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
console.error(`estimateGas failed`, methodName, args, error)
return undefined
})
)
)
@@ -261,19 +300,19 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then(response => {
.then((response: TransactionResponse) => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
parsedAmounts[Field.TOKEN_A]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_A]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_A]?.symbol +
currencyA?.symbol +
' and ' +
parsedAmounts[Field.TOKEN_B]?.toSignificant(3) +
parsedAmounts[Field.CURRENCY_B]?.toSignificant(3) +
' ' +
tokens[Field.TOKEN_B]?.symbol
currencyB?.symbol
})
setTxHash(response.hash)
@@ -281,15 +320,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
ReactGA.event({
category: 'Liquidity',
action: 'Remove',
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
label: [currencyA?.symbol, currencyB?.symbol].join('/')
})
})
.catch(error => {
.catch((error: Error) => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
console.error(error)
})
}
}
@@ -299,12 +336,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.TOKEN_A]?.toSignificant(6)}
{parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.TOKEN_A]?.address} size={'24px'} />
<CurrencyLogo currency={currencyA} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.TOKEN_A]?.symbol}
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
@@ -313,12 +350,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</RowFixed>
<RowBetween align="flex-end">
<Text fontSize={24} fontWeight={500}>
{parsedAmounts[Field.TOKEN_B]?.toSignificant(6)}
{parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)}
</Text>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.TOKEN_B]?.address} size={'24px'} />
<CurrencyLogo currency={currencyB} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.TOKEN_B]?.symbol}
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
@@ -336,34 +373,29 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
{'UNI ' + tokens[Field.TOKEN_A]?.symbol + '/' + tokens[Field.TOKEN_B]?.symbol} Burned
{'UNI ' + currencyA?.symbol + '/' + currencyB?.symbol} Burned
</Text>
<RowFixed>
<DoubleLogo
a0={tokens[Field.TOKEN_A]?.address || ''}
a1={tokens[Field.TOKEN_B]?.address || ''}
margin={true}
/>
<DoubleCurrencyLogo currency0={currencyA} currency1={currencyB} margin={true} />
<Text fontWeight={500} fontSize={16}>
{parsedAmounts[Field.LIQUIDITY]?.toSignificant(6)}
</Text>
</RowFixed>
</RowBetween>
{route && (
{pair && (
<>
<RowBetween>
<Text color={theme.text2} fontWeight={500} fontSize={16}>
Price
</Text>
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)} {tokens[Field.TOKEN_B]?.symbol}
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</Text>
</RowBetween>
<RowBetween>
<div />
<Text fontWeight={500} fontSize={16} color={theme.text1}>
1 {tokens[Field.TOKEN_B]?.symbol} = {route.midPrice.invert().toSignificant(6)}{' '}
{tokens[Field.TOKEN_A]?.symbol}
1 {currencyB?.symbol} = {tokenB ? pair.priceOf(tokenB).toSignificant(6) : '-'} {currencyA?.symbol}
</Text>
</RowBetween>
</>
@@ -377,9 +409,9 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
)
}
const pendingText = `Removing ${parsedAmounts[Field.TOKEN_A]?.toSignificant(6)} ${
tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
const pendingText = `Removing ${parsedAmounts[Field.CURRENCY_A]?.toSignificant(6)} ${
currencyA?.symbol
} and ${parsedAmounts[Field.CURRENCY_B]?.toSignificant(6)} ${currencyB?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
@@ -388,28 +420,63 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
[onUserInput]
)
const oneCurrencyIsETH = currencyA === ETHER || currencyB === ETHER
const oneCurrencyIsWETH = Boolean(
chainId &&
((currencyA && currencyEquals(WETH[chainId], currencyA)) ||
(currencyB && currencyEquals(WETH[chainId], currencyB)))
)
const handleSelectCurrencyA = useCallback(
(currency: Currency) => {
if (currencyIdB && currencyId(currency) === currencyIdB) {
history.push(`/remove/${currencyId(currency)}/${currencyIdA}`)
} else {
history.push(`/remove/${currencyId(currency)}/${currencyIdB}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleSelectCurrencyB = useCallback(
(currency: Currency) => {
if (currencyIdA && currencyId(currency) === currencyIdA) {
history.push(`/remove/${currencyIdB}/${currencyId(currency)}`)
} else {
history.push(`/remove/${currencyIdA}/${currencyId(currency)}`)
}
},
[currencyIdA, currencyIdB, history]
)
const handleDismissConfirmation = useCallback(() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}, [onUserInput, txHash])
return (
<>
<AppBody>
<AddRemoveTabs adding={false} />
<Wrapper>
<ConfirmationModal
<TransactionConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
setShowConfirm(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}}
onDismiss={handleDismissConfirmation}
attemptingTxn={attemptingTxn}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
content={() => (
<ConfirmationModalContent
title={'You will receive'}
onDismiss={handleDismissConfirmation}
topContent={modalHeader}
bottomContent={modalBottom}
/>
)}
pendingText={pendingText}
title="You will receive"
/>
<AutoColumn gap="md">
<LightCard>
@@ -463,26 +530,47 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<AutoColumn gap="10px">
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN_A] || '-'}
{formattedAmounts[Field.CURRENCY_A] || '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_A]?.address} style={{ marginRight: '12px' }} />
<CurrencyLogo currency={currencyA} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokena-symbol">
{tokens[Field.TOKEN_A]?.symbol}
{currencyA?.symbol}
</Text>
</RowFixed>
</RowBetween>
<RowBetween>
<Text fontSize={24} fontWeight={500}>
{formattedAmounts[Field.TOKEN_B] || '-'}
{formattedAmounts[Field.CURRENCY_B] || '-'}
</Text>
<RowFixed>
<TokenLogo address={tokens[Field.TOKEN_B]?.address} style={{ marginRight: '12px' }} />
<CurrencyLogo currency={currencyB} style={{ marginRight: '12px' }} />
<Text fontSize={24} fontWeight={500} id="remove-liquidity-tokenb-symbol">
{tokens[Field.TOKEN_B]?.symbol}
{currencyB?.symbol}
</Text>
</RowFixed>
</RowBetween>
{chainId && (oneCurrencyIsWETH || oneCurrencyIsETH) ? (
<RowBetween style={{ justifyContent: 'flex-end' }}>
{oneCurrencyIsETH ? (
<StyledInternalLink
to={`/remove/${currencyA === ETHER ? WETH[chainId].address : currencyIdA}/${
currencyB === ETHER ? WETH[chainId].address : currencyIdB
}`}
>
Receive WETH
</StyledInternalLink>
) : oneCurrencyIsWETH ? (
<StyledInternalLink
to={`/remove/${
currencyA && currencyEquals(currencyA, WETH[chainId]) ? 'ETH' : currencyIdA
}/${currencyB && currencyEquals(currencyB, WETH[chainId]) ? 'ETH' : currencyIdB}`}
>
Receive ETH
</StyledInternalLink>
) : null}
</RowBetween>
) : null}
</AutoColumn>
</LightCard>
</>
@@ -491,16 +579,14 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
{showDetailed && (
<>
<CurrencyInputPanel
field={Field.LIQUIDITY}
value={formattedAmounts[Field.LIQUIDITY]}
onUserInput={onUserInput}
onUserInput={onLiquidityInput}
onMax={() => {
onUserInput(Field.LIQUIDITY_PERCENT, '100')
}}
showMaxButton={!atMaxAmount}
disableTokenSelect
token={pair?.liquidityToken}
isExchange={true}
disableCurrencySelect
currency={pair?.liquidityToken}
pair={pair}
id="liquidity-amount"
/>
@@ -509,14 +595,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
field={Field.TOKEN_A}
value={formattedAmounts[Field.TOKEN_A]}
onUserInput={onUserInput}
value={formattedAmounts[Field.CURRENCY_A]}
onUserInput={onCurrencyAInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN_A]}
currency={currencyA}
label={'Output'}
disableTokenSelect
onCurrencySelect={handleSelectCurrencyA}
id="remove-liquidity-tokena"
/>
<ColumnCenter>
@@ -524,32 +609,36 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
</ColumnCenter>
<CurrencyInputPanel
hideBalance={true}
field={Field.TOKEN_B}
value={formattedAmounts[Field.TOKEN_B]}
onUserInput={onUserInput}
value={formattedAmounts[Field.CURRENCY_B]}
onUserInput={onCurrencyBInput}
onMax={() => onUserInput(Field.LIQUIDITY_PERCENT, '100')}
showMaxButton={!atMaxAmount}
token={tokens[Field.TOKEN_B]}
currency={currencyB}
label={'Output'}
disableTokenSelect
onCurrencySelect={handleSelectCurrencyB}
id="remove-liquidity-tokenb"
/>
</>
)}
{route && (
{pair && (
<div style={{ padding: '10px 20px' }}>
<RowBetween>
Price:
<div>
1 {tokens[Field.TOKEN_A]?.symbol} = {route.midPrice.toSignificant(6)}{' '}
{tokens[Field.TOKEN_B]?.symbol}
1 {currencyA?.symbol} = {tokenA ? pair.priceOf(tokenA).toSignificant(6) : '-'} {currencyB?.symbol}
</div>
</RowBetween>
<RowBetween>
<div />
<div>
1 {tokens[Field.TOKEN_B]?.symbol} = {route.midPrice.invert().toSignificant(6)}{' '}
{tokens[Field.TOKEN_A]?.symbol}
1 {currencyB?.symbol} ={' '}
{tokenB
? pair
.priceOf(tokenB)
.invert()
.toSignificant(6)
: '-'}{' '}
{currencyA?.symbol}
</div>
</RowBetween>
</div>
@@ -580,7 +669,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
setShowConfirm(true)
}}
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
error={!isValid && !!parsedAmounts[Field.CURRENCY_A] && !!parsedAmounts[Field.CURRENCY_B]}
>
<Text fontSize={16} fontWeight={500}>
{error || 'Remove'}
@@ -595,7 +684,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
{pair ? (
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
<MinimalPositionCard pair={pair} />
<MinimalPositionCard showUnwrapped={oneCurrencyIsWETH} pair={pair} />
</AutoColumn>
) : null}
</>

View File

@@ -0,0 +1,17 @@
import React from 'react'
import { RouteComponentProps, Redirect } from 'react-router-dom'
const OLD_PATH_STRUCTURE = /^(0x[a-fA-F0-9]{40})-(0x[a-fA-F0-9]{40})$/
export function RedirectOldRemoveLiquidityPathStructure({
match: {
params: { tokens }
}
}: RouteComponentProps<{ tokens: string }>) {
if (!OLD_PATH_STRUCTURE.test(tokens)) {
return <Redirect to="/pool" />
}
const [currency0, currency1] = tokens.split('-')
return <Redirect to={`/remove/${currency0}/${currency1}`} />
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.strict.json",
"include": [
"**/*",
"../../../node_modules/eslint-plugin-react/lib/types.d.ts"
]
}

View File

@@ -1,5 +1,5 @@
import { JSBI, TokenAmount } from '@uniswap/sdk'
import React, { useContext, useState, useEffect, useCallback } from 'react'
import { CurrencyAmount, JSBI, Trade } from '@uniswap/sdk'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { ArrowDown } from 'react-feather'
import ReactGA from 'react-ga'
import { Text } from 'rebass'
@@ -8,28 +8,26 @@ import AddressInputPanel from '../../components/AddressInputPanel'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
import Card, { GreyCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
import ConfirmSwapModal from '../../components/swap/ConfirmSwapModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { SwapPoolTabs } from '../../components/NavigationTabs'
import { AutoRow, RowBetween } from '../../components/Row'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds'
import SwapModalFooter from '../../components/swap/SwapModalFooter'
import SwapModalHeader from '../../components/swap/SwapModalHeader'
import TradePrice from '../../components/swap/TradePrice'
import BetterTradeLink from '../../components/swap/BetterTradeLink'
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
import { ArrowWrapper, BottomGrouping, Dots, SwapCallbackError, Wrapper } from '../../components/swap/styleds'
import TradePrice from '../../components/swap/TradePrice'
import { TokenWarningCards } from '../../components/TokenWarningCard'
import { BETTER_TRADE_LINK_THRESHOLD, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
import { ApprovalState, useApproveCallbackFromTrade } from '../../hooks/useApproveCallback'
import useENSAddress from '../../hooks/useENSAddress'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
import { INITIAL_ALLOWED_SLIPPAGE, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
import { getTradeVersion, isTradeBetter } from '../../data/V1'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
import useWrapCallback, { WrapType } from '../../hooks/useWrapCallback'
import { useToggleSettingsMenu, useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import {
useDefaultsFromURLSearch,
@@ -37,16 +35,22 @@ import {
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import {
useExpertModeManager,
useTokenWarningDismissal,
useUserDeadline,
useUserSlippageTolerance
} from '../../state/user/hooks'
import { CursorPointer, LinkStyledButton, TYPE } from '../../theme'
import { maxAmountSpend } from '../../utils/maxAmountSpend'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import { computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { ClickableText } from '../Pool/styleds'
export default function Swap() {
useDefaultsFromURLSearch()
const { account } = useActiveWeb3React()
const { account, chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
@@ -54,7 +58,7 @@ export default function Swap() {
// for expert mode
const toggleSettings = useToggleSettingsMenu()
const [expertMode] = useExpertModeManager()
const [isExpertMode] = useExpertModeManager()
// get custom setting values for user
const [deadline] = useUserDeadline()
@@ -62,14 +66,28 @@ export default function Swap() {
// swap state
const { independentField, typedValue, recipient } = useSwapState()
const { v1Trade, v2Trade, tokenBalances, parsedAmount, tokens, error } = useDerivedSwapInfo()
const {
v1Trade,
v2Trade,
currencyBalances,
parsedAmount,
currencies,
inputError: swapInputError
} = useDerivedSwapInfo()
const { wrapType, execute: onWrap, inputError: wrapInputError } = useWrapCallback(
currencies[Field.INPUT],
currencies[Field.OUTPUT],
typedValue
)
const showWrap: boolean = wrapType !== WrapType.NOT_APPLICABLE
const { address: recipientAddress } = useENSAddress(recipient)
const toggledVersion = useToggledVersion()
const trade =
{
[Version.v1]: v1Trade,
[Version.v2]: v2Trade
}[toggledVersion] ?? undefined
const trade = showWrap
? undefined
: {
[Version.v1]: v1Trade,
[Version.v2]: v2Trade
}[toggledVersion]
const betterTradeLinkVersion: Version | undefined =
toggledVersion === Version.v2 && isTradeBetter(v2Trade, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
@@ -78,41 +96,58 @@ export default function Swap() {
? Version.v2
: undefined
const parsedAmounts = {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount
}
const parsedAmounts = showWrap
? {
[Field.INPUT]: parsedAmount,
[Field.OUTPUT]: parsedAmount
}
: {
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : trade?.inputAmount,
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : trade?.outputAmount
}
const { onSwitchTokens, onTokenSelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
const isValid = !error
const { onSwitchTokens, onCurrencySelection, onUserInput, onChangeRecipient } = useSwapActionHandlers()
const isValid = !swapInputError
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
const handleTypeInput = useCallback(
(field, value) => {
(value: string) => {
onUserInput(Field.INPUT, value)
},
[onUserInput]
)
const handleTypeOutput = useCallback(
(field, value) => {
(value: string) => {
onUserInput(Field.OUTPUT, value)
},
[onUserInput]
)
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
const [txHash, setTxHash] = useState<string>('')
const [{ showConfirm, tradeToConfirm, swapErrorMessage, attemptingTxn, txHash }, setSwapState] = useState<{
showConfirm: boolean
tradeToConfirm: Trade | undefined
attemptingTxn: boolean
swapErrorMessage: string | undefined
txHash: string | undefined
}>({
showConfirm: false,
tradeToConfirm: undefined,
attemptingTxn: false,
swapErrorMessage: undefined,
txHash: undefined
})
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField]?.toSignificant(6) ?? ''
[dependentField]: showWrap
? parsedAmounts[independentField]?.toExact() ?? ''
: parsedAmounts[dependentField]?.toSignificant(6) ?? ''
}
const route = trade?.route
const userHasSpecifiedInputOutput = Boolean(
tokens[Field.INPUT] && tokens[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0))
currencies[Field.INPUT] && currencies[Field.OUTPUT] && parsedAmounts[independentField]?.greaterThan(JSBI.BigInt(0))
)
const noRoute = !route
@@ -129,28 +164,30 @@ export default function Swap() {
}
}, [approval, approvalSubmitted])
const maxAmountInput: TokenAmount | undefined = maxAmountSpend(tokenBalances[Field.INPUT])
const maxAmountInput: CurrencyAmount | undefined = maxAmountSpend(currencyBalances[Field.INPUT])
const atMaxAmountInput = Boolean(maxAmountInput && parsedAmounts[Field.INPUT]?.equalTo(maxAmountInput))
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
// the callback to execute the swap
const swapCallback = useSwapCallback(trade, allowedSlippage, deadline, recipient)
const { callback: swapCallback, error: swapCallbackError } = useSwapCallback(
trade,
allowedSlippage,
deadline,
recipient
)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
const { priceImpactWithoutFee } = computeTradePriceBreakdown(trade)
function onSwap() {
const handleSwap = useCallback(() => {
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
return
}
if (!swapCallback) {
return
}
setAttemptingTxn(true)
setSwapState({ attemptingTxn: true, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: undefined })
swapCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
setSwapState({ attemptingTxn: false, tradeToConfirm, showConfirm, swapErrorMessage: undefined, txHash: hash })
ReactGA.event({
category: 'Swap',
@@ -160,19 +197,23 @@ export default function Swap() {
: (recipientAddress ?? recipient) === account
? 'Swap w/o Send + recipient'
: 'Swap w/ Send',
label: [trade?.inputAmount?.token?.symbol, trade?.outputAmount?.token?.symbol, getTradeVersion(trade)].join(
'/'
)
label: [
trade?.inputAmount?.currency?.symbol,
trade?.outputAmount?.currency?.symbol,
getTradeVersion(trade)
].join('/')
})
})
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
setSwapState({
attemptingTxn: false,
tradeToConfirm,
showConfirm,
swapErrorMessage: error.message,
txHash: undefined
})
})
}
}, [tradeToConfirm, account, priceImpactWithoutFee, recipient, recipientAddress, showConfirm, swapCallback, trade])
// errors
const [showInverted, setShowInverted] = useState<boolean>(false)
@@ -183,87 +224,64 @@ export default function Swap() {
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
// never show if price impact is above threshold in non expert mode
const showApproveFlow =
!error &&
!swapInputError &&
(approval === ApprovalState.NOT_APPROVED ||
approval === ApprovalState.PENDING ||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
!(priceImpactSeverity > 3 && !expertMode)
!(priceImpactSeverity > 3 && !isExpertMode)
function modalHeader() {
return (
<SwapModalHeader
tokens={tokens}
formattedAmounts={formattedAmounts}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactSeverity={priceImpactSeverity}
independentField={independentField}
recipient={recipient}
/>
)
}
const [dismissedToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
const [dismissedToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
const showWarning =
(!dismissedToken0 && !!currencies[Field.INPUT]) || (!dismissedToken1 && !!currencies[Field.OUTPUT])
function modalBottom() {
return (
<SwapModalFooter
confirmText={priceImpactSeverity > 2 ? 'Swap Anyway' : 'Confirm Swap'}
showInverted={showInverted}
severity={priceImpactSeverity}
setShowInverted={setShowInverted}
onSwap={onSwap}
realizedLPFee={realizedLPFee}
parsedAmounts={parsedAmounts}
priceImpactWithoutFee={priceImpactWithoutFee}
slippageAdjustedAmounts={slippageAdjustedAmounts}
trade={trade}
/>
)
}
const handleConfirmDismiss = useCallback(() => {
setSwapState({ showConfirm: false, tradeToConfirm, attemptingTxn, swapErrorMessage, txHash })
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.INPUT, '')
}
}, [attemptingTxn, onUserInput, swapErrorMessage, tradeToConfirm, txHash])
// text to show while loading
const pendingText = `Swapping ${parsedAmounts[Field.INPUT]?.toSignificant(6)} ${
tokens[Field.INPUT]?.symbol
} for ${parsedAmounts[Field.OUTPUT]?.toSignificant(6)} ${tokens[Field.OUTPUT]?.symbol}`
const handleAcceptChanges = useCallback(() => {
setSwapState({ tradeToConfirm: trade, swapErrorMessage, txHash, attemptingTxn, showConfirm })
}, [attemptingTxn, showConfirm, swapErrorMessage, trade, txHash])
return (
<>
<TokenWarningCards tokens={tokens} />
<AppBody>
{showWarning && <TokenWarningCards currencies={currencies} />}
<AppBody disabled={showWarning}>
<SwapPoolTabs active={'swap'} />
<Wrapper id="swap-page">
<ConfirmationModal
<ConfirmSwapModal
isOpen={showConfirm}
title="Confirm Swap"
onDismiss={() => {
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.INPUT, '')
}
setTxHash('')
}}
trade={trade}
originalTrade={tradeToConfirm}
onAcceptChanges={handleAcceptChanges}
attemptingTxn={attemptingTxn}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
pendingText={pendingText}
txHash={txHash}
recipient={recipient}
allowedSlippage={allowedSlippage}
onConfirm={handleSwap}
swapErrorMessage={swapErrorMessage}
onDismiss={handleConfirmDismiss}
/>
<AutoColumn gap={'md'}>
<CurrencyInputPanel
field={Field.INPUT}
label={independentField === Field.OUTPUT ? 'From (estimated)' : 'From'}
label={independentField === Field.OUTPUT && !showWrap ? 'From (estimated)' : 'From'}
value={formattedAmounts[Field.INPUT]}
showMaxButton={!atMaxAmountInput}
token={tokens[Field.INPUT]}
currency={currencies[Field.INPUT]}
onUserInput={handleTypeInput}
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => {
onCurrencySelect={currency => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onTokenSelection(Field.INPUT, address)
onCurrencySelection(Field.INPUT, currency)
}}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
otherCurrency={currencies[Field.OUTPUT]}
id="swap-currency-input"
/>
@@ -277,74 +295,80 @@ export default function Swap() {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
color={currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
{recipient === null ? (
{recipient === null && !showWrap ? (
<LinkStyledButton id="add-recipient-button" onClick={() => onChangeRecipient('')}>
+ add recipient (optional)
+ Add a send (optional)
</LinkStyledButton>
) : null}
</AutoRow>
</AutoColumn>
</CursorPointer>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
label={independentField === Field.INPUT && !showWrap ? 'To (estimated)' : 'To'}
showMaxButton={false}
token={tokens[Field.OUTPUT]}
onTokenSelection={address => onTokenSelection(Field.OUTPUT, address)}
otherSelectedTokenAddress={tokens[Field.INPUT]?.address}
currency={currencies[Field.OUTPUT]}
onCurrencySelect={address => onCurrencySelection(Field.OUTPUT, address)}
otherCurrency={currencies[Field.INPUT]}
id="swap-currency-output"
/>
{recipient !== null ? (
{recipient !== null && !showWrap ? (
<>
<AutoRow justify="space-between" style={{ padding: '0 1rem' }}>
<ArrowWrapper clickable={false}>
<ArrowDown size="16" color={theme.text2} />
</ArrowWrapper>
<LinkStyledButton id="remove-recipient-button" onClick={() => onChangeRecipient(null)}>
- remove recipient
- Remove send
</LinkStyledButton>
</AutoRow>
<AddressInputPanel id="recipient" value={recipient} onChange={onChangeRecipient} />
</>
) : null}
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<TradePrice
inputToken={tokens[Field.INPUT]}
outputToken={tokens[Field.OUTPUT]}
price={trade?.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</RowBetween>
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
{showWrap ? null : (
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
Slippage Tolerance
</ClickableText>
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
{allowedSlippage ? allowedSlippage / 100 : '-'}%
</ClickableText>
<Text fontWeight={500} fontSize={14} color={theme.text2}>
Price
</Text>
<TradePrice
inputCurrency={currencies[Field.INPUT]}
outputCurrency={currencies[Field.OUTPUT]}
price={trade?.executionPrice}
showInverted={showInverted}
setShowInverted={setShowInverted}
/>
</RowBetween>
)}
</AutoColumn>
</Card>
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
<RowBetween align="center">
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
Slippage Tolerance
</ClickableText>
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
{allowedSlippage ? allowedSlippage / 100 : '-'}%
</ClickableText>
</RowBetween>
)}
</AutoColumn>
</Card>
)}
</AutoColumn>
<BottomGrouping>
{!account ? (
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : showWrap ? (
<ButtonPrimary disabled={Boolean(wrapInputError)} onClick={onWrap}>
{wrapInputError ??
(wrapType === WrapType.WRAP ? 'Wrap' : wrapType === WrapType.UNWRAP ? 'Unwrap' : null)}
</ButtonPrimary>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
@@ -362,20 +386,32 @@ export default function Swap() {
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
'Approve ' + currencies[Field.INPUT]?.symbol
)}
</ButtonPrimary>
<ButtonError
onClick={() => {
expertMode ? onSwap() : setShowConfirm(true)
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined
})
}
}}
width="48%"
id="swap-button"
disabled={!isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !expertMode)}
disabled={
!isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !isExpertMode)
}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={16} fontWeight={500}>
{priceImpactSeverity > 3 && !expertMode
{priceImpactSeverity > 3 && !isExpertMode
? `Price Impact High`
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
@@ -384,26 +420,36 @@ export default function Swap() {
) : (
<ButtonError
onClick={() => {
expertMode ? onSwap() : setShowConfirm(true)
if (isExpertMode) {
handleSwap()
} else {
setSwapState({
tradeToConfirm: trade,
attemptingTxn: false,
swapErrorMessage: undefined,
showConfirm: true,
txHash: undefined
})
}
}}
id="swap-button"
disabled={!isValid || (priceImpactSeverity > 3 && !expertMode)}
error={isValid && priceImpactSeverity > 2}
disabled={!isValid || (priceImpactSeverity > 3 && !isExpertMode) || !!swapCallbackError}
error={isValid && priceImpactSeverity > 2 && !swapCallbackError}
>
<Text fontSize={20} fontWeight={500}>
{error
? error
: priceImpactSeverity > 3 && !expertMode
{swapInputError
? swapInputError
: priceImpactSeverity > 3 && !isExpertMode
? `Price Impact Too High`
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
)}
{isExpertMode && swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
</BottomGrouping>
</Wrapper>
</AppBody>
<AdvancedSwapDetailsDropdown trade={trade} />
</>
)

View File

@@ -1,23 +1,20 @@
import { createAction } from '@reduxjs/toolkit'
import { TokenList } from '@uniswap/token-lists'
export type PopupContent =
| {
txn: {
hash: string
success?: boolean
success: boolean
summary?: string
}
}
| {
poolAdded: {
token0?: {
address?: string
symbol?: string
}
token1: {
address?: string
symbol?: string
}
listUpdate: {
listUrl: string
oldList: TokenList
newList: TokenList
auto: boolean
}
}

View File

@@ -0,0 +1,92 @@
import { ChainId } from '@uniswap/sdk'
import { createStore, Store } from 'redux'
import { addPopup, removePopup, toggleSettingsMenu, toggleWalletModal, updateBlockNumber } from './actions'
import reducer, { ApplicationState } from './reducer'
describe('application reducer', () => {
let store: Store<ApplicationState>
beforeEach(() => {
store = createStore(reducer, {
popupList: [],
walletModalOpen: false,
settingsMenuOpen: false,
blockNumber: {
[ChainId.MAINNET]: 3
}
})
})
describe('addPopup', () => {
it('adds the popup to list with a generated id', () => {
store.dispatch(addPopup({ content: { txn: { hash: 'abc', summary: 'test', success: true } } }))
const list = store.getState().popupList
expect(list).toHaveLength(1)
expect(typeof list[0].key).toEqual('string')
expect(list[0].show).toEqual(true)
expect(list[0].content).toEqual({ txn: { hash: 'abc', summary: 'test', success: true } })
})
it('replaces any existing popups with the same key', () => {
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'abc', summary: 'test', success: true } } }))
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'def', summary: 'test2', success: false } } }))
const list = store.getState().popupList
expect(list).toHaveLength(1)
expect(list[0].key).toEqual('abc')
expect(list[0].show).toEqual(true)
expect(list[0].content).toEqual({ txn: { hash: 'def', summary: 'test2', success: false } })
})
})
describe('toggleWalletModal', () => {
it('toggles wallet modal', () => {
store.dispatch(toggleWalletModal())
expect(store.getState().walletModalOpen).toEqual(true)
store.dispatch(toggleWalletModal())
expect(store.getState().walletModalOpen).toEqual(false)
store.dispatch(toggleWalletModal())
expect(store.getState().walletModalOpen).toEqual(true)
})
})
describe('settingsMenuOpen', () => {
it('toggles settings menu', () => {
store.dispatch(toggleSettingsMenu())
expect(store.getState().settingsMenuOpen).toEqual(true)
store.dispatch(toggleSettingsMenu())
expect(store.getState().settingsMenuOpen).toEqual(false)
store.dispatch(toggleSettingsMenu())
expect(store.getState().settingsMenuOpen).toEqual(true)
})
})
describe('updateBlockNumber', () => {
it('updates block number', () => {
store.dispatch(updateBlockNumber({ chainId: ChainId.MAINNET, blockNumber: 4 }))
expect(store.getState().blockNumber[ChainId.MAINNET]).toEqual(4)
})
it('no op if late', () => {
store.dispatch(updateBlockNumber({ chainId: ChainId.MAINNET, blockNumber: 2 }))
expect(store.getState().blockNumber[ChainId.MAINNET]).toEqual(3)
})
it('works with non-set chains', () => {
store.dispatch(updateBlockNumber({ chainId: ChainId.ROPSTEN, blockNumber: 2 }))
expect(store.getState().blockNumber).toEqual({
[ChainId.MAINNET]: 3,
[ChainId.ROPSTEN]: 2
})
})
})
describe('removePopup', () => {
beforeEach(() => {
store.dispatch(addPopup({ key: 'abc', content: { txn: { hash: 'abc', summary: 'test', success: true } } }))
})
it('hides the popup', () => {
expect(store.getState().popupList[0].show).toBe(true)
store.dispatch(removePopup({ key: 'abc' }))
expect(store.getState().popupList).toHaveLength(1)
expect(store.getState().popupList[0].show).toBe(false)
})
})
})

View File

@@ -10,7 +10,7 @@ import {
type PopupList = Array<{ key: string; show: boolean; content: PopupContent }>
interface ApplicationState {
export interface ApplicationState {
blockNumber: { [chainId: number]: number }
popupList: PopupList
walletModalOpen: boolean
@@ -41,12 +41,13 @@ export default createReducer(initialState, builder =>
state.settingsMenuOpen = !state.settingsMenuOpen
})
.addCase(addPopup, (state, { payload: { content, key } }) => {
if (key && state.popupList.some(popup => popup.key === key)) return
state.popupList.push({
key: key || nanoid(),
show: true,
content
})
state.popupList = (key ? state.popupList.filter(popup => popup.key !== key) : state.popupList).concat([
{
key: key || nanoid(),
show: true,
content
}
])
})
.addCase(removePopup, (state, { payload: { key } }) => {
state.popupList.forEach(p => {

View File

@@ -1,15 +1,10 @@
import { createAction } from '@reduxjs/toolkit'
import { ChainId } from '@uniswap/sdk'
export enum Field {
LIQUIDITY_PERCENT = 'LIQUIDITY_PERCENT',
LIQUIDITY = 'LIQUIDITY',
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
CURRENCY_A = 'CURRENCY_A',
CURRENCY_B = 'CURRENCY_B'
}
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInputBurn')
export const setBurnDefaultsFromURLMatchParams = createAction<{
chainId: ChainId
params: { tokens: string }
}>('setBurnDefaultsFromURLMatchParams')

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