Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e32fd3a8fc | ||
|
|
057417c666 | ||
|
|
f1b300af70 | ||
|
|
600049bc6e | ||
|
|
6e91311489 | ||
|
|
f6a464cb3b | ||
|
|
e589c751d7 | ||
|
|
0f91af1df2 | ||
|
|
10ef04510a | ||
|
|
e3b3d9e825 | ||
|
|
3050e967f7 | ||
|
|
2150450760 | ||
|
|
1b07e95885 | ||
|
|
9bb50d6a7b | ||
|
|
b08bb7eaff | ||
|
|
3a36ac5538 | ||
|
|
2962cd0e14 | ||
|
|
6a311aa6d7 | ||
|
|
e78b6d61f2 | ||
|
|
365b429c0b | ||
|
|
32d300009e | ||
|
|
806623c602 | ||
|
|
3272f8e9db | ||
|
|
010ef108eb | ||
|
|
19b1e9e399 | ||
|
|
6287b95b92 | ||
|
|
4e8a6e2a4c |
2
.env
2
.env
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/bug-report.md
vendored
11
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -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
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
9
.github/ISSUE_TEMPLATE/feature-request.md
vendored
9
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -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 [...]
|
||||
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
@@ -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
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/token-request.md
vendored
27
.github/ISSUE_TEMPLATE/token-request.md
vendored
@@ -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?:
|
||||
18
.github/workflows/release.yaml
vendored
18
.github/workflows/release.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
8
cypress/integration/migrate-v1.test.ts
Normal file
8
cypress/integration/migrate-v1.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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', () => {
|
||||
|
||||
19
cypress/integration/token-warning.ts
Normal file
19
cypress/integration/token-warning.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
26
package.json
26
package.json
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -27,6 +27,8 @@ const Base = styled(RebassButton)<{
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
&:disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
94
src/components/CurrencyLogo/index.tsx
Normal file
94
src/components/CurrencyLogo/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
72
src/components/Popups/ListUpdatePopup.tsx
Normal file
72
src/components/Popups/ListUpdatePopup.tsx
Normal 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 "{oldList.name}" has been updated to{' '}
|
||||
<strong>{versionLabel(newList.version)}</strong>.
|
||||
</TYPE.body>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
A token list update is available for the list "{oldList.name}" ({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>
|
||||
)
|
||||
}
|
||||
86
src/components/Popups/PopupItem.tsx
Normal file
86
src/components/Popups/PopupItem.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
39
src/components/Popups/TransactionPopup.tsx
Normal file
39
src/components/Popups/TransactionPopup.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 }} />
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
195
src/components/TransactionConfirmationModal/index.tsx
Normal file
195
src/components/TransactionConfirmationModal/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -349,9 +349,7 @@ export default function WalletModal({
|
||||
{walletView !== WALLET_VIEWS.PENDING && (
|
||||
<Blurb>
|
||||
<span>New to Ethereum? </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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
109
src/components/swap/ConfirmSwapModal.tsx
Normal file
109
src/components/swap/ConfirmSwapModal.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
4
src/components/swap/tsconfig.json
Normal file
4
src/components/swap/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
279
src/constants/abis/weth.json
Normal file
279
src/constants/abis/weth.json
Normal 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"
|
||||
}
|
||||
]
|
||||
@@ -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'
|
||||
|
||||
@@ -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]: {}
|
||||
}
|
||||
)
|
||||
@@ -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')
|
||||
]
|
||||
@@ -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')
|
||||
]
|
||||
@@ -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')
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Token, ChainId } from '@uniswap/sdk'
|
||||
|
||||
export default [new Token(ChainId.ROPSTEN, '0xaD6D458402F60fD3Bd25163575031ACDce07538D', 18, 'DAI', 'Dai Stablecoin')]
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
75
src/hooks/useWrapCallback.ts
Normal file
75
src/hooks/useWrapCallback.ts
Normal 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])
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 don’t 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 don’t 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>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
|
||||
17
src/pages/RemoveLiquidity/redirects.tsx
Normal file
17
src/pages/RemoveLiquidity/redirects.tsx
Normal 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}`} />
|
||||
}
|
||||
7
src/pages/RemoveLiquidity/tsconfig.json
Normal file
7
src/pages/RemoveLiquidity/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.strict.json",
|
||||
"include": [
|
||||
"**/*",
|
||||
"../../../node_modules/eslint-plugin-react/lib/types.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -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} />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
92
src/state/application/reducer.test.ts
Normal file
92
src/state/application/reducer.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user