Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
041c86c04d | ||
|
|
123373e671 | ||
|
|
eb1732deee | ||
|
|
3c13321a71 | ||
|
|
58703f31a0 | ||
|
|
58721fb191 | ||
|
|
678cd1a06f | ||
|
|
a5ff3beb92 | ||
|
|
35ccf425f6 | ||
|
|
fe030412cd | ||
|
|
4d5a43351f | ||
|
|
ac1bc3b3a6 | ||
|
|
d1063d50ed | ||
|
|
46fc74e90f | ||
|
|
2c4f4092d8 | ||
|
|
aac7268dc8 | ||
|
|
fd162a72ff | ||
|
|
e20936709c | ||
|
|
2fda2c8c15 | ||
|
|
1f09757c49 | ||
|
|
7e49babff7 | ||
|
|
b35653ade1 | ||
|
|
57b53013d1 | ||
|
|
bafd3f3c05 | ||
|
|
29db0a50b3 | ||
|
|
9566fb888e | ||
|
|
13c8903e8f | ||
|
|
1d06b47e8d | ||
|
|
0089c2ee43 | ||
|
|
9869a9fcb7 | ||
|
|
631f29d66d | ||
|
|
97deebad37 |
31
.github/workflows/release.yaml
vendored
31
.github/workflows/release.yaml
vendored
@@ -2,7 +2,7 @@ name: Release
|
||||
on:
|
||||
# every morning
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
- cron: '0 12 * * 1-4'
|
||||
|
||||
# releases are triggered on changes to this file
|
||||
push:
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
- v2
|
||||
paths:
|
||||
- '.github/workflows/release.yaml'
|
||||
- '.env.production'
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
@@ -64,14 +65,14 @@ jobs:
|
||||
cidv0: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Update DNS with new IPFS hash
|
||||
uses: uniswap/replace-vercel-dns-records@v1.0.0
|
||||
env:
|
||||
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
|
||||
RECORD_DOMAIN: 'uniswap.org'
|
||||
RECORD_NAME: '_dnslink.app'
|
||||
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
|
||||
uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
|
||||
with:
|
||||
domain: 'uniswap.org'
|
||||
subdomain: '_dnslink.app'
|
||||
record-type: 'TXT'
|
||||
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
|
||||
token: ${{ secrets.VERCEL_TOKEN }}
|
||||
team-name: 'uniswap'
|
||||
cid: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
@@ -82,15 +83,17 @@ jobs:
|
||||
tag_name: ${{ needs.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
|
||||
body: |
|
||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
||||
|
||||
The IPFS hash of the bundle is:
|
||||
IPFS hash of the deployment:
|
||||
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
||||
|
||||
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
|
||||
The latest release is always accessible via our alias to the Cloudflare IPFS gateway at [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
You can also access the Uniswap Interface directly from an IPFS gateway.
|
||||
The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on the Uniswap interface without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway, or our alias to the latest release at [app.uniswap.org](https://app.uniswap.org).
|
||||
The preferred URLs below are safe to use to access this specific release.
|
||||
|
||||
Preferred URLs:
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
|
||||
@@ -2,7 +2,7 @@ describe('Pool', () => {
|
||||
beforeEach(() => cy.visit('/pool'))
|
||||
it('can search for a pool', () => {
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.get('#token-search-input').type('DAI')
|
||||
cy.get('#token-search-input').type('DAI', { delay: 200 })
|
||||
})
|
||||
|
||||
it('can import a pool', () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ describe('Send', () => {
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#sending-no-swap-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,31 +2,31 @@ describe('Swap', () => {
|
||||
beforeEach(() => cy.visit('/swap'))
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.0')
|
||||
.type('0.0', { delay: 200 })
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('\\')
|
||||
.type('\\', { delay: 200 })
|
||||
.should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.001')
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero output amount', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.0')
|
||||
.type('0.0', { delay: 200 })
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
@@ -35,9 +35,8 @@ describe('Swap', () => {
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true, delay: 200 })
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
cy.get('#show-advanced').click()
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@web3-react/fortmatic-connector": "^6.0.9",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.1.1",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
|
||||
"build": "react-scripts build",
|
||||
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
|
||||
75
public/locales/iw.json
Normal file
75
public/locales/iw.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"noWallet": "לא נמצא ארנק",
|
||||
"wrongNetwork": "נבחרה רשת לא נכונה",
|
||||
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
|
||||
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
|
||||
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
|
||||
"disconnected": "מנותק",
|
||||
"swap": "המרה",
|
||||
"send": "שליחה",
|
||||
"pool": "להפקיד",
|
||||
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
|
||||
"input": "מוכר",
|
||||
"output": "אקבל",
|
||||
"estimated": "הערכה",
|
||||
"balance": "בארנק שלי {{ balanceInput }}",
|
||||
"unlock": "שחרור נעילת ארנק",
|
||||
"pending": "ממתין לאישור",
|
||||
"selectToken": "בחרו את הטוקן להמרה",
|
||||
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
|
||||
"noExchange": "לא מתאפשרת המרה",
|
||||
"exchangeRate": "שער המרה",
|
||||
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
|
||||
"selectTokenCont": "בחרו טוקן כדי להמשיך",
|
||||
"noLiquidity": "אין נזילות",
|
||||
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
|
||||
"transactionDetails": "פרטי הטרנזקציה",
|
||||
"hideDetails": "הסתר פרטים נוספים",
|
||||
"youAreSelling": "למכירה",
|
||||
"orTransFail": "או שהטרנזקציה תיכשל",
|
||||
"youWillReceive": "תוצר המרה מינימלי",
|
||||
"youAreBuying": "קונה",
|
||||
"itWillCost": "זה יעלה",
|
||||
"insufficientBalance": "אין בחשבון מספיק מטבעות",
|
||||
"inputNotValid": "קלט לא תקין",
|
||||
"differentToken": "יש צורך בטוקנים שונים",
|
||||
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
|
||||
"invalidRecipient": "לא הוכנסה כתובת תקינה",
|
||||
"recipientAddress": "כתובת יעד",
|
||||
"youAreSending": "כמות לשליחה",
|
||||
"willReceive": "יתקבל לכל הפחות",
|
||||
"to": "אל",
|
||||
"addLiquidity": "להוספת נזילות למאגר",
|
||||
"deposit": "הפקדה",
|
||||
"currentPoolSize": "גודל מאגר הנזילות הכולל",
|
||||
"yourPoolShare": "חלקך במאגר הנזילות",
|
||||
"noZero": "אפס אינו ערך תקין",
|
||||
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
|
||||
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
|
||||
"youAreAdding": "מתווספים למאגר",
|
||||
"and": "וגם",
|
||||
"intoPool": "לתוך הנזילות",
|
||||
"outPool": "מתוך",
|
||||
"youWillMint": "יונפקו לכם",
|
||||
"liquidityTokens": "טוקנים של נזילות",
|
||||
"totalSupplyIs": "חלקך במאגר הנזילות",
|
||||
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
|
||||
"totalSupplyIs0": "אין לך טוקנים של נזילות",
|
||||
"tokenWorth": "שווי כל טוקן נזילות הינו",
|
||||
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר",
|
||||
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
|
||||
"removeLiquidity": "הוצאה של נזילות",
|
||||
"poolTokens": "טוקנים של מאגר הנזילות",
|
||||
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
|
||||
"youAreRemoving": "יוסרו",
|
||||
"youWillRemove": "יוסרו",
|
||||
"createExchange": "ליצירת זוג מסחר",
|
||||
"invalidTokenAddress": "כתובת טוקן לא נכונה",
|
||||
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
|
||||
"invalidSymbol": "תו שגוי",
|
||||
"invalidDecimals": "ספרות עשרוניות שגויות",
|
||||
"tokenAddress": "כתובת הטוקן",
|
||||
"label": "שם",
|
||||
"decimals": "ספרות עשרויות",
|
||||
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
|
||||
}
|
||||
@@ -51,10 +51,10 @@ export const ButtonPrimary = styled(Base)`
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
|
||||
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)}
|
||||
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
@@ -197,7 +197,6 @@ export const ButtonEmpty = styled(Base)`
|
||||
export const ButtonWhite = styled(Base)`
|
||||
border: 1px solid #edeef2;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
};
|
||||
color: black;
|
||||
|
||||
&:focus {
|
||||
@@ -245,6 +244,9 @@ const ButtonErrorStyle = styled(Base)`
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
background-color: ${({ theme }) => theme.red1};
|
||||
border: 1px solid ${({ theme }) => theme.red1};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -264,7 +266,7 @@ export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProp
|
||||
}
|
||||
}
|
||||
|
||||
export function ButtonDropwdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
export function ButtonDropdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonPrimary {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
@@ -275,7 +277,7 @@ export function ButtonDropwdown({ disabled = false, children, ...rest }: { disab
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropwdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonOutlined {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import TokenSearchModal from '../SearchModal/TokenSearchModal'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import SearchModal from '../SearchModal'
|
||||
import { RowBetween } from '../Row'
|
||||
import { TYPE, CursorPointer } from '../../theme'
|
||||
import { Input as NumericalInput } from '../NumericalInput'
|
||||
@@ -159,6 +159,10 @@ export default function CurrencyInputPanel({
|
||||
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
setModalOpen(false)
|
||||
}, [setModalOpen])
|
||||
|
||||
return (
|
||||
<InputPanel id={id}>
|
||||
<Container hideInput={hideInput}>
|
||||
@@ -235,12 +239,9 @@ export default function CurrencyInputPanel({
|
||||
</InputRow>
|
||||
</Container>
|
||||
{!disableTokenSelect && (
|
||||
<SearchModal
|
||||
<TokenSearchModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={() => {
|
||||
setModalOpen(false)
|
||||
}}
|
||||
filterType="tokens"
|
||||
onDismiss={handleDismissSearch}
|
||||
onTokenSelect={onTokenSelection}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hiddenToken={token?.address}
|
||||
|
||||
@@ -13,7 +13,7 @@ interface DoubleTokenLogoProps {
|
||||
margin?: boolean
|
||||
size?: number
|
||||
a0: string
|
||||
a1: string
|
||||
a1?: string
|
||||
}
|
||||
|
||||
const HigherLogo = styled(TokenLogo)`
|
||||
@@ -28,7 +28,7 @@ export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: D
|
||||
return (
|
||||
<TokenWrapper sizeraw={size} margin={margin}>
|
||||
<HigherLogo address={a0} size={size.toString() + 'px'} />
|
||||
<CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />
|
||||
{a1 && <CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />}
|
||||
</TokenWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
70
src/components/Header/VersionSwitch.tsx
Normal file
70
src/components/Header/VersionSwitch.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
const VersionLabel = styled.span<{ enabled: boolean }>`
|
||||
padding: ${({ enabled }) => (enabled ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
|
||||
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.primary3)};
|
||||
}
|
||||
`
|
||||
|
||||
interface VersionToggleProps extends React.ComponentProps<typeof Link> {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
|
||||
border-radius: 16px;
|
||||
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
|
||||
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
|
||||
background: ${({ theme }) => theme.primary5};
|
||||
border: 1px solid ${({ theme }) => theme.primary4};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export function VersionSwitch() {
|
||||
const version = useToggledVersion()
|
||||
const location = useLocation()
|
||||
const query = useParsedQueryString()
|
||||
const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send'
|
||||
|
||||
const toggleDest = useMemo(() => {
|
||||
return versionSwitchAvailable
|
||||
? {
|
||||
...location,
|
||||
search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}`
|
||||
}
|
||||
: location
|
||||
}, [location, query, version, versionSwitchAvailable])
|
||||
|
||||
const handleClick = useCallback(
|
||||
e => {
|
||||
if (!versionSwitchAvailable) e.preventDefault()
|
||||
},
|
||||
[versionSwitchAvailable]
|
||||
)
|
||||
|
||||
return (
|
||||
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
|
||||
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
|
||||
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { Link as HistoryLink } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import Row from '../Row'
|
||||
import Menu from '../Menu'
|
||||
import Web3Status from '../Web3Status'
|
||||
|
||||
import { ExternalLink, StyledInternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { WETH, ChainId } from '@uniswap/sdk'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { YellowCard } from '../Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import Logo from '../../assets/svg/logo.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import LogoDark from '../../assets/svg/logo_white.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import { ExternalLink, StyledInternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
import Settings from '../Settings'
|
||||
import Menu from '../Menu'
|
||||
|
||||
import Row, { RowBetween } from '../Row'
|
||||
import Web3Status from '../Web3Status'
|
||||
import { VersionSwitch } from './VersionSwitch'
|
||||
|
||||
const HeaderFrame = styled.div`
|
||||
display: flex;
|
||||
@@ -31,15 +32,12 @@ const HeaderFrame = styled.div`
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
z-index: 2;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
padding: 12px 0 0 0;
|
||||
width: calc(100%);
|
||||
position: relative;
|
||||
`};
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const HeaderElement = styled.div`
|
||||
@@ -122,32 +120,18 @@ const MigrateBanner = styled(AutoColumn)`
|
||||
`};
|
||||
`
|
||||
|
||||
const VersionLabel = styled.span<{ isV2?: boolean }>`
|
||||
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
|
||||
}
|
||||
`
|
||||
const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
|
||||
[ChainId.MAINNET]: null,
|
||||
[ChainId.RINKEBY]: 'Rinkeby',
|
||||
[ChainId.ROPSTEN]: 'Ropsten',
|
||||
[ChainId.GÖRLI]: 'Görli',
|
||||
[ChainId.KOVAN]: 'Kovan'
|
||||
}
|
||||
|
||||
const VersionToggle = styled.a`
|
||||
border-radius: 16px;
|
||||
background: ${({ theme }) => theme.primary5};
|
||||
border: 1px solid ${({ theme }) => theme.primary4};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
const BalanceWrapper = styled.div`
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Header() {
|
||||
@@ -187,33 +171,24 @@ export default function Header() {
|
||||
</TitleText>
|
||||
)}
|
||||
</Title>
|
||||
<TestnetWrapper style={{ pointerEvents: 'auto' }}>
|
||||
{!isMobile && (
|
||||
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
|
||||
<VersionLabel isV2={true}>V2</VersionLabel>
|
||||
<VersionLabel isV2={false}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)}
|
||||
</TestnetWrapper>
|
||||
<TestnetWrapper style={{ pointerEvents: 'auto' }}>{!isMobile && <VersionSwitch />}</TestnetWrapper>
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
|
||||
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
<BalanceWrapper>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
</BalanceWrapper>
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
<div style={{ pointerEvents: 'auto' }}>
|
||||
<Menu />
|
||||
</div>
|
||||
<Settings />
|
||||
<Menu />
|
||||
</HeaderElement>
|
||||
</RowBetween>
|
||||
</HeaderFrame>
|
||||
|
||||
@@ -77,9 +77,7 @@ const MenuItem = styled(ExternalLink)`
|
||||
}
|
||||
`
|
||||
|
||||
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
|
||||
: 'https://github.com/Uniswap/uniswap-frontend'
|
||||
const CODE_LINK = 'https://github.com/Uniswap/uniswap-frontend'
|
||||
|
||||
export default function Menu() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
|
||||
@@ -18,6 +18,7 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
@@ -69,12 +70,10 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
border-radius: 20px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
width: 65vw;
|
||||
max-height: 65vh;
|
||||
margin: 0;
|
||||
`}
|
||||
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
|
||||
width: 85vw;
|
||||
max-height: 66vh;
|
||||
${mobile &&
|
||||
css`
|
||||
width: 100vw;
|
||||
@@ -86,14 +85,6 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
}
|
||||
`
|
||||
|
||||
const HiddenCloseButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
`
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
@@ -118,21 +109,13 @@ export default function Modal({
|
||||
leave: { opacity: 0 }
|
||||
})
|
||||
|
||||
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] }))
|
||||
const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } }))
|
||||
const bind = useGesture({
|
||||
onDrag: state => {
|
||||
let velocity = state.velocity
|
||||
if (velocity < 1) {
|
||||
velocity = 1
|
||||
}
|
||||
if (velocity > 8) {
|
||||
velocity = 8
|
||||
}
|
||||
set({
|
||||
xy: state.down ? state.movement : [0, 0],
|
||||
config: { mass: 1, tension: 210, friction: 20 }
|
||||
y: state.down ? state.movement[1] : 0
|
||||
})
|
||||
if (velocity > 3 && state.direction[1] > 0) {
|
||||
if (state.velocity > 3 && state.direction[1] > 0) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
@@ -151,6 +134,8 @@ export default function Modal({
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={true}
|
||||
>
|
||||
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||
{initialFocusRef ? null : <div tabIndex={1} />}
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
|
||||
@@ -163,18 +148,17 @@ export default function Modal({
|
||||
<animated.div
|
||||
{...bind()}
|
||||
style={{
|
||||
transform: (xy as any).interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`)
|
||||
transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`)
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
ariaLabel="test"
|
||||
aria-label="dialog content"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile ?? undefined}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</animated.div>
|
||||
@@ -192,8 +176,13 @@ export default function Modal({
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
<StyledDialogContent
|
||||
aria-label="dialog content"
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
|
||||
75
src/components/PositionCard/V1.tsx
Normal file
75
src/components/PositionCard/V1.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
|
||||
import { Text } from 'rebass'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { FixedHeightRow, HoverCard } from './index'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
interface PositionCardProps extends RouteComponentProps<{}> {
|
||||
token: Token
|
||||
V1LiquidityBalance: TokenAmount
|
||||
}
|
||||
|
||||
function V1PositionCard({ token, V1LiquidityBalance, history }: PositionCardProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<DoubleTokenLogo a0={token.address} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
|
||||
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={12}
|
||||
fontWeight={500}
|
||||
ml="0.5rem"
|
||||
px="0.75rem"
|
||||
py="0.25rem"
|
||||
style={{ borderRadius: '1rem' }}
|
||||
backgroundColor={theme.yellow1}
|
||||
color={'black'}
|
||||
>
|
||||
V1
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonSecondary
|
||||
width="68%"
|
||||
onClick={() => {
|
||||
history.push(`/migrate/v1/${V1LiquidityBalance.token.address}`)
|
||||
}}
|
||||
>
|
||||
Migrate
|
||||
</ButtonSecondary>
|
||||
|
||||
<ButtonSecondary
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
width="28%"
|
||||
onClick={() => {
|
||||
history.push(`/remove/v1/${V1LiquidityBalance.token.address}`)
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</ButtonSecondary>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(V1PositionCard)
|
||||
@@ -17,12 +17,13 @@ import { AutoColumn } from '../Column'
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||
import { Dots } from '../swap/styleds'
|
||||
|
||||
const FixedHeightRow = styled(RowBetween)`
|
||||
export const FixedHeightRow = styled(RowBetween)`
|
||||
height: 24px;
|
||||
`
|
||||
|
||||
const HoverCard = styled(Card)`
|
||||
export const HoverCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
|
||||
@@ -72,7 +73,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Your current position
|
||||
Your position
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
@@ -96,7 +97,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
</Text>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token0?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
@@ -111,7 +111,6 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
</Text>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token1?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
@@ -134,7 +133,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
{!token0 || !token1 ? <Dots>Loading</Dots> : `${token0.symbol}/${token1.symbol}`}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
@@ -158,7 +157,7 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
@@ -176,32 +175,28 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
|
||||
<AutoRow justify="center" marginTop={'10px'}>
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
|
||||
|
||||
@@ -25,13 +25,7 @@ export default function PairList({
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
itemSize={54}
|
||||
height={500}
|
||||
itemCount={pairs.length}
|
||||
width="100%"
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
>
|
||||
<FixedSizeList itemSize={56} height={500} itemCount={pairs.length} width="100%" style={{ flex: '1' }}>
|
||||
{({ index, style }) => {
|
||||
const pair = pairs[index]
|
||||
|
||||
|
||||
141
src/components/SearchModal/PairSearchModal.tsx
Normal file
141
src/components/SearchModal/PairSearchModal.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Pair } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs } from '../../state/user/hooks'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, StyledInternalLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { filterPairs } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { pairComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
|
||||
interface PairSearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
}
|
||||
|
||||
function PairSearchModal({ history, isOpen, onDismiss }: PairSearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
return filterPairs(allPairs, searchQuery)
|
||||
}, [allPairs, searchQuery])
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
const query = searchQuery.toLowerCase()
|
||||
const queryMatches = (pair: Pair): boolean =>
|
||||
pair.token0.symbol.toLowerCase() === query || pair.token1.symbol.toLowerCase() === query
|
||||
return filteredPairs.sort((a, b): number => {
|
||||
const [aMatches, bMatches] = [queryMatches(a), queryMatches(b)]
|
||||
if (aMatches && !bMatches) return -1
|
||||
if (bMatches && !aMatches) return 1
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
return pairComparator(a, b, balanceA, balanceB)
|
||||
})
|
||||
}, [searchQuery, filteredPairs, allPairBalances])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
minHeight={70}
|
||||
>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a pool
|
||||
<QuestionHelper text="Find a pair by searching for its name below." />
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={onInput}
|
||||
/>
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Pool Name
|
||||
</Text>
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<PairList
|
||||
pairs={sortedPairList}
|
||||
focusTokenAddress={focusedToken?.address}
|
||||
onAddLiquidity={selectPair}
|
||||
onSelectPair={selectPair}
|
||||
pairBalances={allPairBalances}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
||||
</Text>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(PairSearchModal)
|
||||
@@ -1,24 +1,20 @@
|
||||
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import { JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
|
||||
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
||||
const address = isAddress(tokenAddress)
|
||||
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
||||
}
|
||||
import { isDefaultToken, isCustomAddedToken } from '../../utils'
|
||||
|
||||
export default function TokenList({
|
||||
tokens,
|
||||
@@ -27,39 +23,42 @@ export default function TokenList({
|
||||
onTokenSelect,
|
||||
otherToken,
|
||||
showSendWithSwap,
|
||||
onRemoveAddedToken,
|
||||
otherSelectedText,
|
||||
hideRemove
|
||||
otherSelectedText
|
||||
}: {
|
||||
tokens: Token[]
|
||||
selectedToken: string
|
||||
allTokenBalances: { [tokenAddress: string]: TokenAmount }
|
||||
onTokenSelect: (tokenAddress: string) => void
|
||||
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
|
||||
otherToken: string
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedText: string
|
||||
hideRemove?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const allTokens = useAllTokens()
|
||||
const addToken = useAddUserToken()
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return <ModalInfo>{t('noToken')}</ModalInfo>
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={500}
|
||||
itemCount={tokens.length}
|
||||
itemSize={50}
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
itemSize={56}
|
||||
style={{ flex: '1' }}
|
||||
itemKey={index => tokens[index].address}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const { address, symbol } = tokens[index]
|
||||
const token = tokens[index]
|
||||
const { address, symbol } = token
|
||||
|
||||
const customAdded = !isDefaultToken(address, chainId)
|
||||
const isDefault = isDefaultToken(token)
|
||||
const customAdded = isCustomAddedToken(allTokens, token)
|
||||
const balance = allTokenBalances[address]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
@@ -81,22 +80,36 @@ export default function TokenList({
|
||||
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||
{customAdded && !hideRemove && (
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
onRemoveAddedToken(chainId, address)
|
||||
}}
|
||||
style={{ marginLeft: '4px', fontWeight: 400 }}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
)}
|
||||
{customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Added by user
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
removeToken(chainId, address)
|
||||
}}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
{!isDefault && !customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Found by address
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
addToken(token)
|
||||
}}
|
||||
>
|
||||
(Add)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
</FadedSpan>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
<AutoColumn gap="4px" justify="end">
|
||||
<AutoColumn>
|
||||
{balance ? (
|
||||
<Text>
|
||||
{zeroBalance && showSendWithSwap ? (
|
||||
|
||||
185
src/components/SearchModal/TokenSearchModal.tsx
Normal file
185
src/components/SearchModal/TokenSearchModal.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterTokens } from './filtering'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface TokenSearchModalProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
export default function TokenSearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: TokenSearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
const searchToken = useToken(searchQuery)
|
||||
const searchTokenBalance = useTokenBalanceTreatingWETHasETH(account, searchToken)
|
||||
const allTokenBalances_ = useAllTokenBalancesTreatingWETHasETH()
|
||||
const allTokenBalances = searchToken
|
||||
? {
|
||||
[searchToken.address]: searchTokenBalance
|
||||
}
|
||||
: allTokenBalances_ ?? {}
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [searchToken, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
...(searchToken ? [searchToken] : []),
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, searchQuery, searchToken, tokenComparator])
|
||||
|
||||
const handleTokenSelect = useCallback(
|
||||
(address: string) => {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
},
|
||||
[onDismiss, onTokenSelect]
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
const handleInput = useCallback(event => {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
setTooltipOpen(false)
|
||||
}, [])
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
minHeight={70}
|
||||
>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text="Find a token by searching for its name or symbol or by pasting its address below."
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={handleInput}
|
||||
onBlur={closeTooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<TokenList
|
||||
tokens={filteredSortedTokens}
|
||||
allTokenBalances={allTokenBalances}
|
||||
onTokenSelect={handleTokenSelect}
|
||||
otherSelectedText={otherSelectedText}
|
||||
otherToken={otherSelectedTokenAddress}
|
||||
selectedToken={hiddenToken}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton, StyledInternalLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterPairs, filterTokens } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { useTokenComparator, pairComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface SearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
filterType?: 'tokens'
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
function SearchModal({
|
||||
history,
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
filterType,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: SearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const isTokenView = filterType === 'tokens'
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
|
||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [isTokenView, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, isTokenView, searchQuery, tokenComparator])
|
||||
|
||||
function _onTokenSelect(address: string) {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return allPairs.sort((a, b): number => {
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
return pairComparator(a, b, balanceA, balanceB)
|
||||
})
|
||||
}, [isTokenView, allPairs, allPairBalances])
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return filterPairs(sortedPairList, searchQuery)
|
||||
}, [isTokenView, searchQuery, sortedPairList])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{isTokenView ? 'Select a token' : 'Select a pool'}
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text={
|
||||
isTokenView
|
||||
? 'Find a token by searching for its name or symbol or by pasting its address below.'
|
||||
: 'Find a pair by searching for its name below.'
|
||||
}
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={onInput}
|
||||
onBlur={closeTooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{isTokenView ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
{isTokenView && (
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
)}
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
{isTokenView ? (
|
||||
<TokenList
|
||||
tokens={filteredSortedTokens}
|
||||
allTokenBalances={allTokenBalances}
|
||||
onRemoveAddedToken={removeTokenByAddress}
|
||||
onTokenSelect={_onTokenSelect}
|
||||
otherSelectedText={otherSelectedText}
|
||||
otherToken={otherSelectedTokenAddress}
|
||||
selectedToken={hiddenToken}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hideRemove={Boolean(isAddress(searchQuery))}
|
||||
/>
|
||||
) : (
|
||||
<PairList
|
||||
pairs={filteredPairs}
|
||||
focusTokenAddress={focusedToken?.address}
|
||||
onAddLiquidity={selectPair}
|
||||
onSelectPair={selectPair}
|
||||
pairBalances={allPairBalances}
|
||||
/>
|
||||
)}
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
{isTokenView ? (
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
) : (
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(SearchModal)
|
||||
@@ -8,8 +8,8 @@ export const ModalInfo = styled.div`
|
||||
padding: 1rem 1rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
min-height: 200px;
|
||||
`
|
||||
|
||||
export const FadedSpan = styled(RowFixed)`
|
||||
@@ -50,12 +50,9 @@ export const PaddedColumn = styled(AutoColumn)`
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
const PaddedItem = styled(RowBetween)`
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(PaddedItem)`
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
|
||||
253
src/components/Settings/index.tsx
Normal file
253
src/components/Settings/index.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import React, { useRef, useEffect, useContext, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import {
|
||||
useUserSlippageTolerance,
|
||||
useExpertModeManager,
|
||||
useUserDeadline,
|
||||
useDarkModeManager
|
||||
} from '../../state/user/hooks'
|
||||
import SlippageTabs from '../SlippageTabs'
|
||||
import { RowFixed, RowBetween } from '../Row'
|
||||
import { TYPE } from '../../theme'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import Toggle from '../Toggle'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ButtonError } from '../Button'
|
||||
import { useSettingsMenuOpen, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { Text } from 'rebass'
|
||||
import Modal from '../Modal'
|
||||
|
||||
const StyledMenuIcon = styled(Settings)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledCloseIcon = styled(X)`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
> * {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
const EmojiWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
right: 0px;
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const MenuFlyout = styled.span`
|
||||
min-width: 20.125rem;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.01);
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
right: 0rem;
|
||||
z-index: 100;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
min-width: 18.125rem;
|
||||
right: -46px;
|
||||
`};
|
||||
`
|
||||
|
||||
const Break = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
const ModalContentWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
export default function SettingsTab() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const open = useSettingsMenuOpen()
|
||||
const toggle = useToggleSettingsMenu()
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
const [userSlippageTolerance, setUserslippageTolerance] = useUserSlippageTolerance()
|
||||
|
||||
const [deadline, setDeadline] = useUserDeadline()
|
||||
|
||||
const [expertMode, toggleExpertMode] = useExpertModeManager()
|
||||
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = e => {
|
||||
if (node.current?.contains(e.target) ?? false) {
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [open, toggle])
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)}>
|
||||
<ModalContentWrapper>
|
||||
<AutoColumn gap="lg">
|
||||
<RowBetween style={{ padding: '0 2rem' }}>
|
||||
<div />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Are you sure?
|
||||
</Text>
|
||||
<StyledCloseIcon onClick={() => setShowConfirmation(false)} />
|
||||
</RowBetween>
|
||||
<Break />
|
||||
<AutoColumn gap="lg" style={{ padding: '0 2rem' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Expert mode turns off the confirm transaction prompt and allows high slippage trades that often result
|
||||
in bad rates and lost funds.
|
||||
</Text>
|
||||
<Text fontWeight={600} fontSize={20}>
|
||||
ONLY USE THIS MODE IF YOU KNOW WHAT YOU ARE DOING.
|
||||
</Text>
|
||||
<ButtonError
|
||||
error={true}
|
||||
padding={'12px'}
|
||||
onClick={() => {
|
||||
if (window.prompt(`Please type the word "confirm" to enable expert mode.`) === 'confirm') {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
Turn On Expert Mode
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</ModalContentWrapper>
|
||||
</Modal>
|
||||
<StyledMenuButton onClick={toggle}>
|
||||
<StyledMenuIcon />
|
||||
{expertMode && (
|
||||
<EmojiWrapper>
|
||||
<span role="img" aria-label="wizard-icon">
|
||||
🧙
|
||||
</span>
|
||||
</EmojiWrapper>
|
||||
)}
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<MenuFlyout>
|
||||
<AutoColumn gap="md" style={{ padding: '1rem' }}>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Transaction Settings
|
||||
</Text>
|
||||
<SlippageTabs
|
||||
rawSlippage={userSlippageTolerance}
|
||||
setRawSlippage={setUserslippageTolerance}
|
||||
deadline={deadline}
|
||||
setDeadline={setDeadline}
|
||||
/>
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Interface Settings
|
||||
</Text>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Expert Mode
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Bypasses confirmation modals and allows high slippage trades. Use at your own risk." />
|
||||
</RowFixed>
|
||||
<Toggle
|
||||
isActive={expertMode}
|
||||
toggle={
|
||||
expertMode
|
||||
? () => {
|
||||
toggleExpertMode()
|
||||
setShowConfirmation(false)
|
||||
}
|
||||
: () => setShowConfirmation(true)
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Toggle Dark Mode
|
||||
</TYPE.black>
|
||||
</RowFixed>
|
||||
<Toggle isActive={darkMode} toggle={toggleDarkMode} />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</MenuFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
)
|
||||
}
|
||||
@@ -78,10 +78,6 @@ const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }
|
||||
}
|
||||
`
|
||||
|
||||
const SlippageSelector = styled.div`
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
export interface SlippageTabsProps {
|
||||
rawSlippage: number
|
||||
setRawSlippage: (rawSlippage: number) => void
|
||||
@@ -146,15 +142,14 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Set slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
|
||||
<SlippageSelector>
|
||||
<AutoColumn gap="md">
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
@@ -220,16 +215,16 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
: 'Your transaction may be frontrun'}
|
||||
</RowBetween>
|
||||
)}
|
||||
</SlippageSelector>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Deadline
|
||||
Transaction deadline
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
|
||||
</RowFixed>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<RowFixed>
|
||||
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
||||
<Input
|
||||
color={!!deadlineError ? 'red' : undefined}
|
||||
@@ -246,6 +241,6 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
</TYPE.body>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
</>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
41
src/components/Toggle/index.tsx
Normal file
41
src/components/Toggle/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.text4) : 'none')};
|
||||
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.text2) : theme.text3)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
const StyledToggle = styled.a<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Toggle({ isActive, toggle }: ToggleProps) {
|
||||
return (
|
||||
<StyledToggle isActive={isActive} target="_self" onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} isOnSwitch={true}>
|
||||
On
|
||||
</ToggleElement>
|
||||
<ToggleElement isActive={!isActive} isOnSwitch={false}>
|
||||
Off
|
||||
</ToggleElement>
|
||||
</StyledToggle>
|
||||
)
|
||||
}
|
||||
@@ -3,13 +3,12 @@ import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { getEtherscanLink, isDefaultToken } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
@@ -68,9 +67,8 @@ interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'
|
||||
|
||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const isDefaultToken = Boolean(
|
||||
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address]
|
||||
)
|
||||
|
||||
const isDefault = isDefaultToken(token)
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
@@ -80,7 +78,7 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (isDefaultToken || !token || !chainId) return false
|
||||
if (isDefault || !token || !chainId) return false
|
||||
|
||||
return Object.keys(allTokens).some(tokenAddress => {
|
||||
const userToken = allTokens[tokenAddress]
|
||||
@@ -89,9 +87,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
|
||||
}
|
||||
return userToken.symbol.toLowerCase() === tokenSymbol || userToken.name.toLowerCase() === tokenName
|
||||
})
|
||||
}, [isDefaultToken, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (isDefaultToken || !token || dismissed) return null
|
||||
if (isDefault || !token || dismissed) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
@@ -51,13 +51,16 @@ export default function TxnPopup({
|
||||
isRunning ? delay : null
|
||||
)
|
||||
|
||||
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
|
||||
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
|
||||
{success ? (
|
||||
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} />
|
||||
) : (
|
||||
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
|
||||
)}
|
||||
<AutoRow onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||
</div>
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>
|
||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
||||
|
||||
@@ -15,7 +15,7 @@ const InfoCard = styled.button<{ active?: boolean }>`
|
||||
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)};
|
||||
`
|
||||
|
||||
const OptionCard = styled(InfoCard)`
|
||||
const OptionCard = styled(InfoCard as any)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -30,7 +30,7 @@ const OptionCardLeft = styled.div`
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const OptionCardClickable = styled(OptionCard)<{ clickable?: boolean }>`
|
||||
const OptionCardClickable = styled(OptionCard as any)<{ clickable?: boolean }>`
|
||||
margin-top: 0;
|
||||
&:hover {
|
||||
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
|
||||
@@ -114,7 +114,6 @@ export default function Option({
|
||||
<OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color}>
|
||||
{' '}
|
||||
{active ? (
|
||||
<CircleWrapper>
|
||||
<GreenCircle>
|
||||
|
||||
@@ -3,8 +3,7 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Option from './Option'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import WalletConnectData from './WalletConnectData'
|
||||
import { walletconnect, injected } from '../../connectors'
|
||||
import { injected } from '../../connectors'
|
||||
import { darken } from 'polished'
|
||||
import Loader from '../Loader'
|
||||
|
||||
@@ -65,28 +64,22 @@ const LoadingWrapper = styled.div`
|
||||
`
|
||||
|
||||
export default function PendingView({
|
||||
uri = '',
|
||||
size,
|
||||
connector,
|
||||
error = false,
|
||||
setPendingError,
|
||||
tryActivation
|
||||
}: {
|
||||
uri?: string
|
||||
size?: number
|
||||
connector?: AbstractConnector
|
||||
error?: boolean
|
||||
setPendingError: (error: boolean) => void
|
||||
tryActivation: (connector: AbstractConnector) => void
|
||||
}) {
|
||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
||||
const isMetamask = window?.ethereum?.isMetaMask
|
||||
|
||||
return (
|
||||
<PendingSection>
|
||||
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
|
||||
<LoadingMessage error={error}>
|
||||
<LoadingWrapper>
|
||||
{!error && <StyledLoader />}
|
||||
{error ? (
|
||||
<ErrorGroup>
|
||||
<div>Error connecting.</div>
|
||||
@@ -99,10 +92,11 @@ export default function PendingView({
|
||||
Try Again
|
||||
</ErrorButton>
|
||||
</ErrorGroup>
|
||||
) : connector === walletconnect ? (
|
||||
'Scan QR code with a compatible wallet...'
|
||||
) : (
|
||||
'Initializing...'
|
||||
<>
|
||||
<StyledLoader />
|
||||
Initializing...
|
||||
</>
|
||||
)}
|
||||
</LoadingWrapper>
|
||||
</LoadingMessage>
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import QRCode from 'qrcode.react'
|
||||
|
||||
const QRCodeWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
interface WalletConnectDataProps {
|
||||
uri?: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
|
||||
return <QRCodeWrapper>{uri && <QRCode size={size} value={uri} />}</QRCodeWrapper>
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import ReactGA from 'react-ga'
|
||||
import styled from 'styled-components'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
||||
|
||||
@@ -15,8 +14,9 @@ import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, walletconnect, fortmatic, portis } from '../../connectors'
|
||||
import { injected, fortmatic, portis } from '../../connectors'
|
||||
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
@@ -152,19 +152,6 @@ export default function WalletModal({
|
||||
}
|
||||
}, [walletModalOpen])
|
||||
|
||||
// set up uri listener for walletconnect
|
||||
const [uri, setUri] = useState()
|
||||
useEffect(() => {
|
||||
const activateWC = uri => {
|
||||
setUri(uri)
|
||||
// setWalletView(WALLET_VIEWS.PENDING)
|
||||
}
|
||||
walletconnect.on(URI_AVAILABLE, activateWC)
|
||||
return () => {
|
||||
walletconnect.off(URI_AVAILABLE, activateWC)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// close modal when a connection is successful
|
||||
const activePrevious = usePrevious(active)
|
||||
const connectorPrevious = usePrevious(connector)
|
||||
@@ -190,6 +177,12 @@ export default function WalletModal({
|
||||
})
|
||||
setPendingWallet(connector) // set wallet for pending view
|
||||
setWalletView(WALLET_VIEWS.PENDING)
|
||||
|
||||
// if the connector is walletconnect and the user has already tried to connect, manually reset the connector
|
||||
if (connector instanceof WalletConnectConnector && connector.walletConnectProvider?.wc?.uri) {
|
||||
connector.walletConnectProvider = undefined
|
||||
}
|
||||
|
||||
activate(connector, undefined, true).catch(error => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
@@ -345,8 +338,6 @@ export default function WalletModal({
|
||||
<ContentWrapper>
|
||||
{walletView === WALLET_VIEWS.PENDING ? (
|
||||
<PendingView
|
||||
uri={uri}
|
||||
size={220}
|
||||
connector={pendingWallet}
|
||||
error={pendingError}
|
||||
setPendingError={setPendingError}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronUp, ChevronRight } from 'react-feather'
|
||||
import { Text, Flex } from 'rebass'
|
||||
import { ChevronRight } from 'react-feather'
|
||||
import { Flex } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { SectionBreak } from './styleds'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
|
||||
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
@@ -61,39 +61,27 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
|
||||
<SectionBreak />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
|
||||
export interface AdvancedSwapDetailsProps {
|
||||
trade?: Trade
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
|
||||
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const showRoute = trade?.route?.path?.length > 2
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
|
||||
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Hide Advanced
|
||||
</Text>
|
||||
<ChevronUp color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
|
||||
<SectionBreak />
|
||||
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
|
||||
|
||||
<SlippageTabs {...slippageTabProps} />
|
||||
|
||||
{trade?.route?.path?.length > 2 && (
|
||||
<AutoColumn style={{ padding: '0 20px' }}>
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
|
||||
{showRoute && <SectionBreak />}
|
||||
{showRoute && (
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Route
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { RowBetween } from '../Row'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||
import { AdvancedDropdown } from './styleds'
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({
|
||||
showAdvanced,
|
||||
setShowAdvanced,
|
||||
...rest
|
||||
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
|
||||
showAdvanced: boolean
|
||||
setShowAdvanced: (showAdvanced: boolean) => void
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
padding-top: calc(16px + 2rem);
|
||||
padding-bottom: 20px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
|
||||
transform: ${({ show }) => (show ? 'translateY(0%)' : 'translateY(-100%)')};
|
||||
transition: transform 300ms ease-in-out;
|
||||
`
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
|
||||
const lastTrade = useLast(trade)
|
||||
|
||||
return (
|
||||
<AdvancedDropdown>
|
||||
{showAdvanced ? (
|
||||
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
|
||||
) : (
|
||||
<CursorPointer>
|
||||
<RowBetween onClick={() => setShowAdvanced(true)} padding={'8px 20px'} id="show-advanced">
|
||||
<Text fontSize={16} fontWeight={500} style={{ userSelect: 'none' }}>
|
||||
Show Advanced
|
||||
</Text>
|
||||
<ChevronDown color={theme.text2} />
|
||||
</RowBetween>
|
||||
</CursorPointer>
|
||||
)}
|
||||
</AdvancedDropdown>
|
||||
<AdvancedDetailsFooter show={Boolean(trade)}>
|
||||
<AdvancedSwapDetails {...rest} trade={lastTrade} />
|
||||
</AdvancedDetailsFooter>
|
||||
)
|
||||
}
|
||||
|
||||
40
src/components/swap/BetterTradeLink.tsx
Normal file
40
src/components/swap/BetterTradeLink.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { stringify } from 'qs'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { useLocation } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import { DEFAULT_VERSION, Version } from '../../hooks/useToggledVersion'
|
||||
|
||||
import { StyledInternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function BetterTradeLink({ version }: { version: Version }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const location = useLocation()
|
||||
const search = useParsedQueryString()
|
||||
|
||||
const linkDestination = useMemo(() => {
|
||||
return {
|
||||
...location,
|
||||
search: `?${stringify({
|
||||
...search,
|
||||
use: version !== DEFAULT_VERSION ? version : undefined
|
||||
})}`
|
||||
}
|
||||
}, [location, search, version])
|
||||
|
||||
return (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on{' '}
|
||||
<StyledInternalLink to={linkDestination}>
|
||||
<b>Uniswap {version.toUpperCase()} ↗</b>
|
||||
</StyledInternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
export function PriceSlippageWarningCard({ priceSlippage }: { priceSlippage: Percent }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<YellowCard style={{ padding: '20px', paddingTop: '10px' }}>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<RowFixed style={{ paddingTop: '8px' }}>
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>{' '}
|
||||
<Text fontWeight={500} marginLeft="4px" color={theme.text1}>
|
||||
Price Warning
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<Text lineHeight="145.23%;" fontSize={16} fontWeight={400} color={theme.text1}>
|
||||
This trade will move the price by ~{priceSlippage.toFixed(2)}%.
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Trade } from '@uniswap/sdk'
|
||||
import { Price, Token } from '@uniswap/sdk'
|
||||
import { useContext } from 'react'
|
||||
import { Repeat } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -7,20 +7,19 @@ import { ThemeContext } from 'styled-components'
|
||||
import { StyledBalanceMaxMini } from './styleds'
|
||||
|
||||
interface TradePriceProps {
|
||||
trade?: Trade
|
||||
price?: Price
|
||||
inputToken?: Token
|
||||
outputToken?: Token
|
||||
showInverted: boolean
|
||||
setShowInverted: (showInverted: boolean) => void
|
||||
}
|
||||
|
||||
export default function TradePrice({ trade, showInverted, setShowInverted }: TradePriceProps) {
|
||||
export default function TradePrice({ price, inputToken, outputToken, showInverted, setShowInverted }: TradePriceProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const inputToken = trade?.inputAmount?.token
|
||||
const outputToken = trade?.outputAmount?.token
|
||||
|
||||
const price = showInverted
|
||||
? trade?.executionPrice?.toSignificant(6)
|
||||
: trade?.executionPrice?.invert()?.toSignificant(6)
|
||||
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
|
||||
|
||||
const show = Boolean(inputToken && outputToken)
|
||||
const label = showInverted
|
||||
? `${outputToken?.symbol} per ${inputToken?.symbol}`
|
||||
: `${inputToken?.symbol} per ${outputToken?.symbol}`
|
||||
@@ -32,10 +31,16 @@ export default function TradePrice({ trade, showInverted, setShowInverted }: Tra
|
||||
color={theme.text2}
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
>
|
||||
{price && `${price} ${label}`}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
{show ? (
|
||||
<>
|
||||
{formattedPrice ?? '-'} {label}
|
||||
<StyledBalanceMaxMini onClick={() => setShowInverted(!showInverted)}>
|
||||
<Repeat size={14} />
|
||||
</StyledBalanceMaxMini>
|
||||
</>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { YellowCard } from '../Card'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBetter: string }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return v1TradeLinkIfBetter ? (
|
||||
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
|
||||
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
|
||||
There is a better price for this trade on{' '}
|
||||
<ExternalLink href={v1TradeLinkIfBetter}>
|
||||
<b>Uniswap V1 ↗</b>
|
||||
</ExternalLink>
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null
|
||||
}
|
||||
@@ -21,19 +21,6 @@ export const ArrowWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export const AdvancedDropdown = styled.div`
|
||||
padding-top: calc(10px + 2rem);
|
||||
padding-bottom: 10px;
|
||||
margin-top: -2rem;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
z-index: -1;
|
||||
`
|
||||
|
||||
export const SectionBreak = styled.div`
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
@@ -45,9 +32,15 @@ export const BottomGrouping = styled.div`
|
||||
position: relative;
|
||||
`
|
||||
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
|
||||
color: ${({ theme, severity }) =>
|
||||
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.text1 : theme.green1};
|
||||
severity === 3 || severity === 4
|
||||
? theme.red1
|
||||
: severity === 2
|
||||
? theme.yellow2
|
||||
: severity === 1
|
||||
? theme.text1
|
||||
: theme.green1};
|
||||
`
|
||||
|
||||
export const InputGroup = styled(AutoColumn)`
|
||||
@@ -65,7 +58,7 @@ export const StyledNumerical = styled(NumericalInput)`
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
export const StyledBalanceMaxMini = styled.button<{ active?: boolean }>`
|
||||
export const StyledBalanceMaxMini = styled.button`
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
|
||||
@@ -27,7 +27,7 @@ export const injected = new InjectedConnector({
|
||||
export const walletconnect = new WalletConnectConnector({
|
||||
rpc: { 1: NETWORK_URL },
|
||||
bridge: 'https://bridge.walletconnect.org',
|
||||
qrcode: false,
|
||||
qrcode: true,
|
||||
pollingInterval: POLLING_INTERVAL
|
||||
})
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
|
||||
]
|
||||
}
|
||||
|
||||
const MAINNET_WALLETS = {
|
||||
const TESTNET_CAPABLE_WALLETS = {
|
||||
INJECTED: {
|
||||
connector: injected,
|
||||
name: 'Injected',
|
||||
@@ -80,9 +80,9 @@ const MAINNET_WALLETS = {
|
||||
|
||||
export const SUPPORTED_WALLETS =
|
||||
process.env.REACT_APP_CHAIN_ID !== '1'
|
||||
? MAINNET_WALLETS
|
||||
? TESTNET_CAPABLE_WALLETS
|
||||
: {
|
||||
...MAINNET_WALLETS,
|
||||
...TESTNET_CAPABLE_WALLETS,
|
||||
...{
|
||||
WALLET_CONNECT: {
|
||||
connector: walletconnect,
|
||||
@@ -90,7 +90,8 @@ export const SUPPORTED_WALLETS =
|
||||
iconName: 'walletConnectIcon.svg',
|
||||
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
||||
href: null,
|
||||
color: '#4196FC'
|
||||
color: '#4196FC',
|
||||
mobile: true
|
||||
},
|
||||
WALLET_LINK: {
|
||||
connector: walletlink,
|
||||
@@ -151,12 +152,13 @@ export const ONE_BIPS = new Percent(JSBI.BigInt(1), JSBI.BigInt(10000))
|
||||
export const BIPS_BASE = JSBI.BigInt(10000)
|
||||
// used for warning states
|
||||
export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1%
|
||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||
|
||||
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(300), BIPS_BASE) // 3%
|
||||
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
|
||||
// if the price slippage exceeds this number, force the user to type 'confirm' to execute
|
||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(2500), BIPS_BASE) // 25%
|
||||
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
|
||||
// for non expert mode disable swaps above this
|
||||
export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BIPS_BASE) // 15%
|
||||
|
||||
// used to ensure the user doesn't send so much ETH so they end up with <.01
|
||||
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
|
||||
export const V1_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
|
||||
|
||||
@@ -19,6 +19,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
|
||||
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
|
||||
new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound'),
|
||||
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
|
||||
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
|
||||
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
|
||||
@@ -64,6 +65,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'),
|
||||
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'),
|
||||
@@ -94,6 +96,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
|
||||
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
|
||||
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
|
||||
new Token(ChainId.MAINNET, '0x0Ae055097C6d159879521C384F1D2123D1f195e6', 18, 'STAKE', 'STAKE'),
|
||||
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
|
||||
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
|
||||
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
|
||||
|
||||
@@ -3,29 +3,15 @@ import { useMemo } from 'react'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
|
||||
import { usePairContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult, useMultipleContractSingleData } from '../state/multicall/hooks'
|
||||
import { useMultipleContractSingleData } from '../state/multicall/hooks'
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
|
||||
/*
|
||||
* if loading, return undefined
|
||||
* if no pair created yet, return null
|
||||
* if pair already created (even if 0 reserves), return pair
|
||||
*/
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const contract = usePairContract(pairAddress, false)
|
||||
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
|
||||
|
||||
return useMemo(() => {
|
||||
if (loading || !tokenA || !tokenB) return undefined
|
||||
if (!reserves) return null
|
||||
const { reserve0, reserve1 } = reserves
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||
}, [loading, reserves, tokenA, tokenB])
|
||||
}
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
|
||||
const pairAddresses = useMemo(
|
||||
() =>
|
||||
@@ -51,3 +37,7 @@ export function usePairs(tokens: [Token | undefined, Token | undefined][]): (und
|
||||
})
|
||||
}, [results, tokens])
|
||||
}
|
||||
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
return usePairs([[tokenA, tokenB]])[0]
|
||||
}
|
||||
|
||||
119
src/data/V1.ts
119
src/data/V1.ts
@@ -1,27 +1,26 @@
|
||||
import { ChainId, JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../hooks'
|
||||
import { useAllTokens } from '../hooks/Tokens'
|
||||
import { useV1FactoryContract } from '../hooks/useContract'
|
||||
import { Version } from '../hooks/useToggledVersion'
|
||||
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
|
||||
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
|
||||
function useV1PairAddress(tokenAddress?: string): string | undefined {
|
||||
export function useV1ExchangeAddress(tokenAddress?: string): string | undefined {
|
||||
const contract = useV1FactoryContract()
|
||||
|
||||
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
|
||||
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
|
||||
}
|
||||
|
||||
class MockV1Pair extends Pair {
|
||||
readonly isV1: true = true
|
||||
}
|
||||
class MockV1Pair extends Pair {}
|
||||
|
||||
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
||||
const isWETH = token?.equals(WETH[token?.chainId])
|
||||
const isWETH: boolean = token && WETH[token.chainId] ? token.equals(WETH[token.chainId]) : false
|
||||
|
||||
// will only return an address on mainnet, and not for WETH
|
||||
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
|
||||
const v1PairAddress = useV1ExchangeAddress(isWETH ? undefined : token?.address)
|
||||
const tokenBalance = useTokenBalance(v1PairAddress, token)
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
||||
|
||||
@@ -41,9 +40,9 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
|
||||
return useMemo(
|
||||
() =>
|
||||
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
|
||||
const token = allTokens[args[ix][0]]
|
||||
if (result?.[0]) {
|
||||
memo[result?.[0]] = token
|
||||
if (result?.[0] && result[0] !== AddressZero) {
|
||||
const token = allTokens[args[ix][0]]
|
||||
memo[result[0]] = token
|
||||
}
|
||||
return memo
|
||||
}, {}) ?? {},
|
||||
@@ -53,12 +52,13 @@ export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
|
||||
|
||||
// returns whether any of the tokens in the user's token list have liquidity on v1
|
||||
export function useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||
const exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const fakeLiquidityTokens = useMemo(
|
||||
() => (chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
|
||||
() =>
|
||||
chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1', 'Uniswap V1')) : [],
|
||||
[chainId, exchanges]
|
||||
)
|
||||
|
||||
@@ -74,24 +74,23 @@ export function useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||
)
|
||||
}
|
||||
|
||||
export function useV1TradeLinkIfBetter(
|
||||
/**
|
||||
* Returns the trade to execute on V1 to go between input and output token
|
||||
*/
|
||||
export function useV1Trade(
|
||||
isExactIn?: boolean,
|
||||
input?: Token,
|
||||
output?: Token,
|
||||
exactAmount?: TokenAmount,
|
||||
v2Trade?: Trade,
|
||||
minimumDelta: Percent = new Percent('0')
|
||||
): string | undefined {
|
||||
inputToken?: Token,
|
||||
outputToken?: Token,
|
||||
exactAmount?: TokenAmount
|
||||
): Trade | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const isMainnet: boolean = chainId === ChainId.MAINNET
|
||||
|
||||
// get the mock v1 pairs
|
||||
const inputPair = useMockV1Pair(input)
|
||||
const outputPair = useMockV1Pair(output)
|
||||
const inputPair = useMockV1Pair(inputToken)
|
||||
const outputPair = useMockV1Pair(outputToken)
|
||||
|
||||
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET])
|
||||
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET])
|
||||
const inputIsWETH = (inputToken && chainId && WETH[chainId] && inputToken.equals(WETH[chainId])) ?? false
|
||||
const outputIsWETH = (outputToken && chainId && WETH[chainId] && outputToken.equals(WETH[chainId])) ?? false
|
||||
|
||||
// construct a direct or through ETH v1 route
|
||||
let pairs: Pair[] = []
|
||||
@@ -105,7 +104,7 @@ export function useV1TradeLinkIfBetter(
|
||||
pairs = [inputPair, outputPair]
|
||||
}
|
||||
|
||||
const route = input && pairs && pairs.length > 0 && new Route(pairs, input)
|
||||
const route = inputToken && pairs && pairs.length > 0 && new Route(pairs, inputToken)
|
||||
let v1Trade: Trade | undefined
|
||||
try {
|
||||
v1Trade =
|
||||
@@ -113,25 +112,53 @@ export function useV1TradeLinkIfBetter(
|
||||
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
|
||||
: undefined
|
||||
} catch {}
|
||||
return v1Trade
|
||||
}
|
||||
|
||||
let v1HasBetterTrade = false
|
||||
if (v1Trade) {
|
||||
if (isExactIn) {
|
||||
// discount the v1 output amount by minimumDelta
|
||||
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
|
||||
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
||||
} else {
|
||||
// inflate the v1 amount by minimumDelta
|
||||
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
|
||||
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
||||
}
|
||||
export function getTradeVersion(trade?: Trade): Version | undefined {
|
||||
const isV1 = trade?.route?.pairs?.some(pair => pair instanceof MockV1Pair)
|
||||
if (isV1) return Version.v1
|
||||
if (isV1 === false) return Version.v2
|
||||
return undefined
|
||||
}
|
||||
|
||||
// returns the v1 exchange against which a trade should be executed
|
||||
export function useV1TradeExchangeAddress(trade: Trade | undefined): string | undefined {
|
||||
const tokenAddress: string | undefined = useMemo(() => {
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
const isV1 = tradeVersion === Version.v1
|
||||
return isV1
|
||||
? trade &&
|
||||
WETH[trade.inputAmount.token.chainId] &&
|
||||
trade.inputAmount.token.equals(WETH[trade.inputAmount.token.chainId])
|
||||
? trade.outputAmount.token.address
|
||||
: trade?.inputAmount?.token?.address
|
||||
: undefined
|
||||
}, [trade])
|
||||
return useV1ExchangeAddress(tokenAddress)
|
||||
}
|
||||
|
||||
const ZERO_PERCENT = new Percent('0')
|
||||
const ONE_HUNDRED_PERCENT = new Percent('1')
|
||||
// returns whether tradeB is better than tradeA by at least a threshold
|
||||
export function isTradeBetter(
|
||||
tradeA: Trade | undefined,
|
||||
tradeB: Trade | undefined,
|
||||
minimumDelta: Percent = ZERO_PERCENT
|
||||
): boolean | undefined {
|
||||
if (!tradeA || !tradeB) return undefined
|
||||
|
||||
if (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!tradeA.inputAmount.token.equals(tradeB.inputAmount.token) ||
|
||||
!tradeB.outputAmount.token.equals(tradeB.outputAmount.token)
|
||||
) {
|
||||
throw new Error('Trades are not comparable')
|
||||
}
|
||||
|
||||
return v1HasBetterTrade && input && output
|
||||
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
|
||||
outputIsWETH ? 'ETH' : output.address
|
||||
}`
|
||||
: undefined
|
||||
if (minimumDelta.equalTo(ZERO_PERCENT)) {
|
||||
return tradeA.executionPrice.lessThan(tradeB.executionPrice)
|
||||
} else {
|
||||
return tradeA.executionPrice.raw.multiply(minimumDelta.add(ONE_HUNDRED_PERCENT)).lessThan(tradeB.executionPrice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { ChainId, Token, WETH } from '@uniswap/sdk'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { ALL_TOKENS } from '../constants/tokens'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useAddUserToken, useUserAddedTokens } from '../state/user/hooks'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
@@ -100,21 +100,3 @@ export function useToken(tokenAddress?: string): Token | undefined | null {
|
||||
tokenNameBytes32.result
|
||||
])
|
||||
}
|
||||
|
||||
// gets token information by address (typically user input) and
|
||||
// automatically adds it for the user if it's a valid token address
|
||||
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined | null {
|
||||
const addToken = useAddUserToken()
|
||||
const token = useToken(tokenAddress)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainId || !token) return
|
||||
if (WETH[chainId as ChainId]?.address === token.address) return
|
||||
if (allTokens[token.address]) return
|
||||
addToken(token)
|
||||
}, [token, addToken, chainId, allTokens])
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import { Trade, WETH, TokenAmount } from '@uniswap/sdk'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { ROUTER_ADDRESS } from '../constants'
|
||||
import { useTokenAllowance } from '../data/Allowances'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin } from '../utils'
|
||||
import { useTokenContract } from './useContract'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
export enum ApprovalState {
|
||||
UNKNOWN,
|
||||
@@ -21,16 +23,16 @@ export enum ApprovalState {
|
||||
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
|
||||
export function useApproveCallback(
|
||||
amountToApprove?: TokenAmount,
|
||||
addressToApprove?: string
|
||||
spender?: string
|
||||
): [ApprovalState, () => Promise<void>] {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, addressToApprove)
|
||||
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address)
|
||||
const currentAllowance = useTokenAllowance(amountToApprove?.token, account ?? undefined, spender)
|
||||
const pendingApproval = useHasPendingApproval(amountToApprove?.token?.address, spender)
|
||||
|
||||
// check the current approval status
|
||||
const approval = useMemo(() => {
|
||||
if (!amountToApprove) return ApprovalState.UNKNOWN
|
||||
const approvalState: ApprovalState = useMemo(() => {
|
||||
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
|
||||
// we treat WETH as ETH which requires no approvals
|
||||
if (amountToApprove.token.equals(WETH[amountToApprove.token.chainId])) return ApprovalState.APPROVED
|
||||
// we might not have enough data to know whether or not we need to approve
|
||||
@@ -38,13 +40,13 @@ export function useApproveCallback(
|
||||
if (pendingApproval) return ApprovalState.PENDING
|
||||
// amountToApprove will be defined if currentAllowance is
|
||||
return currentAllowance.lessThan(amountToApprove) ? ApprovalState.NOT_APPROVED : ApprovalState.APPROVED
|
||||
}, [amountToApprove, currentAllowance, pendingApproval])
|
||||
}, [amountToApprove, currentAllowance, pendingApproval, spender])
|
||||
|
||||
const tokenContract = useTokenContract(amountToApprove?.token?.address)
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const approve = useCallback(async (): Promise<void> => {
|
||||
if (approval !== ApprovalState.NOT_APPROVED) {
|
||||
if (approvalState !== ApprovalState.NOT_APPROVED) {
|
||||
console.error('approve was called unnecessarily')
|
||||
return
|
||||
}
|
||||
@@ -59,30 +61,35 @@ export function useApproveCallback(
|
||||
return
|
||||
}
|
||||
|
||||
if (!spender) {
|
||||
console.error('no spender')
|
||||
return
|
||||
}
|
||||
|
||||
let useExact = false
|
||||
const estimatedGas = await tokenContract.estimateGas.approve(addressToApprove, MaxUint256).catch(() => {
|
||||
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
|
||||
// general fallback for tokens who restrict approval amounts
|
||||
useExact = true
|
||||
return tokenContract.estimateGas.approve(addressToApprove, amountToApprove.raw.toString())
|
||||
return tokenContract.estimateGas.approve(spender, amountToApprove.raw.toString())
|
||||
})
|
||||
|
||||
return tokenContract
|
||||
.approve(addressToApprove, useExact ? amountToApprove.raw.toString() : MaxUint256, {
|
||||
.approve(spender, useExact ? amountToApprove.raw.toString() : MaxUint256, {
|
||||
gasLimit: calculateGasMargin(estimatedGas)
|
||||
})
|
||||
.then((response: TransactionResponse) => {
|
||||
addTransaction(response, {
|
||||
summary: 'Approve ' + amountToApprove?.token?.symbol,
|
||||
approvalOfToken: amountToApprove?.token?.address
|
||||
summary: 'Approve ' + amountToApprove.token.symbol,
|
||||
approval: { tokenAddress: amountToApprove.token.address, spender: spender }
|
||||
})
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.debug('Failed to approve token', error)
|
||||
throw error
|
||||
})
|
||||
}, [approval, tokenContract, addressToApprove, amountToApprove, addTransaction])
|
||||
}, [approvalState, tokenContract, spender, amountToApprove, addTransaction])
|
||||
|
||||
return [approval, approve]
|
||||
return [approvalState, approve]
|
||||
}
|
||||
|
||||
// wraps useApproveCallback in the context of a swap
|
||||
@@ -91,5 +98,7 @@ export function useApproveCallbackFromTrade(trade?: Trade, allowedSlippage = 0)
|
||||
() => (trade ? computeSlippageAdjustedAmounts(trade, allowedSlippage)[Field.INPUT] : undefined),
|
||||
[trade, allowedSlippage]
|
||||
)
|
||||
return useApproveCallback(amountToApprove, ROUTER_ADDRESS)
|
||||
const tradeIsV1 = getTradeVersion(trade) === Version.v1
|
||||
const v1ExchangeAddress = useV1TradeExchangeAddress(trade)
|
||||
return useApproveCallback(amountToApprove, tradeIsV1 ? v1ExchangeAddress : ROUTER_ADDRESS)
|
||||
}
|
||||
|
||||
@@ -30,23 +30,23 @@ export function useV1FactoryContract(): Contract | null {
|
||||
return useContract(V1_FACTORY_ADDRESSES[chainId as ChainId], V1_FACTORY_ABI, false)
|
||||
}
|
||||
|
||||
export function useV1ExchangeContract(address: string): Contract | null {
|
||||
return useContract(address, V1_EXCHANGE_ABI, false)
|
||||
}
|
||||
|
||||
export function useV2MigratorContract(): Contract | null {
|
||||
return useContract(MIGRATOR_ADDRESS, MIGRATOR_ABI, true)
|
||||
}
|
||||
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
export function useV1ExchangeContract(address?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(address, V1_EXCHANGE_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
|
||||
13
src/hooks/useLast.ts
Normal file
13
src/hooks/useLast.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Returns the last truthy value of type T
|
||||
* @param value changing value
|
||||
*/
|
||||
export default function useLast<T>(value: T | undefined | null): T | null | undefined {
|
||||
const [last, setLast] = useState<T | null | undefined>(value)
|
||||
useEffect(() => {
|
||||
setLast(last => value ?? last)
|
||||
}, [value])
|
||||
return last
|
||||
}
|
||||
11
src/hooks/useParsedQueryString.ts
Normal file
11
src/hooks/useParsedQueryString.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { parse, ParsedQs } from 'qs'
|
||||
import { useMemo } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
export default function useParsedQueryString(): ParsedQs {
|
||||
const { search } = useLocation()
|
||||
return useMemo(
|
||||
() => (search && search.length > 1 ? parse(search, { parseArrays: false, ignoreQueryPrefix: true }) : {}),
|
||||
[search]
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { MaxUint256 } from '@ethersproject/constants'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { ChainId, Token, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { ChainId, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, ROUTER_ADDRESS } from '../constants'
|
||||
import { useTokenAllowance } from '../data/Allowances'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useV1ExchangeContract } from './useContract'
|
||||
import useENSName from './useENSName'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
enum SwapType {
|
||||
EXACT_TOKENS_FOR_TOKENS,
|
||||
@@ -17,25 +21,37 @@ enum SwapType {
|
||||
EXACT_ETH_FOR_TOKENS,
|
||||
TOKENS_FOR_EXACT_TOKENS,
|
||||
TOKENS_FOR_EXACT_ETH,
|
||||
ETH_FOR_EXACT_TOKENS
|
||||
ETH_FOR_EXACT_TOKENS,
|
||||
V1_EXACT_ETH_FOR_TOKENS,
|
||||
V1_EXACT_TOKENS_FOR_ETH,
|
||||
V1_EXACT_TOKENS_FOR_TOKENS,
|
||||
V1_ETH_FOR_EXACT_TOKENS,
|
||||
V1_TOKENS_FOR_EXACT_ETH,
|
||||
V1_TOKENS_FOR_EXACT_TOKENS
|
||||
}
|
||||
|
||||
function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, chainId: number): SwapType {
|
||||
function getSwapType(trade: Trade | undefined): SwapType | undefined {
|
||||
if (!trade) return undefined
|
||||
const chainId = trade.inputAmount.token.chainId
|
||||
const inputWETH = trade.inputAmount.token.equals(WETH[chainId])
|
||||
const outputWETH = trade.outputAmount.token.equals(WETH[chainId])
|
||||
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
|
||||
const isV1 = getTradeVersion(trade) === Version.v1
|
||||
if (isExactIn) {
|
||||
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.EXACT_ETH_FOR_TOKENS
|
||||
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.EXACT_TOKENS_FOR_ETH
|
||||
if (inputWETH) {
|
||||
return isV1 ? SwapType.V1_EXACT_ETH_FOR_TOKENS : SwapType.EXACT_ETH_FOR_TOKENS
|
||||
} else if (outputWETH) {
|
||||
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_ETH : SwapType.EXACT_TOKENS_FOR_ETH
|
||||
} else {
|
||||
return SwapType.EXACT_TOKENS_FOR_TOKENS
|
||||
return isV1 ? SwapType.V1_EXACT_TOKENS_FOR_TOKENS : SwapType.EXACT_TOKENS_FOR_TOKENS
|
||||
}
|
||||
} else {
|
||||
if (tokens[Field.INPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.ETH_FOR_EXACT_TOKENS
|
||||
} else if (tokens[Field.OUTPUT]?.equals(WETH[chainId as ChainId])) {
|
||||
return SwapType.TOKENS_FOR_EXACT_ETH
|
||||
if (inputWETH) {
|
||||
return isV1 ? SwapType.V1_ETH_FOR_EXACT_TOKENS : SwapType.ETH_FOR_EXACT_TOKENS
|
||||
} else if (outputWETH) {
|
||||
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_ETH : SwapType.TOKENS_FOR_EXACT_ETH
|
||||
} else {
|
||||
return SwapType.TOKENS_FOR_EXACT_TOKENS
|
||||
return isV1 ? SwapType.V1_TOKENS_FOR_EXACT_TOKENS : SwapType.TOKENS_FOR_EXACT_TOKENS
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,18 +60,24 @@ function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, c
|
||||
// and the user has approved the slippage adjusted input amount for the trade
|
||||
export function useSwapCallback(
|
||||
trade?: Trade, // trade to execute, required
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips, optional
|
||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now, optional
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
|
||||
to?: string // recipient of output, optional
|
||||
): null | (() => Promise<string>) {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const inputAllowance = useTokenAllowance(trade?.inputAmount?.token, account ?? undefined, ROUTER_ADDRESS)
|
||||
const addTransaction = useTransactionAdder()
|
||||
const recipient = to ? isAddress(to) : account
|
||||
const ensName = useENSName(to)
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
|
||||
const inputAllowance = useTokenAllowance(
|
||||
trade?.inputAmount?.token,
|
||||
account ?? undefined,
|
||||
tradeVersion === Version.v1 ? v1Exchange?.address : ROUTER_ADDRESS
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !recipient) return null
|
||||
if (!trade || !recipient || !tradeVersion) return null
|
||||
|
||||
// will always be defined
|
||||
const {
|
||||
@@ -78,17 +100,17 @@ export function useSwapCallback(
|
||||
throw new Error('missing dependencies in onSwap callback')
|
||||
}
|
||||
|
||||
const routerContract: Contract = getRouterContract(chainId, library, account)
|
||||
const contract: Contract | null =
|
||||
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
|
||||
if (!contract) {
|
||||
throw new Error('Failed to get a swap contract')
|
||||
}
|
||||
|
||||
const path = trade.route.path.map(t => t.address)
|
||||
|
||||
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
|
||||
|
||||
const swapType = getSwapType(
|
||||
{ [Field.INPUT]: trade.inputAmount.token, [Field.OUTPUT]: trade.outputAmount.token },
|
||||
trade.tradeType === TradeType.EXACT_INPUT,
|
||||
chainId as ChainId
|
||||
)
|
||||
const swapType = getSwapType(trade)
|
||||
|
||||
// let estimate: Function, method: Function,
|
||||
let methodNames: string[],
|
||||
@@ -145,18 +167,83 @@ export function useSwapCallback(
|
||||
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 = await Promise.all(
|
||||
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
|
||||
methodNames.map(methodName =>
|
||||
routerContract.estimateGas[methodName](...args, value ? { value } : {})
|
||||
contract.estimateGas[methodName](...args, value ? { value } : {})
|
||||
.then(calculateGasMargin)
|
||||
.catch(error => {
|
||||
console.error(`estimateGas failed for ${methodName}`, error)
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// we expect failures from left to right, so throw if we see failures
|
||||
// from right to left
|
||||
for (let i = 0; i < safeGasEstimates.length - 1; i++) {
|
||||
// if the FoT method fails, but the regular method does not, we should not
|
||||
// use the regular method. this probably means something is wrong with the fot token.
|
||||
if (BigNumber.isBigNumber(safeGasEstimates[i]) && !BigNumber.isBigNumber(safeGasEstimates[i + 1])) {
|
||||
throw new Error(
|
||||
'An error occurred. Please try raising your slippage. If that does not work, contact support.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
|
||||
BigNumber.isBigNumber(safeGasEstimate)
|
||||
)
|
||||
@@ -185,38 +272,25 @@ export function useSwapCallback(
|
||||
const methodName = methodNames[indexOfSuccessfulEstimation]
|
||||
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
|
||||
|
||||
return routerContract[methodName](...args, {
|
||||
return contract[methodName](...args, {
|
||||
gasLimit: safeGasEstimate,
|
||||
...(value ? { value } : {})
|
||||
})
|
||||
.then((response: any) => {
|
||||
if (recipient === account) {
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Swap ' +
|
||||
slippageAdjustedInput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.inputAmount.token.symbol +
|
||||
' for ' +
|
||||
slippageAdjustedOutput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.outputAmount.token.symbol
|
||||
})
|
||||
} else {
|
||||
addTransaction(response, {
|
||||
summary:
|
||||
'Swap ' +
|
||||
slippageAdjustedInput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.inputAmount.token.symbol +
|
||||
' for ' +
|
||||
slippageAdjustedOutput.toSignificant(3) +
|
||||
' ' +
|
||||
trade.outputAmount.token.symbol +
|
||||
' to ' +
|
||||
(ensName ?? recipient)
|
||||
})
|
||||
}
|
||||
const inputSymbol = trade.inputAmount.token.symbol
|
||||
const outputSymbol = trade.outputAmount.token.symbol
|
||||
const inputAmount = slippageAdjustedInput.toSignificant(3)
|
||||
const outputAmount = slippageAdjustedOutput.toSignificant(3)
|
||||
|
||||
const base = `Swap ${inputAmount} ${inputSymbol} for ${outputAmount} ${outputSymbol}`
|
||||
const withRecipient = recipient === account ? base : `${base} to ${ensName ?? recipient}`
|
||||
|
||||
const withVersion =
|
||||
tradeVersion === Version.v2 ? withRecipient : `${withRecipient} on ${tradeVersion.toUpperCase()}`
|
||||
|
||||
addTransaction(response, {
|
||||
summary: withVersion
|
||||
})
|
||||
|
||||
return response.hash
|
||||
})
|
||||
@@ -227,11 +301,24 @@ export function useSwapCallback(
|
||||
}
|
||||
// otherwise, the error was unexpected and we need to convey that
|
||||
else {
|
||||
console.error(`swap failed for ${methodName}`, error)
|
||||
console.error(`Swap failed`, error, methodName, args, value)
|
||||
throw Error('An error occurred while swapping. Please contact support.')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient])
|
||||
}, [
|
||||
trade,
|
||||
recipient,
|
||||
tradeVersion,
|
||||
allowedSlippage,
|
||||
chainId,
|
||||
inputAllowance,
|
||||
library,
|
||||
account,
|
||||
v1Exchange,
|
||||
deadline,
|
||||
addTransaction,
|
||||
ensName
|
||||
])
|
||||
}
|
||||
|
||||
15
src/hooks/useToggledVersion.ts
Normal file
15
src/hooks/useToggledVersion.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import useParsedQueryString from './useParsedQueryString'
|
||||
|
||||
export enum Version {
|
||||
v1 = 'v1',
|
||||
v2 = 'v2'
|
||||
}
|
||||
|
||||
export const DEFAULT_VERSION: Version = Version.v2
|
||||
|
||||
export default function useToggledVersion(): Version {
|
||||
const { use } = useParsedQueryString()
|
||||
if (!use || typeof use !== 'string') return Version.v2
|
||||
if (use.toLowerCase() === 'v1') return Version.v1
|
||||
return DEFAULT_VERSION
|
||||
}
|
||||
@@ -18,7 +18,9 @@ import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
|
||||
function getLibrary(provider: any): Web3Provider {
|
||||
return new Web3Provider(provider)
|
||||
const library = new Web3Provider(provider)
|
||||
library.pollingInterval = 15000
|
||||
return library
|
||||
}
|
||||
|
||||
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
||||
|
||||
@@ -17,7 +17,7 @@ import Row, { AutoRow, RowBetween, RowFixed, RowFlat } from '../../components/Ro
|
||||
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
|
||||
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { ROUTER_ADDRESS, MIN_ETH, ONE_BIPS } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import { Field } from '../../state/mint/actions'
|
||||
import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import { useUserSlippageTolerance, useUserDeadline, useIsExpertMode } from '../../state/user/hooks'
|
||||
|
||||
export default function AddLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||
useDefaultsFromURLMatchParams(params)
|
||||
@@ -45,6 +45,8 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
const expertMode = useIsExpertMode()
|
||||
|
||||
// mint state
|
||||
const { independentField, typedValue, otherTypedValue } = useMintState()
|
||||
const {
|
||||
@@ -64,14 +66,13 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
const isValid = !error
|
||||
|
||||
// modal and loading
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
|
||||
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, setShowConfirm] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
|
||||
|
||||
// tx parameters
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
// txn values
|
||||
const [deadline] = useUserDeadline() // custom from users settings
|
||||
const [allowedSlippage] = useUserSlippageTolerance() // custom from users
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
|
||||
// get formatted amounts
|
||||
const formattedAmounts = {
|
||||
@@ -269,8 +270,8 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
|
||||
const PriceBar = () => {
|
||||
return (
|
||||
<AutoColumn gap="md" justify="space-between">
|
||||
<AutoRow justify="space-between">
|
||||
<AutoColumn gap="md">
|
||||
<AutoRow justify="space-around" gap="4px">
|
||||
<AutoColumn justify="center">
|
||||
<TYPE.black>{price?.toSignificant(6) ?? '0'}</TYPE.black>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} pt={1}>
|
||||
@@ -389,50 +390,59 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
|
||||
|
||||
{!account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : approvalA === ApprovalState.NOT_APPROVED || approvalA === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveACallback} disabled={approvalA === ApprovalState.PENDING}>
|
||||
{approvalA === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : approvalB === ApprovalState.NOT_APPROVED || approvalB === ApprovalState.PENDING ? (
|
||||
<ButtonLight onClick={approveBCallback} disabled={approvalB === ApprovalState.PENDING}>
|
||||
{approvalB === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
||||
)}
|
||||
</ButtonLight>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? 'Supply'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
<AutoColumn gap={'md'}>
|
||||
{(approvalA === ApprovalState.NOT_APPROVED ||
|
||||
approvalA === ApprovalState.PENDING ||
|
||||
approvalB === ApprovalState.NOT_APPROVED ||
|
||||
approvalB === ApprovalState.PENDING) &&
|
||||
isValid && (
|
||||
<RowBetween>
|
||||
{approvalA !== ApprovalState.APPROVED && (
|
||||
<ButtonPrimary
|
||||
onClick={approveACallback}
|
||||
disabled={approvalA === ApprovalState.PENDING}
|
||||
width={approvalB !== ApprovalState.APPROVED ? '48%' : '100%'}
|
||||
>
|
||||
{approvalA === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_A]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_A]?.symbol
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
{approvalB !== ApprovalState.APPROVED && (
|
||||
<ButtonPrimary
|
||||
onClick={approveBCallback}
|
||||
disabled={approvalB === ApprovalState.PENDING}
|
||||
width={approvalA !== ApprovalState.APPROVED ? '48%' : '100%'}
|
||||
>
|
||||
{approvalB === ApprovalState.PENDING ? (
|
||||
<Dots>Approving {tokens[Field.TOKEN_B]?.symbol}</Dots>
|
||||
) : (
|
||||
'Approve ' + tokens[Field.TOKEN_B]?.symbol
|
||||
)}
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</RowBetween>
|
||||
)}
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
expertMode ? onAdd() : setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid || approvalA !== ApprovalState.APPROVED || approvalB !== ApprovalState.APPROVED}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? 'Supply'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B] ? (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{pair && !noLiquidity ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
||||
<PositionCard pair={pair} minimal={true} />
|
||||
|
||||
@@ -11,6 +11,7 @@ import AddLiquidity from './AddLiquidity'
|
||||
import CreatePool from './CreatePool'
|
||||
import MigrateV1 from './MigrateV1'
|
||||
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
|
||||
import RemoveV1Exchange from './MigrateV1/RemoveV1Exchange'
|
||||
import Pool from './Pool'
|
||||
import PoolFinder from './PoolFinder'
|
||||
import RemoveLiquidity from './RemoveLiquidity'
|
||||
@@ -49,25 +50,6 @@ const BodyWrapper = styled.div`
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const BackgroundGradient = styled.div`
|
||||
width: 100%;
|
||||
height: 170vh;
|
||||
background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`};
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
|
||||
transform: translateY(-70vh);
|
||||
|
||||
@media (max-width: 960px) {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
transform: translateY(-150px);
|
||||
}
|
||||
`
|
||||
|
||||
const Marginer = styled.div`
|
||||
margin-top: 5rem;
|
||||
`
|
||||
@@ -103,13 +85,13 @@ export default function App() {
|
||||
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
|
||||
<Route exact strict path="/migrate/v1" component={MigrateV1} />
|
||||
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
|
||||
<Route exact strict path="/remove/v1/:address" component={RemoveV1Exchange} />
|
||||
<Route component={RedirectPathToSwapOnly} />
|
||||
</Switch>
|
||||
</Web3ReactManager>
|
||||
<Marginer />
|
||||
<Footer />
|
||||
</BodyWrapper>
|
||||
<BackgroundGradient />
|
||||
</AppWrapper>
|
||||
</Router>
|
||||
</Suspense>
|
||||
|
||||
@@ -5,12 +5,12 @@ import AppBody from '../AppBody'
|
||||
|
||||
import Row, { AutoRow } from '../../components/Row'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
|
||||
import { Text } from 'rebass'
|
||||
import { Plus } from 'react-feather'
|
||||
import { TYPE, StyledInternalLink } from '../../theme'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button'
|
||||
import { ButtonPrimary, ButtonDropdown, ButtonDropdownLight } from '../../components/Button'
|
||||
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
@@ -59,16 +59,16 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
<AutoColumn gap="20px">
|
||||
<AutoColumn gap="24px">
|
||||
{!token0Address ? (
|
||||
<ButtonDropwdown
|
||||
<ButtonDropdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select first token</Text>
|
||||
</ButtonDropwdown>
|
||||
</ButtonDropdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
@@ -83,13 +83,13 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
{token0?.address === WETH[chainId]?.address && '(default)'}
|
||||
</TYPE.darkGray>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
</ButtonDropdownLight>
|
||||
)}
|
||||
<ColumnCenter>
|
||||
<Plus size="16" color="#888D9B" />
|
||||
</ColumnCenter>
|
||||
{!token1Address ? (
|
||||
<ButtonDropwdown
|
||||
<ButtonDropdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
@@ -97,9 +97,9 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
disabled={step !== STEP.SELECT_TOKENS}
|
||||
>
|
||||
<Text fontSize={20}>Select second token</Text>
|
||||
</ButtonDropwdown>
|
||||
</ButtonDropdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
@@ -111,7 +111,7 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
{token1?.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
</ButtonDropdownLight>
|
||||
)}
|
||||
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
|
||||
<AutoRow padding="10px" justify="center">
|
||||
@@ -128,9 +128,8 @@ export default function CreatePool({ location }: RouteComponentProps) {
|
||||
</ButtonPrimary>
|
||||
)}
|
||||
</AutoColumn>
|
||||
<SearchModal
|
||||
<TokenSearchModal
|
||||
isOpen={showSearch}
|
||||
filterType="tokens"
|
||||
onTokenSelect={address => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
}}
|
||||
|
||||
@@ -8,31 +8,97 @@ import { ButtonConfirmed } from '../../components/Button'
|
||||
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../../components/Row'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
|
||||
import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useContract'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
|
||||
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { TYPE, ExternalLink } from '../../theme'
|
||||
import { isAddress, getEtherscanLink } from '../../utils'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { FormattedPoolTokenAmount } from './index'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
|
||||
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
const ONE = JSBI.BigInt(1)
|
||||
const ZERO_FRACTION = new Fraction(ZERO, ONE)
|
||||
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
|
||||
|
||||
function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
|
||||
return (
|
||||
<>
|
||||
{tokenAmount.equalTo(JSBI.BigInt(0))
|
||||
? '0'
|
||||
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
|
||||
? tokenAmount.toSignificant(4)
|
||||
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function V1LiquidityInfo({
|
||||
token,
|
||||
liquidityTokenAmount,
|
||||
tokenWorth,
|
||||
ethWorth
|
||||
}: {
|
||||
token: Token
|
||||
liquidityTokenAmount: TokenAmount
|
||||
tokenWorth: TokenAmount
|
||||
ethWorth: Fraction
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
|
||||
<TokenLogo size="24px" address={token.address} />
|
||||
<div style={{ marginLeft: '.75rem' }}>
|
||||
<TYPE.mediumHeader>
|
||||
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />}{' '}
|
||||
{token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH
|
||||
</TYPE.mediumHeader>
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
<RowBetween my="1rem">
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token.equals(WETH[chainId]) ? 'WETH' : token.symbol}:
|
||||
</Text>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{tokenWorth.toSignificant(4)}
|
||||
</Text>
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token.address} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween mb="1rem">
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled ETH:
|
||||
</Text>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{ethWorth.toSignificant(4)}
|
||||
</Text>
|
||||
<TokenLogo size="20px" style={{ marginLeft: '8px' }} address={WETH[chainId].address} />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount: TokenAmount; token: Token }) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
|
||||
@@ -125,69 +191,96 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
|
||||
})
|
||||
}, [minAmountToken, minAmountETH, migrator, token, account, addTransaction])
|
||||
|
||||
const noLiquidityTokens = liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
|
||||
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
|
||||
|
||||
const largePriceDifference = Boolean(priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5)))
|
||||
const largePriceDifference = !!priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5))
|
||||
|
||||
const isSuccessfullyMigrated = Boolean(noLiquidityTokens && pendingMigrationHash)
|
||||
const isSuccessfullyMigrated = !!pendingMigrationHash && !!noLiquidityTokens
|
||||
|
||||
return (
|
||||
<AutoColumn gap="20px">
|
||||
{!isFirstLiquidityProvider ? (
|
||||
largePriceDifference ? (
|
||||
<YellowCard>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
It is best to deposit liquidity into Uniswap V2 at a price you believe is correct. If you believe the
|
||||
price is incorrect, you can either make a swap to move the price or wait for someone else to do so.
|
||||
</TYPE.body>
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<TYPE.body>V1 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<TYPE.body>V2 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div>Price Difference:</div>
|
||||
<div>{priceDifferenceAbs.toSignificant(4)}%</div>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null
|
||||
) : (
|
||||
<PinkCard>
|
||||
<AutoColumn gap="10px">
|
||||
<div>
|
||||
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
|
||||
current V1 price. Your transaction cost also includes the gas to create the pool.
|
||||
</div>
|
||||
<div>V1 Price</div>
|
||||
<AutoColumn>
|
||||
<div>
|
||||
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</div>
|
||||
<div>
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
This tool will safely migrate your V1 liquidity to V2 with minimal price risk. The process is completely
|
||||
trustless thanks to the{' '}
|
||||
<ExternalLink href={getEtherscanLink(chainId, MIGRATOR_ADDRESS, 'address')}>
|
||||
<TYPE.blue display="inline">Uniswap migration contract↗</TYPE.blue>
|
||||
</ExternalLink>
|
||||
.
|
||||
</TYPE.body>
|
||||
|
||||
{!isFirstLiquidityProvider && largePriceDifference ? (
|
||||
<YellowCard>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
It{"'"}s best to deposit liquidity into Uniswap V2 at a price you believe is correct. If the V2 price seems
|
||||
incorrect, you can either make a swap to move the price or wait for someone else to do so.
|
||||
</TYPE.body>
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<TYPE.body>V1 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</div>
|
||||
</AutoColumn>
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
|
||||
<RowBetween>
|
||||
<TYPE.body>V2 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v2SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
|
||||
<RowBetween>
|
||||
<TYPE.body color="inherit">Price Difference:</TYPE.body>
|
||||
<TYPE.black color="inherit">{priceDifferenceAbs.toSignificant(4)}%</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</YellowCard>
|
||||
) : null}
|
||||
|
||||
{isFirstLiquidityProvider && (
|
||||
<PinkCard>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
|
||||
current V1 price. Your transaction cost also includes the gas to create the pool.
|
||||
</TYPE.body>
|
||||
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<TYPE.body>V1 Price:</TYPE.body>
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<TYPE.black>
|
||||
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</PinkCard>
|
||||
)}
|
||||
|
||||
<LightCard>
|
||||
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
|
||||
<TokenLogo size="24px" address={token.address} />{' '}
|
||||
<div style={{ marginLeft: '.75rem' }}>
|
||||
<TYPE.mediumHeader>
|
||||
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />} {token.symbol} Pool Tokens
|
||||
</TYPE.mediumHeader>
|
||||
</div>
|
||||
</AutoRow>
|
||||
<V1LiquidityInfo
|
||||
token={token}
|
||||
liquidityTokenAmount={liquidityTokenAmount}
|
||||
tokenWorth={tokenWorth}
|
||||
ethWorth={ethWorth}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', marginTop: '1rem' }}>
|
||||
<AutoColumn gap="12px" style={{ flex: '1', marginRight: 12 }}>
|
||||
<ButtonConfirmed
|
||||
@@ -195,11 +288,13 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED}
|
||||
onClick={approve}
|
||||
>
|
||||
{approval === ApprovalState.PENDING
|
||||
? 'Approving...'
|
||||
: approval === ApprovalState.APPROVED
|
||||
? 'Approved'
|
||||
: 'Approve'}
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
</AutoColumn>
|
||||
<AutoColumn gap="12px" style={{ flex: '1' }}>
|
||||
@@ -214,13 +309,13 @@ function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount
|
||||
}
|
||||
onClick={migrate}
|
||||
>
|
||||
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? 'Migrating...' : 'Migrate'}
|
||||
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? <Dots>Migrating</Dots> : 'Migrate'}
|
||||
</ButtonConfirmed>
|
||||
</AutoColumn>
|
||||
</div>
|
||||
</LightCard>
|
||||
<TYPE.darkGray style={{ textAlign: 'center' }}>
|
||||
{'Your ' + token.symbol + ' liquidity will become Uniswap V2 ' + token.symbol + '/ETH liquidity.'}
|
||||
{`Your Uniswap V1 ${token.symbol}/ETH liquidity will become Uniswap V2 ${token.symbol}/ETH liquidity.`}
|
||||
</TYPE.darkGray>
|
||||
</AutoColumn>
|
||||
)
|
||||
@@ -232,39 +327,33 @@ export default function MigrateV1Exchange({
|
||||
params: { address }
|
||||
}
|
||||
}: RouteComponentProps<{ address: string }>) {
|
||||
const validated = isAddress(address)
|
||||
const validatedAddress = isAddress(address)
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
const exchangeContract = useV1ExchangeContract(validated ? validated : undefined)
|
||||
|
||||
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined)
|
||||
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
|
||||
|
||||
const token = useTokenByAddressAndAutomaticallyAdd(tokenAddress)
|
||||
const token = useToken(tokenAddress)
|
||||
|
||||
const liquidityToken: Token | undefined = useMemo(
|
||||
() => (validated && token ? new Token(chainId, validated, 18, `UNI-V1-${token.symbol}`) : undefined),
|
||||
[chainId, token, validated]
|
||||
() =>
|
||||
validatedAddress && token
|
||||
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
|
||||
: undefined,
|
||||
[chainId, validatedAddress, token]
|
||||
)
|
||||
|
||||
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
history.push('/migrate/v1')
|
||||
}, [history])
|
||||
|
||||
if (!validated) {
|
||||
// redirect for invalid url params
|
||||
if (!validatedAddress || tokenAddress === AddressZero) {
|
||||
console.error('Invalid address in path', address)
|
||||
return <Redirect to="/migrate/v1" />
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<BodyWrapper>
|
||||
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
@@ -272,13 +361,30 @@ export default function MigrateV1Exchange({
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<ArrowLeft onClick={handleBack} />
|
||||
</div>
|
||||
<TYPE.mediumHeader>Migrate {token?.symbol} Pool Tokens</TYPE.mediumHeader>
|
||||
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
{userLiquidityBalance && token ? (
|
||||
{!account ? (
|
||||
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
|
||||
) : validatedAddress && token?.equals(WETH[chainId]) ? (
|
||||
<>
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
Because Uniswap V2 uses WETH under the hood, your Uniswap V1 WETH/ETH liquidity cannot be migrated. You
|
||||
may want to remove your liquidity instead.
|
||||
</TYPE.body>
|
||||
|
||||
<ButtonConfirmed
|
||||
onClick={() => {
|
||||
history.push(`/remove/v1/${validatedAddress}`)
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</ButtonConfirmed>
|
||||
</>
|
||||
) : userLiquidityBalance && token ? (
|
||||
<V1PairMigration liquidityTokenAmount={userLiquidityBalance} token={token} />
|
||||
) : (
|
||||
<EmptyState message="Loading..." />
|
||||
|
||||
189
src/pages/MigrateV1/RemoveV1Exchange.tsx
Normal file
189
src/pages/MigrateV1/RemoveV1Exchange.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import { TransactionResponse } from '@ethersproject/abstract-provider'
|
||||
import { JSBI, Token, TokenAmount, WETH, Fraction, Percent } from '@uniswap/sdk'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Redirect, RouteComponentProps } from 'react-router'
|
||||
import { ButtonConfirmed } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow } from '../../components/Row'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useV1ExchangeContract } from '../../hooks/useContract'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
|
||||
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { useTokenBalance, useETHBalances } from '../../state/wallet/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { V1LiquidityInfo } from './MigrateV1Exchange'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
|
||||
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
|
||||
const ZERO = JSBI.BigInt(0)
|
||||
const ONE = JSBI.BigInt(1)
|
||||
const ZERO_FRACTION = new Fraction(ZERO, ONE)
|
||||
|
||||
function V1PairRemoval({
|
||||
exchangeContract,
|
||||
liquidityTokenAmount,
|
||||
token
|
||||
}: {
|
||||
exchangeContract: Contract
|
||||
liquidityTokenAmount: TokenAmount
|
||||
token: Token
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
|
||||
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
|
||||
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
|
||||
|
||||
const [confirmingRemoval, setConfirmingRemoval] = useState<boolean>(false)
|
||||
const [pendingRemovalHash, setPendingRemovalHash] = useState<string | null>(null)
|
||||
|
||||
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
|
||||
|
||||
const ethWorth: Fraction = exchangeETHBalance
|
||||
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
|
||||
: ZERO_FRACTION
|
||||
|
||||
const tokenWorth: TokenAmount = exchangeTokenBalance
|
||||
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
|
||||
: new TokenAmount(token, ZERO)
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
const isRemovalPending = useIsTransactionPending(pendingRemovalHash)
|
||||
|
||||
const remove = useCallback(() => {
|
||||
if (!liquidityTokenAmount) return
|
||||
|
||||
setConfirmingRemoval(true)
|
||||
exchangeContract
|
||||
.removeLiquidity(
|
||||
liquidityTokenAmount.raw.toString(),
|
||||
1, // min_eth, this is safe because we're removing liquidity
|
||||
1, // min_tokens, this is safe because we're removing liquidity
|
||||
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
|
||||
)
|
||||
.then((response: TransactionResponse) => {
|
||||
ReactGA.event({
|
||||
category: 'Remove',
|
||||
action: 'V1',
|
||||
label: token?.symbol
|
||||
})
|
||||
|
||||
addTransaction(response, {
|
||||
summary: `Remove ${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH V1 liquidity`
|
||||
})
|
||||
setPendingRemovalHash(response.hash)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
setConfirmingRemoval(false)
|
||||
})
|
||||
}, [exchangeContract, liquidityTokenAmount, token, chainId, addTransaction])
|
||||
|
||||
const noLiquidityTokens = !!liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
|
||||
|
||||
const isSuccessfullyRemoved = !!pendingRemovalHash && !!noLiquidityTokens
|
||||
|
||||
return (
|
||||
<AutoColumn gap="20px">
|
||||
<TYPE.body my={9} style={{ fontWeight: 400 }}>
|
||||
This tool will remove your V1 liquidity and send the underlying assets to your wallet.
|
||||
</TYPE.body>
|
||||
|
||||
<LightCard>
|
||||
<V1LiquidityInfo
|
||||
token={token}
|
||||
liquidityTokenAmount={liquidityTokenAmount}
|
||||
tokenWorth={tokenWorth}
|
||||
ethWorth={ethWorth}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'flex', marginTop: '1rem' }}>
|
||||
<ButtonConfirmed
|
||||
confirmed={isSuccessfullyRemoved}
|
||||
disabled={isSuccessfullyRemoved || noLiquidityTokens || isRemovalPending || confirmingRemoval}
|
||||
onClick={remove}
|
||||
>
|
||||
{isSuccessfullyRemoved ? 'Success' : isRemovalPending ? <Dots>Removing</Dots> : 'Remove'}
|
||||
</ButtonConfirmed>
|
||||
</div>
|
||||
</LightCard>
|
||||
<TYPE.darkGray style={{ textAlign: 'center' }}>
|
||||
{`Your Uniswap V1 ${
|
||||
token.equals(WETH[chainId]) ? 'WETH' : token.symbol
|
||||
}/ETH liquidity will be redeemed for underlying assets.`}
|
||||
</TYPE.darkGray>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RemoveV1Exchange({
|
||||
history,
|
||||
match: {
|
||||
params: { address }
|
||||
}
|
||||
}: RouteComponentProps<{ address: string }>) {
|
||||
const validatedAddress = isAddress(address)
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
|
||||
const exchangeContract = useV1ExchangeContract(validatedAddress ? validatedAddress : undefined, true)
|
||||
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
|
||||
const token = useToken(tokenAddress)
|
||||
|
||||
const liquidityToken: Token | undefined = useMemo(
|
||||
() =>
|
||||
validatedAddress && token
|
||||
? new Token(chainId, validatedAddress, 18, `UNI-V1-${token.symbol}`, 'Uniswap V1')
|
||||
: undefined,
|
||||
[chainId, validatedAddress, token]
|
||||
)
|
||||
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
history.push('/migrate/v1')
|
||||
}, [history])
|
||||
|
||||
// redirect for invalid url params
|
||||
if (!validatedAddress || tokenAddress === AddressZero) {
|
||||
console.error('Invalid address in path', address)
|
||||
return <Redirect to="/migrate/v1" />
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<ArrowLeft onClick={handleBack} />
|
||||
</div>
|
||||
<TYPE.mediumHeader>Remove V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<QuestionHelper text="Remove your Uniswap V1 liquidity tokens." />
|
||||
</div>
|
||||
</AutoRow>
|
||||
|
||||
{!account ? (
|
||||
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
|
||||
) : userLiquidityBalance && token ? (
|
||||
<V1PairRemoval
|
||||
exchangeContract={exchangeContract}
|
||||
liquidityTokenAmount={userLiquidityBalance}
|
||||
token={token}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState message="Loading..." />
|
||||
)}
|
||||
</AutoColumn>
|
||||
</BodyWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,143 +1,123 @@
|
||||
import { Fraction, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
||||
import { JSBI, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useMemo, useState, useEffect } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonPrimary } from '../../components/Button'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import { AutoRow } from '../../components/Row'
|
||||
import { SearchInput } from '../../components/SearchModal/styleds'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { useAllTokenV1Exchanges } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { useToken, useAllTokens } from '../../hooks/Tokens'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { GreyCard } from '../../components/Card'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { BodyWrapper } from '../AppBody'
|
||||
import { EmptyState } from './EmptyState'
|
||||
|
||||
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
|
||||
|
||||
export function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
|
||||
return (
|
||||
<>
|
||||
{tokenAmount.equalTo(JSBI.BigInt(0))
|
||||
? '0'
|
||||
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
|
||||
? tokenAmount.toSignificant(6)
|
||||
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
|
||||
</>
|
||||
)
|
||||
}
|
||||
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'
|
||||
|
||||
export default function MigrateV1({ history }: RouteComponentProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const allV1Exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const v1LiquidityTokens: Token[] = useMemo(() => {
|
||||
return Object.keys(allV1Exchanges).map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
|
||||
}, [chainId, allV1Exchanges])
|
||||
|
||||
const v1LiquidityBalances = useTokenBalances(account, v1LiquidityTokens)
|
||||
|
||||
const [tokenSearch, setTokenSearch] = useState<string>('')
|
||||
const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch])
|
||||
|
||||
const searchedToken: Token | undefined = useTokenByAddressAndAutomaticallyAdd(tokenSearch)
|
||||
// automatically add the search token
|
||||
const token = useToken(tokenSearch)
|
||||
const isDefault = isDefaultToken(token)
|
||||
const allTokens = useAllTokens()
|
||||
const isCustomAdded = isCustomAddedToken(allTokens, token)
|
||||
const addToken = useAddUserToken()
|
||||
useEffect(() => {
|
||||
if (token && !isDefault && !isCustomAdded) {
|
||||
addToken(token)
|
||||
}
|
||||
}, [token, isDefault, isCustomAdded, addToken])
|
||||
|
||||
const unmigratedLiquidityExchangeAddresses: TokenAmount[] = useMemo(
|
||||
() =>
|
||||
Object.keys(v1LiquidityBalances)
|
||||
.filter(tokenAddress =>
|
||||
v1LiquidityBalances[tokenAddress]
|
||||
? JSBI.greaterThan(v1LiquidityBalances[tokenAddress]?.raw, JSBI.BigInt(0))
|
||||
: false
|
||||
)
|
||||
.map(tokenAddress => v1LiquidityBalances[tokenAddress])
|
||||
.sort((a1, a2) => {
|
||||
if (searchedToken) {
|
||||
if (allV1Exchanges[a1.token.address].address === searchedToken.address) return -1
|
||||
if (allV1Exchanges[a2.token.address].address === searchedToken.address) return 1
|
||||
}
|
||||
return a1.token.address < a2.token.address ? -1 : 1
|
||||
}),
|
||||
[allV1Exchanges, searchedToken, v1LiquidityBalances]
|
||||
// get V1 LP balances
|
||||
const V1Exchanges = useAllTokenV1Exchanges()
|
||||
const V1LiquidityTokens: Token[] = useMemo(() => {
|
||||
return Object.keys(V1Exchanges).map(
|
||||
exchangeAddress => new Token(chainId, exchangeAddress, 18, 'UNI-V1', 'Uniswap V1')
|
||||
)
|
||||
}, [chainId, V1Exchanges])
|
||||
const [V1LiquidityBalances, V1LiquidityBalancesLoading] = useTokenBalancesWithLoadingIndicator(
|
||||
account,
|
||||
V1LiquidityTokens
|
||||
)
|
||||
const allV1PairsWithLiquidity = V1LiquidityTokens.filter(V1LiquidityToken => {
|
||||
return (
|
||||
V1LiquidityBalances?.[V1LiquidityToken.address] &&
|
||||
JSBI.greaterThan(V1LiquidityBalances[V1LiquidityToken.address].raw, JSBI.BigInt(0))
|
||||
)
|
||||
}).map(V1LiquidityToken => {
|
||||
return (
|
||||
<V1PositionCard
|
||||
key={V1LiquidityToken.address}
|
||||
token={V1Exchanges[V1LiquidityToken.address]}
|
||||
V1LiquidityBalance={V1LiquidityBalances[V1LiquidityToken.address]}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
// should never always be false, because a V1 exhchange exists for WETH on all testnets
|
||||
const isLoading = Object.keys(V1Exchanges)?.length === 0 || V1LiquidityBalancesLoading
|
||||
|
||||
const handleBackClick = useCallback(() => {
|
||||
history.push('/pool')
|
||||
}, [history])
|
||||
|
||||
return (
|
||||
<BodyWrapper style={{ maxWidth: 450, padding: 24 }}>
|
||||
<AutoColumn gap="24px">
|
||||
<AutoRow style={{ justifyContent: 'space-between' }}>
|
||||
<BodyWrapper style={{ padding: 24 }}>
|
||||
<AutoColumn gap="16px">
|
||||
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
|
||||
<div style={{ cursor: 'pointer' }}>
|
||||
<ArrowLeft onClick={handleBackClick} />
|
||||
</div>
|
||||
<TYPE.mediumHeader>Migrate V1 Liquidity</TYPE.mediumHeader>
|
||||
<div>
|
||||
<ArrowLeft style={{ cursor: 'pointer' }} onClick={handleBackClick} />
|
||||
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
|
||||
</div>
|
||||
<TYPE.largeHeader>Migrate Liquidity</TYPE.largeHeader>
|
||||
<div></div>
|
||||
</AutoRow>
|
||||
<GreyCard>
|
||||
<TYPE.main style={{ lineHeight: '140%' }}>
|
||||
For each pool, approve the migration helper and click migrate liquidity. Your liquidity will be withdrawn
|
||||
from Uniswap V1 and deposited into Uniswap V2.
|
||||
</TYPE.main>
|
||||
<TYPE.black padding={'1rem 0 0 0'} style={{ lineHeight: '140%' }}>
|
||||
If your liquidity does not appear below automatically, you may need to find it by pasting the token address
|
||||
into the search box below.
|
||||
</TYPE.black>
|
||||
</GreyCard>
|
||||
<AutoRow>
|
||||
<SearchInput
|
||||
value={tokenSearch}
|
||||
onChange={handleTokenSearchChange}
|
||||
placeholder="Find liquidity by pasting a token address."
|
||||
/>
|
||||
</AutoRow>
|
||||
|
||||
{unmigratedLiquidityExchangeAddresses.map(poolTokenAmount => (
|
||||
<div
|
||||
key={poolTokenAmount.token.address}
|
||||
style={{ borderRadius: '20px', padding: 16, backgroundColor: theme.bg2 }}
|
||||
>
|
||||
<AutoRow style={{ justifyContent: 'space-between' }}>
|
||||
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
|
||||
<TokenLogo size="32px" address={allV1Exchanges[poolTokenAmount.token.address].address} />{' '}
|
||||
<div style={{ marginLeft: '.75rem' }}>
|
||||
<TYPE.main fontWeight={600}>
|
||||
<FormattedPoolTokenAmount tokenAmount={poolTokenAmount} />
|
||||
</TYPE.main>
|
||||
<TYPE.main fontWeight={500}>
|
||||
{allV1Exchanges[poolTokenAmount.token.address].symbol} Pool Tokens
|
||||
</TYPE.main>
|
||||
</div>
|
||||
</AutoRow>
|
||||
<div>
|
||||
<ButtonPrimary
|
||||
onClick={() => {
|
||||
history.push(`/migrate/v1/${poolTokenAmount.token.address}`)
|
||||
}}
|
||||
style={{ padding: '8px 12px', borderRadius: '12px' }}
|
||||
>
|
||||
Migrate
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
|
||||
For each pool shown below, click migrate to remove your liquidity from Uniswap V1 and deposit it into Uniswap
|
||||
V2.
|
||||
</TYPE.body>
|
||||
|
||||
{!account ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
Connect to a wallet to view your V1 liquidity.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : isLoading ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
<Dots>Loading</Dots>
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : (
|
||||
<>
|
||||
<AutoRow>
|
||||
<SearchInput
|
||||
value={tokenSearch}
|
||||
onChange={handleTokenSearchChange}
|
||||
placeholder="Enter a token address to find liquidity"
|
||||
/>
|
||||
</AutoRow>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{account && unmigratedLiquidityExchangeAddresses.length === 0 ? (
|
||||
<EmptyState message="No V1 Liquidity found." />
|
||||
) : null}
|
||||
|
||||
{!account ? <ButtonPrimary onClick={toggleWalletModal}>Connect to a wallet</ButtonPrimary> : null}
|
||||
{allV1PairsWithLiquidity?.length > 0 ? (
|
||||
<>{allV1PairsWithLiquidity}</>
|
||||
) : (
|
||||
<EmptyState message="No V1 Liquidity found." />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</BodyWrapper>
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, useContext } from 'react'
|
||||
import React, { useState, useContext, useCallback } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { JSBI, Pair } from '@uniswap/sdk'
|
||||
import { JSBI } from '@uniswap/sdk'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
import Question from '../../components/QuestionHelper'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import PairSearchModal from '../../components/SearchModal/PairSearchModal'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import { useUserHasLiquidityInAllTokens } from '../../data/V1'
|
||||
import { useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks'
|
||||
import { StyledInternalLink, TYPE } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { LightCard } from '../../components/Card'
|
||||
@@ -16,9 +16,10 @@ import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { usePairs } from '../../data/Reserves'
|
||||
import { useAllDummyPairs } from '../../state/user/hooks'
|
||||
import AppBody from '../AppBody'
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
|
||||
const Positions = styled.div`
|
||||
position: relative;
|
||||
@@ -31,36 +32,42 @@ const FixedBottom = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
function PositionCardWrapper({ dummyPair }: { dummyPair: Pair }) {
|
||||
const pair = usePair(dummyPair.token0, dummyPair.token1)
|
||||
return <PositionCard pair={pair} />
|
||||
}
|
||||
|
||||
export default function Pool({ history }: RouteComponentProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { account } = useActiveWeb3React()
|
||||
const [showPoolSearch, setShowPoolSearch] = useState(false)
|
||||
|
||||
// initiate listener for LP balances
|
||||
const pairs = useAllDummyPairs()
|
||||
const pairBalances = useTokenBalances(
|
||||
// fetch the user's balances of all tracked V2 LP tokens
|
||||
const V2DummyPairs = useAllDummyPairs()
|
||||
const [V2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
|
||||
account,
|
||||
pairs?.map(p => p.liquidityToken)
|
||||
V2DummyPairs?.map(p => p.liquidityToken)
|
||||
)
|
||||
// fetch the reserves for all V2 pools in which the user has a balance
|
||||
const V2DummyPairsWithABalance = V2DummyPairs.filter(
|
||||
V2DummyPair =>
|
||||
V2PairsBalances[V2DummyPair.liquidityToken.address] &&
|
||||
JSBI.greaterThan(V2PairsBalances[V2DummyPair.liquidityToken.address].raw, JSBI.BigInt(0))
|
||||
)
|
||||
const V2Pairs = usePairs(
|
||||
V2DummyPairsWithABalance.map(V2DummyPairWithABalance => [
|
||||
V2DummyPairWithABalance.token0,
|
||||
V2DummyPairWithABalance.token1
|
||||
])
|
||||
)
|
||||
const V2IsLoading =
|
||||
fetchingV2PairBalances || V2Pairs?.length < V2DummyPairsWithABalance.length || V2Pairs?.some(V2Pair => !!!V2Pair)
|
||||
|
||||
const filteredExchangeList = pairs
|
||||
.filter(pair => {
|
||||
return (
|
||||
pairBalances?.[pair.liquidityToken.address] &&
|
||||
JSBI.greaterThan(pairBalances[pair.liquidityToken.address].raw, JSBI.BigInt(0))
|
||||
)
|
||||
})
|
||||
.map((pair, i) => {
|
||||
return <PositionCardWrapper key={i} dummyPair={pair} />
|
||||
})
|
||||
const allV2PairsWithLiquidity = V2Pairs.filter(V2Pair => !!V2Pair).map(V2Pair => (
|
||||
<PositionCard key={V2Pair.liquidityToken.address} pair={V2Pair} />
|
||||
))
|
||||
|
||||
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
|
||||
|
||||
const handleSearchDismiss = useCallback(() => {
|
||||
setShowPoolSearch(false)
|
||||
}, [setShowPoolSearch])
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
@@ -72,42 +79,49 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
}}
|
||||
>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Join {filteredExchangeList?.length > 0 ? 'another' : 'a'} pool
|
||||
Join {allV2PairsWithLiquidity?.length > 0 ? 'another' : 'a'} pool
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
|
||||
<Positions>
|
||||
<AutoColumn gap="12px">
|
||||
<RowBetween padding={'0 8px'}>
|
||||
<Text color={theme.text1} fontWeight={500}>
|
||||
Your Pooled Liquidity
|
||||
Your Liquidity
|
||||
</Text>
|
||||
<Question text="When you add liquidity, you are given pool tokens that represent your share. If you don’t see a pool you joined in this list, try importing a pool below." />
|
||||
</RowBetween>
|
||||
{filteredExchangeList?.length === 0 && (
|
||||
<LightCard
|
||||
padding="40px
|
||||
"
|
||||
>
|
||||
|
||||
{!account ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
Connect to a wallet to view your liquidity.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : V2IsLoading ? (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
<Dots>Loading</Dots>
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
) : allV2PairsWithLiquidity?.length > 0 ? (
|
||||
<>{allV2PairsWithLiquidity}</>
|
||||
) : (
|
||||
<LightCard padding="40px">
|
||||
<TYPE.body color={theme.text3} textAlign="center">
|
||||
No liquidity found.
|
||||
</TYPE.body>
|
||||
</LightCard>
|
||||
)}
|
||||
{filteredExchangeList}
|
||||
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
|
||||
{!hasV1Liquidity ? (
|
||||
<>
|
||||
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
|
||||
<StyledInternalLink id="import-pool-link" to="/find">
|
||||
Import it.
|
||||
</StyledInternalLink>
|
||||
</>
|
||||
) : (
|
||||
<StyledInternalLink id="migrate-v1-liquidity-link" to="/migrate/v1">
|
||||
Migrate your V1 liquidity.
|
||||
|
||||
<div>
|
||||
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
|
||||
{hasV1Liquidity ? 'Uniswap V1 liquidity found!' : "Don't see a pool you joined?"}{' '}
|
||||
<StyledInternalLink id="import-pool-link" to={hasV1Liquidity ? '/migrate/v1' : '/find'}>
|
||||
{hasV1Liquidity ? 'Migrate now.' : 'Import it.'}
|
||||
</StyledInternalLink>
|
||||
)}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</AutoColumn>
|
||||
<FixedBottom>
|
||||
<ColumnCenter>
|
||||
@@ -117,7 +131,7 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
</ColumnCenter>
|
||||
</FixedBottom>
|
||||
</Positions>
|
||||
<SearchModal isOpen={showPoolSearch} onDismiss={() => setShowPoolSearch(false)} />
|
||||
<PairSearchModal isOpen={showPoolSearch} onDismiss={handleSearchDismiss} />
|
||||
</AutoColumn>
|
||||
</AppBody>
|
||||
)
|
||||
|
||||
@@ -17,9 +17,13 @@ export const MaxButton = styled.button<{ width: string }>`
|
||||
border: 1px solid ${({ theme }) => theme.primary5};
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
padding: 0.25rem 0.5rem;
|
||||
`};
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
margin: 0.25rem;
|
||||
overflow: hidden;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ButtonDropwdown, ButtonDropwdownLight, ButtonPrimary } from '../../components/Button'
|
||||
import { ButtonDropdownLight } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
import Row from '../../components/Row'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import TokenSearchModal from '../../components/SearchModal/TokenSearchModal'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
@@ -23,138 +22,133 @@ enum Fields {
|
||||
TOKEN1 = 1
|
||||
}
|
||||
|
||||
export default function PoolFinder({ history }: RouteComponentProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
|
||||
export default function PoolFinder() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const [token0Address, setToken0Address] = useState<string>()
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false)
|
||||
const [activeField, setActiveField] = useState<number>(Fields.TOKEN1)
|
||||
|
||||
const [token0Address, setToken0Address] = useState<string>(WETH[chainId].address)
|
||||
const [token1Address, setToken1Address] = useState<string>()
|
||||
const token0: Token = useToken(token0Address)
|
||||
const token1: Token = useToken(token1Address)
|
||||
|
||||
const pair: Pair = usePair(token0, token1)
|
||||
const addPair = usePairAdder()
|
||||
|
||||
useEffect(() => {
|
||||
if (pair) {
|
||||
addPair(pair)
|
||||
}
|
||||
}, [pair, addPair])
|
||||
|
||||
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
|
||||
|
||||
const newPair: boolean =
|
||||
pair === null ||
|
||||
(!!pair && JSBI.equal(pair.reserve0.raw, JSBI.BigInt(0)) && JSBI.equal(pair.reserve1.raw, JSBI.BigInt(0)))
|
||||
const allowImport: boolean = position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
|
||||
|
||||
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
|
||||
const poolImported: boolean = !!position && JSBI.greaterThan(position.raw, JSBI.BigInt(0))
|
||||
|
||||
const handleTokenSelect = useCallback(
|
||||
(address: string) => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
},
|
||||
[activeField]
|
||||
)
|
||||
|
||||
const handleSearchDismiss = useCallback(() => {
|
||||
setShowSearch(false)
|
||||
}, [setShowSearch])
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<AutoColumn gap="md">
|
||||
{!token0Address ? (
|
||||
<ButtonDropwdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select first token</Text>
|
||||
</ButtonDropwdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN0)
|
||||
}}
|
||||
>
|
||||
{token0 ? (
|
||||
<Row>
|
||||
<TokenLogo address={token0Address} />
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
{token0?.symbol}
|
||||
{token0.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
)}
|
||||
) : (
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
Select a Token
|
||||
</Text>
|
||||
)}
|
||||
</ButtonDropdownLight>
|
||||
|
||||
<ColumnCenter>
|
||||
<Plus size="16" color="#888D9B" />
|
||||
</ColumnCenter>
|
||||
{!token1Address ? (
|
||||
<ButtonDropwdown
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20}>Select second token</Text>
|
||||
</ButtonDropwdown>
|
||||
) : (
|
||||
<ButtonDropwdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
|
||||
<ButtonDropdownLight
|
||||
onClick={() => {
|
||||
setShowSearch(true)
|
||||
setActiveField(Fields.TOKEN1)
|
||||
}}
|
||||
>
|
||||
{token1 ? (
|
||||
<Row>
|
||||
<TokenLogo address={token1Address} />
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
{token1?.symbol}
|
||||
{token1.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
</ButtonDropwdownLight>
|
||||
)}
|
||||
{allowImport && (
|
||||
) : (
|
||||
<Text fontWeight={500} fontSize={20} marginLeft={'12px'}>
|
||||
Select a Token
|
||||
</Text>
|
||||
)}
|
||||
</ButtonDropdownLight>
|
||||
|
||||
{poolImported && (
|
||||
<ColumnCenter
|
||||
style={{ justifyItems: 'center', backgroundColor: '', padding: '12px 0px', borderRadius: '12px' }}
|
||||
>
|
||||
<Text textAlign="center" fontWeight={500} color="">
|
||||
Pool Imported!
|
||||
Pool Found!
|
||||
</Text>
|
||||
</ColumnCenter>
|
||||
)}
|
||||
|
||||
{position ? (
|
||||
!JSBI.equal(position.raw, JSBI.BigInt(0)) ? (
|
||||
poolImported ? (
|
||||
<PositionCard pair={pair} minimal={true} border="1px solid #CED0D9" />
|
||||
) : (
|
||||
<LightCard padding="45px 10px">
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Text textAlign="center">Pool found, you don’t have liquidity on this pair yet.</Text>
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>
|
||||
<Text textAlign="center">Add liquidity to this pair instead.</Text>
|
||||
<Text textAlign="center">You don’t have liquidity in this pool yet.</Text>
|
||||
<StyledInternalLink to={`/add/${token0.address}-${token1.address}`}>
|
||||
<Text textAlign="center">Add liquidity?</Text>
|
||||
</StyledInternalLink>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
)
|
||||
) : newPair ? (
|
||||
<LightCard padding="45px">
|
||||
<LightCard padding="45px 10px">
|
||||
<AutoColumn gap="sm" justify="center">
|
||||
<Text color="">No pool found.</Text>
|
||||
<Text textAlign="center">No pool found.</Text>
|
||||
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink>
|
||||
</AutoColumn>
|
||||
</LightCard>
|
||||
) : (
|
||||
<LightCard padding={'45px'}>
|
||||
<Text color="#C3C5CB" textAlign="center">
|
||||
Select a token pair to find your liquidity.
|
||||
<LightCard padding="45px 10px">
|
||||
<Text textAlign="center">
|
||||
{!account ? 'Connect to a wallet to find pools' : 'Select a token to find your liquidity.'}
|
||||
</Text>
|
||||
</LightCard>
|
||||
)}
|
||||
|
||||
<ButtonPrimary disabled={!allowImport} onClick={() => history.goBack()}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
<SearchModal
|
||||
|
||||
<TokenSearchModal
|
||||
isOpen={showSearch}
|
||||
filterType="tokens"
|
||||
onTokenSelect={address => {
|
||||
activeField === Fields.TOKEN0 ? setToken0Address(address) : setToken1Address(address)
|
||||
}}
|
||||
onDismiss={() => {
|
||||
setShowSearch(false)
|
||||
}}
|
||||
onTokenSelect={handleTokenSelect}
|
||||
onDismiss={handleSearchDismiss}
|
||||
hiddenToken={activeField === Fields.TOKEN0 ? token1Address : token0Address}
|
||||
/>
|
||||
</AppBody>
|
||||
|
||||
@@ -7,7 +7,7 @@ import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonConfirmed, ButtonPrimary, ButtonLight, ButtonError } from '../../components/Button'
|
||||
import { ButtonPrimary, ButtonLight, ButtonError, ButtonConfirmed } from '../../components/Button'
|
||||
import { LightCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
@@ -18,7 +18,7 @@ import Row, { RowBetween, RowFixed } from '../../components/Row'
|
||||
|
||||
import Slider from '../../components/Slider'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { ROUTER_ADDRESS } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { usePairContract } from '../../hooks/useContract'
|
||||
|
||||
@@ -31,9 +31,9 @@ import { useApproveCallback, ApprovalState } from '../../hooks/useApproveCallbac
|
||||
import { Dots } from '../../components/swap/styleds'
|
||||
import { useDefaultsFromURLMatchParams, useBurnActionHandlers } from '../../state/burn/hooks'
|
||||
import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import { Field } from '../../state/burn/actions'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useUserDeadline, useUserSlippageTolerance } from '../../state/user/hooks'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
|
||||
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
|
||||
@@ -48,19 +48,18 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
// burn state
|
||||
const { independentField, typedValue } = useBurnState()
|
||||
const { tokens, pair, route, parsedAmounts, error } = useDerivedBurnInfo()
|
||||
const { onUserInput } = useBurnActionHandlers()
|
||||
const { onUserInput: _onUserInput } = useBurnActionHandlers()
|
||||
const isValid = !error
|
||||
|
||||
// modal and loading
|
||||
const [showDetailed, setShowDetailed] = useState<boolean>(false) // toggling detailed view
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
|
||||
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, setShowConfirm] = useState<boolean>(false)
|
||||
const [showDetailed, setShowDetailed] = useState<boolean>(false)
|
||||
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
|
||||
|
||||
// tx parameters
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
// txn values
|
||||
const [txHash, setTxHash] = useState<string>('')
|
||||
const [deadline] = useUserDeadline()
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const formattedAmounts = {
|
||||
[Field.LIQUIDITY_PERCENT]: parsedAmounts[Field.LIQUIDITY_PERCENT].equalTo('0')
|
||||
@@ -144,6 +143,15 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
})
|
||||
}
|
||||
|
||||
// wrapped onUserInput to clear signatures
|
||||
const onUserInput = useCallback(
|
||||
(field: Field, typedValue: string) => {
|
||||
setSignatureData(null)
|
||||
return _onUserInput(field, typedValue)
|
||||
},
|
||||
[_onUserInput]
|
||||
)
|
||||
|
||||
// tx sending
|
||||
const addTransaction = useTransactionAdder()
|
||||
async function onRemove() {
|
||||
@@ -359,34 +367,11 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
</RowBetween>
|
||||
</>
|
||||
)}
|
||||
<RowBetween mt="1rem">
|
||||
<ButtonConfirmed
|
||||
onClick={onAttemptToApprove}
|
||||
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
|
||||
mr="0.5rem"
|
||||
fontWeight={500}
|
||||
fontSize={20}
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
|
||||
<ButtonPrimary
|
||||
disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)}
|
||||
onClick={onRemove}
|
||||
ml="0.5rem"
|
||||
>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Confirm
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
<ButtonPrimary disabled={!(approval === ApprovalState.APPROVED || signatureData !== null)} onClick={onRemove}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Confirm
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -571,34 +556,41 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
|
||||
{!account ? (
|
||||
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error || 'Remove'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
<RowBetween>
|
||||
<ButtonConfirmed
|
||||
onClick={onAttemptToApprove}
|
||||
confirmed={approval === ApprovalState.APPROVED || signatureData !== null}
|
||||
disabled={approval !== ApprovalState.NOT_APPROVED || signatureData !== null}
|
||||
mr="0.5rem"
|
||||
fontWeight={500}
|
||||
fontSize={16}
|
||||
>
|
||||
{approval === ApprovalState.PENDING ? (
|
||||
<Dots>Approving</Dots>
|
||||
) : approval === ApprovalState.APPROVED || signatureData !== null ? (
|
||||
'Approved'
|
||||
) : (
|
||||
'Approve'
|
||||
)}
|
||||
</ButtonConfirmed>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
}}
|
||||
disabled={!isValid || (signatureData === null && approval !== ApprovalState.APPROVED)}
|
||||
error={!isValid && !!parsedAmounts[Field.TOKEN_A] && !!parsedAmounts[Field.TOKEN_B]}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{error || 'Remove'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
)}
|
||||
</div>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{isValid ? (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{pair ? (
|
||||
<AutoColumn style={{ minWidth: '20rem', marginTop: '1rem' }}>
|
||||
<PositionCard pair={pair} minimal={true} />
|
||||
|
||||
@@ -2,7 +2,6 @@ import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import AddressInputPanel from '../../components/AddressInputPanel'
|
||||
@@ -11,24 +10,24 @@ import Card, { BlueCard, GreyCard } from '../../components/Card'
|
||||
import { AutoColumn, ColumnCenter } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../../components/Row'
|
||||
import { AutoRow, RowBetween } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
|
||||
import SwapModalFooter from '../../components/swap/SwapModalFooter'
|
||||
import { ArrowWrapper, BottomGrouping, Dots, InputGroup, StyledNumerical, Wrapper } from '../../components/swap/styleds'
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import { TransferModalHeader } from '../../components/swap/TransferModalHeader'
|
||||
import V1TradeLink from '../../components/swap/V1TradeLink'
|
||||
import BetterTradeLink from '../../components/swap/BetterTradeLink'
|
||||
import TokenLogo from '../../components/TokenLogo'
|
||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
|
||||
import { INITIAL_ALLOWED_SLIPPAGE, MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
|
||||
import { getTradeVersion, isTradeBetter } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useSendCallback } from '../../hooks/useSendCallback'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
@@ -40,10 +39,11 @@ import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
import { useUserSlippageTolerance, useUserDeadline, useExpertModeManager } from '../../state/user/hooks'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
|
||||
export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURLSearch(search)
|
||||
export default function Send() {
|
||||
useDefaultsFromURLSearch()
|
||||
|
||||
// text translation
|
||||
// const { t } = useTranslation()
|
||||
@@ -53,6 +53,10 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// for expert mode
|
||||
const toggleSettings = useToggleSettingsMenu()
|
||||
const [expertMode] = useExpertModeManager()
|
||||
|
||||
// sending state
|
||||
const [sendingWithSwap, setSendingWithSwap] = useState<boolean>(false)
|
||||
const [recipient, setRecipient] = useState<string>('')
|
||||
@@ -62,26 +66,41 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
// trade details, check query params for initial state
|
||||
const { independentField, typedValue } = useSwapState()
|
||||
const {
|
||||
parsedAmounts,
|
||||
bestTrade,
|
||||
parsedAmount,
|
||||
bestTrade: bestTradeV2,
|
||||
tokenBalances,
|
||||
tokens,
|
||||
error: swapError,
|
||||
v1TradeLinkIfBetter
|
||||
v1Trade
|
||||
} = useDerivedSwapInfo()
|
||||
const isSwapValid = !swapError && !recipientError && bestTrade
|
||||
|
||||
const toggledVersion = useToggledVersion()
|
||||
const bestTrade = {
|
||||
[Version.v1]: v1Trade,
|
||||
[Version.v2]: bestTradeV2
|
||||
}[toggledVersion]
|
||||
|
||||
const betterTradeLinkVersion: Version | undefined =
|
||||
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
|
||||
? Version.v1
|
||||
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
|
||||
? Version.v2
|
||||
: undefined
|
||||
|
||||
const parsedAmounts = {
|
||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
|
||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
|
||||
}
|
||||
|
||||
const isSwapValid = !swapError && !recipientError && bestTrade
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
// modal and loading
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
|
||||
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>('')
|
||||
|
||||
// tx parameters
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
const [deadline] = useUserDeadline() // custom from user settings
|
||||
const [allowedSlippage] = useUserSlippageTolerance() // custom from user settings
|
||||
|
||||
const route = bestTrade?.route
|
||||
const userHasSpecifiedInputOutput =
|
||||
@@ -152,7 +171,11 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
ReactGA.event({
|
||||
category: 'Send',
|
||||
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
|
||||
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join(';')
|
||||
label: [
|
||||
bestTrade.inputAmount.token.symbol,
|
||||
bestTrade.outputAmount.token.symbol,
|
||||
getTradeVersion(bestTrade)
|
||||
].join('/')
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -195,7 +218,8 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
((sendingWithSwap && isSwapValid) || (!sendingWithSwap && isSendValid)) &&
|
||||
(approval === ApprovalState.NOT_APPROVED ||
|
||||
approval === ApprovalState.PENDING ||
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED))
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
|
||||
!(severity > 3 && !expertMode)
|
||||
|
||||
function modalHeader() {
|
||||
if (!sendingWithSwap) {
|
||||
@@ -420,7 +444,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={onUserInput}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
label={independentField === Field.INPUT && parsedAmounts[Field.OUTPUT] ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
@@ -450,28 +473,34 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
}}
|
||||
/>
|
||||
</AutoColumn>
|
||||
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
|
||||
{sendingWithSwap && (
|
||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
Price
|
||||
</Text>
|
||||
<TradePrice showInverted={showInverted} setShowInverted={setShowInverted} trade={bestTrade} />
|
||||
<TradePrice
|
||||
inputToken={tokens[Field.INPUT]}
|
||||
outputToken={tokens[Field.OUTPUT]}
|
||||
price={bestTrade?.executionPrice}
|
||||
showInverted={showInverted}
|
||||
setShowInverted={setShowInverted}
|
||||
/>
|
||||
</RowBetween>
|
||||
|
||||
{bestTrade && severity > 1 && (
|
||||
<RowBetween>
|
||||
<TYPE.main
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
fontSize={14}
|
||||
>
|
||||
Price Impact
|
||||
</TYPE.main>
|
||||
<RowFixed>
|
||||
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
|
||||
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
|
||||
</RowFixed>
|
||||
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
|
||||
<RowBetween align="center">
|
||||
<ClickableText>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
Slippage Tolerance
|
||||
</Text>
|
||||
</ClickableText>
|
||||
<ClickableText>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
{allowedSlippage ? allowedSlippage / 100 : '-'}%
|
||||
</Text>
|
||||
</ClickableText>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
@@ -509,7 +538,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
</ButtonPrimary>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
|
||||
}}
|
||||
width="48%"
|
||||
id="send-button"
|
||||
@@ -517,49 +546,38 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
error={sendingWithSwap && isSwapValid && severity > 2}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{`Send${severity > 2 ? ' Anyway' : ''}`}
|
||||
{severity > 3 && !expertMode ? `Price Impact High` : `Send${severity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
expertMode ? (sendingWithSwap ? onSwap() : onSend()) : setShowConfirm(true)
|
||||
}}
|
||||
id="send-button"
|
||||
disabled={(sendingWithSwap && !isSwapValid) || (!sendingWithSwap && !isSendValid)}
|
||||
disabled={
|
||||
(sendingWithSwap && !isSwapValid) ||
|
||||
(!sendingWithSwap && !isSendValid) ||
|
||||
(severity > 3 && !expertMode && sendingWithSwap)
|
||||
}
|
||||
error={sendingWithSwap && isSwapValid && severity > 2}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{(sendingWithSwap ? swapError : null) ||
|
||||
sendAmountError ||
|
||||
recipientError ||
|
||||
(severity > 3 && !expertMode && `Price Impact Too High`) ||
|
||||
`Send${severity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{priceImpactWithoutFee && severity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
<AdvancedSwapDetailsDropdown trade={bestTrade} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import React, { useContext, useState, useEffect } from 'react'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
|
||||
@@ -10,22 +9,24 @@ import Card, { GreyCard } from '../../components/Card'
|
||||
import { AutoColumn } from '../../components/Column'
|
||||
import ConfirmationModal from '../../components/ConfirmationModal'
|
||||
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
|
||||
import QuestionHelper from '../../components/QuestionHelper'
|
||||
import { RowBetween, RowFixed } from '../../components/Row'
|
||||
import { RowBetween } from '../../components/Row'
|
||||
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
|
||||
import confirmPriceImpactWithoutFee from '../../components/swap/confirmPriceImpactWithoutFee'
|
||||
import FormattedPriceImpact from '../../components/swap/FormattedPriceImpact'
|
||||
import { ArrowWrapper, BottomGrouping, Dots, Wrapper } from '../../components/swap/styleds'
|
||||
import SwapModalFooter from '../../components/swap/SwapModalFooter'
|
||||
import SwapModalHeader from '../../components/swap/SwapModalHeader'
|
||||
import TradePrice from '../../components/swap/TradePrice'
|
||||
import V1TradeLink from '../../components/swap/V1TradeLink'
|
||||
import BetterTradeLink from '../../components/swap/BetterTradeLink'
|
||||
import { TokenWarningCards } from '../../components/TokenWarningCard'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE, MIN_ETH } from '../../constants'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useApproveCallback'
|
||||
import { useSwapCallback } from '../../hooks/useSwapCallback'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { useWalletModalToggle, useToggleSettingsMenu } from '../../state/application/hooks'
|
||||
import { useExpertModeManager, useUserSlippageTolerance, useUserDeadline } from '../../state/user/hooks'
|
||||
|
||||
import { INITIAL_ALLOWED_SLIPPAGE, MIN_ETH, BETTER_TRADE_LINK_THRESHOLD } from '../../constants'
|
||||
import { getTradeVersion, isTradeBetter } from '../../data/V1'
|
||||
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import {
|
||||
useDefaultsFromURLSearch,
|
||||
@@ -36,10 +37,10 @@ import {
|
||||
import { CursorPointer, TYPE } from '../../theme'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import AppBody from '../AppBody'
|
||||
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
|
||||
import { ClickableText } from '../Pool/styleds'
|
||||
|
||||
export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
useDefaultsFromURLSearch(search)
|
||||
export default function Swap() {
|
||||
useDefaultsFromURLSearch()
|
||||
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
@@ -47,23 +48,44 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// for expert mode
|
||||
const toggleSettings = useToggleSettingsMenu()
|
||||
const [expertMode] = useExpertModeManager()
|
||||
|
||||
// get custom setting values for user
|
||||
const [deadline] = useUserDeadline()
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
// swap state
|
||||
const { independentField, typedValue } = useSwapState()
|
||||
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
|
||||
const { bestTrade: bestTradeV2, tokenBalances, parsedAmount, tokens, error, v1Trade } = useDerivedSwapInfo()
|
||||
const toggledVersion = useToggledVersion()
|
||||
const bestTrade = {
|
||||
[Version.v1]: v1Trade,
|
||||
[Version.v2]: bestTradeV2
|
||||
}[toggledVersion]
|
||||
|
||||
const betterTradeLinkVersion: Version | undefined =
|
||||
toggledVersion === Version.v2 && isTradeBetter(bestTradeV2, v1Trade, BETTER_TRADE_LINK_THRESHOLD)
|
||||
? Version.v1
|
||||
: toggledVersion === Version.v1 && isTradeBetter(v1Trade, bestTradeV2)
|
||||
? Version.v2
|
||||
: undefined
|
||||
|
||||
const parsedAmounts = {
|
||||
[Field.INPUT]: independentField === Field.INPUT ? parsedAmount : bestTrade?.inputAmount,
|
||||
[Field.OUTPUT]: independentField === Field.OUTPUT ? parsedAmount : bestTrade?.outputAmount
|
||||
}
|
||||
|
||||
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
|
||||
const isValid = !error
|
||||
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
|
||||
|
||||
// modal and loading
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
|
||||
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>('')
|
||||
|
||||
// tx parameters
|
||||
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
|
||||
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
|
||||
|
||||
const formattedAmounts = {
|
||||
[independentField]: typedValue,
|
||||
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
|
||||
@@ -83,13 +105,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// check if user has gone through approval process, used to show two step buttons, reset on token change
|
||||
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
|
||||
|
||||
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
|
||||
const showApproveFlow =
|
||||
!error &&
|
||||
(approval === ApprovalState.NOT_APPROVED ||
|
||||
approval === ApprovalState.PENDING ||
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED))
|
||||
|
||||
// mark when a user has submitted an approval, reset onTokenSelection for input field
|
||||
useEffect(() => {
|
||||
if (approval === ApprovalState.PENDING) {
|
||||
@@ -122,7 +137,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
if (priceImpactWithoutFee && !confirmPriceImpactWithoutFee(priceImpactWithoutFee)) {
|
||||
return
|
||||
}
|
||||
|
||||
setAttemptingTxn(true)
|
||||
swapCallback()
|
||||
.then(hash => {
|
||||
@@ -132,7 +146,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
ReactGA.event({
|
||||
category: 'Swap',
|
||||
action: 'Swap w/o Send',
|
||||
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
|
||||
label: [
|
||||
bestTrade.inputAmount.token.symbol,
|
||||
bestTrade.outputAmount.token.symbol,
|
||||
getTradeVersion(bestTrade)
|
||||
].join('/')
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -150,6 +168,15 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
// warnings on slippage
|
||||
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
|
||||
// never show if price impact is above threshold in non expert mode
|
||||
const showApproveFlow =
|
||||
!error &&
|
||||
(approval === ApprovalState.NOT_APPROVED ||
|
||||
approval === ApprovalState.PENDING ||
|
||||
(approvalSubmitted && approval === ApprovalState.APPROVED)) &&
|
||||
!(priceImpactSeverity > 3 && !expertMode)
|
||||
|
||||
function modalHeader() {
|
||||
return (
|
||||
<SwapModalHeader
|
||||
@@ -245,7 +272,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
field={Field.OUTPUT}
|
||||
value={formattedAmounts[Field.OUTPUT]}
|
||||
onUserInput={onUserInput}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
label={independentField === Field.INPUT ? 'To (estimated)' : 'To'}
|
||||
showMaxButton={false}
|
||||
token={tokens[Field.OUTPUT]}
|
||||
@@ -255,33 +281,33 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
/>
|
||||
</>
|
||||
|
||||
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
|
||||
<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 trade={bestTrade} showInverted={showInverted} setShowInverted={setShowInverted} />
|
||||
</RowBetween>
|
||||
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
|
||||
<AutoColumn gap="4px">
|
||||
<RowBetween align="center">
|
||||
<Text fontWeight={500} fontSize={14} color={theme.text2}>
|
||||
Price
|
||||
</Text>
|
||||
<TradePrice
|
||||
inputToken={tokens[Field.INPUT]}
|
||||
outputToken={tokens[Field.OUTPUT]}
|
||||
price={bestTrade?.executionPrice}
|
||||
showInverted={showInverted}
|
||||
setShowInverted={setShowInverted}
|
||||
/>
|
||||
</RowBetween>
|
||||
|
||||
{bestTrade && priceImpactSeverity > 1 && (
|
||||
<RowBetween>
|
||||
<TYPE.main
|
||||
style={{ justifyContent: 'center', alignItems: 'center', display: 'flex' }}
|
||||
fontSize={14}
|
||||
>
|
||||
Price Impact
|
||||
</TYPE.main>
|
||||
<RowFixed>
|
||||
<FormattedPriceImpact priceImpact={priceImpactWithoutFee} />
|
||||
<QuestionHelper text="The difference between the market price and estimated price due to trade size." />
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
)}
|
||||
{allowedSlippage !== INITIAL_ALLOWED_SLIPPAGE && (
|
||||
<RowBetween align="center">
|
||||
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
Slippage Tolerance
|
||||
</ClickableText>
|
||||
<ClickableText fontWeight={500} fontSize={14} color={theme.text2} onClick={toggleSettings}>
|
||||
{allowedSlippage ? allowedSlippage / 100 : '-'}%
|
||||
</ClickableText>
|
||||
</RowBetween>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Card>
|
||||
</AutoColumn>
|
||||
<BottomGrouping>
|
||||
{!account ? (
|
||||
@@ -308,54 +334,44 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
|
||||
</ButtonPrimary>
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
expertMode ? onSwap() : setShowConfirm(true)
|
||||
}}
|
||||
width="48%"
|
||||
id="swap-button"
|
||||
disabled={!isValid || approval !== ApprovalState.APPROVED}
|
||||
disabled={!isValid || approval !== ApprovalState.APPROVED || (priceImpactSeverity > 3 && !expertMode)}
|
||||
error={isValid && priceImpactSeverity > 2}
|
||||
>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{`Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
{priceImpactSeverity > 3 && !expertMode
|
||||
? `Price Impact High`
|
||||
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
</RowBetween>
|
||||
) : (
|
||||
<ButtonError
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
expertMode ? onSwap() : setShowConfirm(true)
|
||||
}}
|
||||
id="swap-button"
|
||||
disabled={!isValid}
|
||||
disabled={!isValid || (priceImpactSeverity > 3 && !expertMode)}
|
||||
error={isValid && priceImpactSeverity > 2}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{error ?? `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
{error
|
||||
? error
|
||||
: priceImpactSeverity > 3 && !expertMode
|
||||
? `Price Impact Too High`
|
||||
: `Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
)}
|
||||
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
|
||||
{betterTradeLinkVersion && <BetterTradeLink version={betterTradeLinkVersion} />}
|
||||
</BottomGrouping>
|
||||
</Wrapper>
|
||||
</AppBody>
|
||||
|
||||
{bestTrade && (
|
||||
<AdvancedSwapDetailsDropdown
|
||||
trade={bestTrade}
|
||||
rawSlippage={allowedSlippage}
|
||||
deadline={deadline}
|
||||
showAdvanced={showAdvanced}
|
||||
setShowAdvanced={setShowAdvanced}
|
||||
setDeadline={setDeadline}
|
||||
setRawSlippage={setAllowedSlippage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{priceImpactWithoutFee && priceImpactSeverity > 2 && (
|
||||
<AutoColumn gap="lg" style={{ marginTop: '1rem' }}>
|
||||
<PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />
|
||||
</AutoColumn>
|
||||
)}
|
||||
<AdvancedSwapDetailsDropdown trade={bestTrade} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@ export type PopupContent =
|
||||
|
||||
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
|
||||
export const toggleWalletModal = createAction<void>('toggleWalletModal')
|
||||
export const toggleSettingsMenu = createAction<void>('toggleSettingsMenu')
|
||||
export const addPopup = createAction<{ key?: string; content: PopupContent }>('addPopup')
|
||||
export const removePopup = createAction<{ key: string }>('removePopup')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { addPopup, PopupContent, removePopup, toggleWalletModal } from './actions'
|
||||
import { addPopup, PopupContent, removePopup, toggleWalletModal, toggleSettingsMenu } from './actions'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { AppState } from '../index'
|
||||
|
||||
@@ -19,6 +19,15 @@ export function useWalletModalToggle(): () => void {
|
||||
return useCallback(() => dispatch(toggleWalletModal()), [dispatch])
|
||||
}
|
||||
|
||||
export function useSettingsMenuOpen(): boolean {
|
||||
return useSelector((state: AppState) => state.application.settingsMenuOpen)
|
||||
}
|
||||
|
||||
export function useToggleSettingsMenu(): () => void {
|
||||
const dispatch = useDispatch()
|
||||
return useCallback(() => dispatch(toggleSettingsMenu()), [dispatch])
|
||||
}
|
||||
|
||||
// returns a function that allows adding a popup
|
||||
export function useAddPopup(): (content: PopupContent, key?: string) => void {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { createReducer, nanoid } from '@reduxjs/toolkit'
|
||||
import { addPopup, PopupContent, removePopup, toggleWalletModal, updateBlockNumber } from './actions'
|
||||
import {
|
||||
addPopup,
|
||||
PopupContent,
|
||||
removePopup,
|
||||
toggleWalletModal,
|
||||
toggleSettingsMenu,
|
||||
updateBlockNumber
|
||||
} from './actions'
|
||||
|
||||
type PopupList = Array<{ key: string; show: boolean; content: PopupContent }>
|
||||
|
||||
@@ -7,12 +14,14 @@ interface ApplicationState {
|
||||
blockNumber: { [chainId: number]: number }
|
||||
popupList: PopupList
|
||||
walletModalOpen: boolean
|
||||
settingsMenuOpen: boolean
|
||||
}
|
||||
|
||||
const initialState: ApplicationState = {
|
||||
blockNumber: {},
|
||||
popupList: [],
|
||||
walletModalOpen: false
|
||||
walletModalOpen: false,
|
||||
settingsMenuOpen: false
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
@@ -28,6 +37,9 @@ export default createReducer(initialState, builder =>
|
||||
.addCase(toggleWalletModal, state => {
|
||||
state.walletModalOpen = !state.walletModalOpen
|
||||
})
|
||||
.addCase(toggleSettingsMenu, state => {
|
||||
state.settingsMenuOpen = !state.settingsMenuOpen
|
||||
})
|
||||
.addCase(addPopup, (state, { payload: { content, key } }) => {
|
||||
if (key && state.popupList.some(popup => popup.key === key)) return
|
||||
state.popupList.push({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useActiveWeb3React } from '../../hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { Field, typeInput } from './actions'
|
||||
import { setDefaultsFromURLMatchParams } from '../mint/actions'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { Token, Pair, TokenAmount, Percent, JSBI, Route } from '@uniswap/sdk'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTokenBalances } from '../wallet/hooks'
|
||||
@@ -40,12 +40,12 @@ export function useDerivedBurnInfo(): {
|
||||
} = useBurnState()
|
||||
|
||||
// tokens
|
||||
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
|
||||
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
|
||||
const tokenA = useToken(tokenAAddress)
|
||||
const tokenB = useToken(tokenBAddress)
|
||||
const tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token } = useMemo(
|
||||
() => ({
|
||||
[Field.TOKEN_A]: tokenA,
|
||||
[Field.TOKEN_B]: tokenB
|
||||
[Field.TOKEN_A]: tokenA ?? undefined,
|
||||
[Field.TOKEN_B]: tokenB ?? undefined
|
||||
}),
|
||||
[tokenA, tokenB]
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Token, TokenAmount, Route, JSBI, Price, Percent, Pair } from '@uniswap/
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { setDefaultsFromURLMatchParams, Field, typeInput } from './actions'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
|
||||
import { usePair } from '../../data/Reserves'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
@@ -42,8 +42,8 @@ export function useDerivedMintInfo(): {
|
||||
const dependentField = independentField === Field.TOKEN_A ? Field.TOKEN_B : Field.TOKEN_A
|
||||
|
||||
// tokens
|
||||
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
|
||||
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
|
||||
const tokenA = useToken(tokenAAddress)
|
||||
const tokenB = useToken(tokenBAddress)
|
||||
const tokens: { [field in Field]?: Token } = useMemo(
|
||||
() => ({
|
||||
[Field.TOKEN_A]: tokenA,
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { addMulticallListeners, removeMulticallListeners, updateMulticallResults } from './actions'
|
||||
import {
|
||||
addMulticallListeners,
|
||||
errorFetchingMulticallResults,
|
||||
fetchingMulticallResults,
|
||||
removeMulticallListeners,
|
||||
updateMulticallResults
|
||||
} from './actions'
|
||||
import reducer, { MulticallState } from './reducer'
|
||||
import { Store, createStore } from '@reduxjs/toolkit'
|
||||
|
||||
@@ -169,4 +175,89 @@ describe('multicall reducer', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('fetchingMulticallResults', () => {
|
||||
it('updates state to fetching', () => {
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }]
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
[1]: {
|
||||
[`${DAI_ADDRESS}-0x0`]: { fetchingBlockNumber: 2 }
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('errorFetchingMulticallResults', () => {
|
||||
it('does nothing if not fetching', () => {
|
||||
store.dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 1,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }]
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
[1]: {}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('updates block number if we were fetching', () => {
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }]
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }]
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
[1]: {
|
||||
[`${DAI_ADDRESS}-0x0`]: {
|
||||
blockNumber: 2,
|
||||
// null data indicates error
|
||||
data: null
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('does nothing if not errored on latest block', () => {
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 3,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }]
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }]
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
[1]: {
|
||||
[`${DAI_ADDRESS}-0x0`]: { fetchingBlockNumber: 3 }
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -89,8 +89,12 @@ export default createReducer(initialState, builder =>
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (current && current.fetchingBlockNumber !== fetchingBlockNumber) return
|
||||
delete current.fetchingBlockNumber
|
||||
if (!current) return // only should be dispatched if we are already fetching
|
||||
if (current.fetchingBlockNumber === fetchingBlockNumber) {
|
||||
delete current.fetchingBlockNumber
|
||||
current.data = null
|
||||
current.blockNumber = fetchingBlockNumber
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(updateMulticallResults, (state, { payload: { chainId, results, blockNumber } }) => {
|
||||
|
||||
@@ -5,7 +5,12 @@ export enum Field {
|
||||
OUTPUT = 'OUTPUT'
|
||||
}
|
||||
|
||||
export const setDefaultsFromURLSearch = createAction<{ chainId: number; queryString?: string }>('setDefaultsFromURL')
|
||||
export const selectToken = createAction<{ field: Field; address: string }>('selectToken')
|
||||
export const switchTokens = createAction<void>('switchTokens')
|
||||
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInput')
|
||||
export const replaceSwapState = createAction<{
|
||||
field: Field
|
||||
typedValue: string
|
||||
inputTokenAddress?: string
|
||||
outputTokenAddress?: string
|
||||
}>('replaceSwapState')
|
||||
|
||||
53
src/state/swap/hooks.test.ts
Normal file
53
src/state/swap/hooks.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import { parse } from 'qs'
|
||||
import { Field } from './actions'
|
||||
import { queryParametersToSwapState } from './hooks'
|
||||
|
||||
describe('hooks', () => {
|
||||
describe('#queryParametersToSwapState', () => {
|
||||
test('ETH to DAI', () => {
|
||||
expect(
|
||||
queryParametersToSwapState(
|
||||
parse(
|
||||
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT',
|
||||
{ parseArrays: false, ignoreQueryPrefix: true }
|
||||
),
|
||||
ChainId.MAINNET
|
||||
)
|
||||
).toEqual({
|
||||
[Field.OUTPUT]: { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
|
||||
[Field.INPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
typedValue: '20.5',
|
||||
independentField: Field.OUTPUT
|
||||
})
|
||||
})
|
||||
|
||||
test('does not duplicate eth for invalid output token', () => {
|
||||
expect(
|
||||
queryParametersToSwapState(
|
||||
parse('?outputCurrency=invalid', { parseArrays: false, ignoreQueryPrefix: true }),
|
||||
ChainId.MAINNET
|
||||
)
|
||||
).toEqual({
|
||||
[Field.INPUT]: { address: '' },
|
||||
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
typedValue: '',
|
||||
independentField: Field.INPUT
|
||||
})
|
||||
})
|
||||
|
||||
test('output ETH only', () => {
|
||||
expect(
|
||||
queryParametersToSwapState(
|
||||
parse('?outputCurrency=eth&exactAmount=20.5', { parseArrays: false, ignoreQueryPrefix: true }),
|
||||
ChainId.MAINNET
|
||||
)
|
||||
).toEqual({
|
||||
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
[Field.INPUT]: { address: '' },
|
||||
typedValue: '20.5',
|
||||
independentField: Field.INPUT
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,22 @@
|
||||
import { Version } from './../../hooks/useToggledVersion'
|
||||
import { parseUnits } from '@ethersproject/units'
|
||||
import { JSBI, Token, TokenAmount, Trade } from '@uniswap/sdk'
|
||||
import { ChainId, JSBI, Token, TokenAmount, Trade, WETH } from '@uniswap/sdk'
|
||||
import { ParsedQs } from 'qs'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useV1Trade } from '../../data/V1'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useToken } from '../../hooks/Tokens'
|
||||
import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades'
|
||||
import useParsedQueryString from '../../hooks/useParsedQueryString'
|
||||
import { isAddress } from '../../utils'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
|
||||
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
|
||||
import { useV1TradeLinkIfBetter } from '../../data/V1'
|
||||
import { V1_TRADE_LINK_THRESHOLD } from '../../constants'
|
||||
import { Field, replaceSwapState, selectToken, switchTokens, typeInput } from './actions'
|
||||
import { SwapState } from './reducer'
|
||||
import useToggledVersion from '../../hooks/useToggledVersion'
|
||||
import { useUserSlippageTolerance } from '../user/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../../utils/prices'
|
||||
|
||||
export function useSwapState(): AppState['swap'] {
|
||||
return useSelector<AppState, AppState['swap']>(state => state.swap)
|
||||
@@ -33,7 +40,7 @@ export function useSwapActionHandlers(): {
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const onSwapTokens = useCallback(() => {
|
||||
const onSwitchTokens = useCallback(() => {
|
||||
dispatch(switchTokens())
|
||||
}, [dispatch])
|
||||
|
||||
@@ -45,7 +52,7 @@ export function useSwapActionHandlers(): {
|
||||
)
|
||||
|
||||
return {
|
||||
onSwitchTokens: onSwapTokens,
|
||||
onSwitchTokens,
|
||||
onTokenSelection,
|
||||
onUserInput
|
||||
}
|
||||
@@ -73,13 +80,15 @@ export function tryParseAmount(value?: string, token?: Token): TokenAmount | und
|
||||
export function useDerivedSwapInfo(): {
|
||||
tokens: { [field in Field]?: Token }
|
||||
tokenBalances: { [field in Field]?: TokenAmount }
|
||||
parsedAmounts: { [field in Field]?: TokenAmount }
|
||||
parsedAmount: TokenAmount | undefined
|
||||
bestTrade: Trade | null
|
||||
error?: string
|
||||
v1TradeLinkIfBetter?: string
|
||||
v1Trade: Trade | undefined
|
||||
} {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const toggledVersion = useToggledVersion()
|
||||
|
||||
const {
|
||||
independentField,
|
||||
typedValue,
|
||||
@@ -87,58 +96,68 @@ export function useDerivedSwapInfo(): {
|
||||
[Field.OUTPUT]: { address: tokenOutAddress }
|
||||
} = useSwapState()
|
||||
|
||||
const tokenIn = useTokenByAddressAndAutomaticallyAdd(tokenInAddress)
|
||||
const tokenOut = useTokenByAddressAndAutomaticallyAdd(tokenOutAddress)
|
||||
const tokenIn = useToken(tokenInAddress)
|
||||
const tokenOut = useToken(tokenOutAddress)
|
||||
|
||||
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [tokenIn, tokenOut])
|
||||
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
|
||||
tokenIn ?? undefined,
|
||||
tokenOut ?? undefined
|
||||
])
|
||||
|
||||
const isExactIn: boolean = independentField === Field.INPUT
|
||||
const amount = tryParseAmount(typedValue, isExactIn ? tokenIn : tokenOut)
|
||||
const parsedAmount = tryParseAmount(typedValue, (isExactIn ? tokenIn : tokenOut) ?? undefined)
|
||||
|
||||
const bestTradeExactIn = useTradeExactIn(isExactIn ? amount : undefined, tokenOut)
|
||||
const bestTradeExactOut = useTradeExactOut(tokenIn, !isExactIn ? amount : undefined)
|
||||
const bestTradeExactIn = useTradeExactIn(isExactIn ? parsedAmount : undefined, tokenOut ?? undefined)
|
||||
const bestTradeExactOut = useTradeExactOut(tokenIn ?? undefined, !isExactIn ? parsedAmount : undefined)
|
||||
|
||||
const bestTrade = isExactIn ? bestTradeExactIn : bestTradeExactOut
|
||||
|
||||
const parsedAmounts = {
|
||||
[Field.INPUT]: isExactIn ? amount : bestTrade?.inputAmount,
|
||||
[Field.OUTPUT]: isExactIn ? bestTrade?.outputAmount : amount
|
||||
}
|
||||
|
||||
const tokenBalances = {
|
||||
[Field.INPUT]: relevantTokenBalances?.[tokenIn?.address ?? ''],
|
||||
[Field.OUTPUT]: relevantTokenBalances?.[tokenOut?.address ?? '']
|
||||
}
|
||||
|
||||
const tokens: { [field in Field]?: Token } = {
|
||||
[Field.INPUT]: tokenIn,
|
||||
[Field.OUTPUT]: tokenOut
|
||||
[Field.INPUT]: tokenIn ?? undefined,
|
||||
[Field.OUTPUT]: tokenOut ?? undefined
|
||||
}
|
||||
|
||||
// get link to trade on v1, if a better rate exists
|
||||
const v1TradeLinkIfBetter = useV1TradeLinkIfBetter(
|
||||
isExactIn,
|
||||
tokens[Field.INPUT],
|
||||
tokens[Field.OUTPUT],
|
||||
isExactIn ? parsedAmounts[Field.INPUT] : parsedAmounts[Field.OUTPUT],
|
||||
bestTrade ?? undefined,
|
||||
V1_TRADE_LINK_THRESHOLD
|
||||
)
|
||||
const v1Trade = useV1Trade(isExactIn, tokens[Field.INPUT], tokens[Field.OUTPUT], parsedAmount)
|
||||
|
||||
let error: string | undefined
|
||||
if (!account) {
|
||||
error = 'Connect Wallet'
|
||||
}
|
||||
|
||||
if (!parsedAmounts[Field.INPUT]) {
|
||||
if (!parsedAmount) {
|
||||
error = error ?? 'Enter an amount'
|
||||
}
|
||||
|
||||
if (!parsedAmounts[Field.OUTPUT]) {
|
||||
error = error ?? 'Enter an amount'
|
||||
if (!tokens[Field.INPUT] || !tokens[Field.OUTPUT]) {
|
||||
error = error ?? 'Select a token'
|
||||
}
|
||||
|
||||
const [balanceIn, amountIn] = [tokenBalances[Field.INPUT], parsedAmounts[Field.INPUT]]
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const slippageAdjustedAmounts =
|
||||
bestTrade && allowedSlippage && computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
|
||||
|
||||
const slippageAdjustedAmountsV1 =
|
||||
v1Trade && allowedSlippage && computeSlippageAdjustedAmounts(v1Trade, allowedSlippage)
|
||||
|
||||
// compare input balance to MAx input based on version
|
||||
const [balanceIn, amountIn] = [
|
||||
tokenBalances[Field.INPUT],
|
||||
toggledVersion === Version.v1
|
||||
? slippageAdjustedAmountsV1
|
||||
? slippageAdjustedAmountsV1[Field.INPUT]
|
||||
: null
|
||||
: slippageAdjustedAmounts
|
||||
? slippageAdjustedAmounts[Field.INPUT]
|
||||
: null
|
||||
]
|
||||
|
||||
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
|
||||
error = 'Insufficient ' + amountIn.token.symbol + ' balance'
|
||||
}
|
||||
@@ -146,20 +165,72 @@ export function useDerivedSwapInfo(): {
|
||||
return {
|
||||
tokens,
|
||||
tokenBalances,
|
||||
parsedAmounts,
|
||||
parsedAmount,
|
||||
bestTrade,
|
||||
error,
|
||||
v1TradeLinkIfBetter
|
||||
v1Trade
|
||||
}
|
||||
}
|
||||
|
||||
// updates the swap state to use the defaults for a given network whenever the query
|
||||
// string updates
|
||||
export function useDefaultsFromURLSearch(search?: string) {
|
||||
function parseCurrencyFromURLParameter(urlParam: any, chainId: number): string {
|
||||
if (typeof urlParam === 'string') {
|
||||
const valid = isAddress(urlParam)
|
||||
if (valid) return valid
|
||||
if (urlParam.toLowerCase() === 'eth') return WETH[chainId as ChainId]?.address ?? ''
|
||||
if (valid === false) return WETH[chainId as ChainId]?.address ?? ''
|
||||
}
|
||||
|
||||
return WETH[chainId as ChainId]?.address
|
||||
}
|
||||
|
||||
function parseTokenAmountURLParameter(urlParam: any): string {
|
||||
return typeof urlParam === 'string' && !isNaN(parseFloat(urlParam)) ? urlParam : ''
|
||||
}
|
||||
|
||||
function parseIndependentFieldURLParameter(urlParam: any): Field {
|
||||
return typeof urlParam === 'string' && urlParam.toLowerCase() === 'output' ? Field.OUTPUT : Field.INPUT
|
||||
}
|
||||
|
||||
export function queryParametersToSwapState(parsedQs: ParsedQs, chainId: ChainId): SwapState {
|
||||
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency, chainId)
|
||||
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency, chainId)
|
||||
if (inputCurrency === outputCurrency) {
|
||||
if (typeof parsedQs.outputCurrency === 'string') {
|
||||
inputCurrency = ''
|
||||
} else {
|
||||
outputCurrency = ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
[Field.INPUT]: {
|
||||
address: inputCurrency
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
address: outputCurrency
|
||||
},
|
||||
typedValue: parseTokenAmountURLParameter(parsedQs.exactAmount),
|
||||
independentField: parseIndependentFieldURLParameter(parsedQs.exactField)
|
||||
}
|
||||
}
|
||||
|
||||
// updates the swap state to use the defaults for a given network
|
||||
export function useDefaultsFromURLSearch() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const parsedQs = useParsedQueryString()
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainId) return
|
||||
dispatch(setDefaultsFromURLSearch({ chainId, queryString: search }))
|
||||
}, [dispatch, search, chainId])
|
||||
const parsed = queryParametersToSwapState(parsedQs, chainId)
|
||||
dispatch(
|
||||
replaceSwapState({
|
||||
typedValue: parsed.typedValue,
|
||||
field: parsed.independentField,
|
||||
inputTokenAddress: parsed[Field.INPUT].address,
|
||||
outputTokenAddress: parsed[Field.OUTPUT].address
|
||||
})
|
||||
)
|
||||
// eslint-disable-next-line
|
||||
}, [dispatch, chainId])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import { createStore, Store } from 'redux'
|
||||
import { Field, setDefaultsFromURLSearch } from './actions'
|
||||
import { Field, selectToken } from './actions'
|
||||
import reducer, { SwapState } from './reducer'
|
||||
|
||||
describe('swap reducer', () => {
|
||||
@@ -15,54 +14,21 @@ describe('swap reducer', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('setDefaultsFromURL', () => {
|
||||
test('ETH to DAI', () => {
|
||||
describe('selectToken', () => {
|
||||
it('changes token', () => {
|
||||
store.dispatch(
|
||||
setDefaultsFromURLSearch({
|
||||
chainId: ChainId.MAINNET,
|
||||
queryString:
|
||||
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT'
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
[Field.OUTPUT]: { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F' },
|
||||
[Field.INPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
typedValue: '20.5',
|
||||
independentField: Field.OUTPUT
|
||||
})
|
||||
})
|
||||
|
||||
test('does not duplicate eth for invalid output token', () => {
|
||||
store.dispatch(
|
||||
setDefaultsFromURLSearch({
|
||||
chainId: ChainId.MAINNET,
|
||||
queryString: '?outputCurrency=invalid'
|
||||
selectToken({
|
||||
field: Field.OUTPUT,
|
||||
address: '0x0000'
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
[Field.OUTPUT]: { address: '0x0000' },
|
||||
[Field.INPUT]: { address: '' },
|
||||
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
typedValue: '',
|
||||
independentField: Field.INPUT
|
||||
})
|
||||
})
|
||||
|
||||
test('output ETH only', () => {
|
||||
store.dispatch(
|
||||
setDefaultsFromURLSearch({
|
||||
chainId: ChainId.MAINNET,
|
||||
queryString: '?outputCurrency=eth&exactAmount=20.5'
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
[Field.OUTPUT]: { address: WETH[ChainId.MAINNET].address },
|
||||
[Field.INPUT]: { address: '' },
|
||||
typedValue: '20.5',
|
||||
independentField: Field.INPUT
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { parse } from 'qs'
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import { isAddress } from '../../utils'
|
||||
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
|
||||
import { Field, replaceSwapState, selectToken, switchTokens, typeInput } from './actions'
|
||||
|
||||
export interface SwapState {
|
||||
readonly independentField: Field
|
||||
@@ -26,58 +23,18 @@ const initialState: SwapState = {
|
||||
}
|
||||
}
|
||||
|
||||
function parseCurrencyFromURLParameter(urlParam: any, chainId: number): string {
|
||||
if (typeof urlParam === 'string') {
|
||||
const valid = isAddress(urlParam)
|
||||
if (valid) return valid
|
||||
if (urlParam.toLowerCase() === 'eth') return WETH[chainId as ChainId]?.address ?? ''
|
||||
if (valid === false) return WETH[chainId as ChainId]?.address ?? ''
|
||||
}
|
||||
|
||||
return WETH[chainId as ChainId]?.address
|
||||
}
|
||||
|
||||
function parseTokenAmountURLParameter(urlParam: any): string {
|
||||
return typeof urlParam === 'string' && !isNaN(parseFloat(urlParam)) ? urlParam : ''
|
||||
}
|
||||
|
||||
function parseIndependentFieldURLParameter(urlParam: any): Field {
|
||||
return typeof urlParam === 'string' && urlParam.toLowerCase() === 'output' ? Field.OUTPUT : Field.INPUT
|
||||
}
|
||||
|
||||
export default createReducer<SwapState>(initialState, builder =>
|
||||
builder
|
||||
.addCase(setDefaultsFromURLSearch, (_, { payload: { queryString, chainId } }) => {
|
||||
if (queryString && queryString.length > 1) {
|
||||
const parsedQs = parse(queryString, { parseArrays: false, ignoreQueryPrefix: true })
|
||||
|
||||
let inputCurrency = parseCurrencyFromURLParameter(parsedQs.inputCurrency, chainId)
|
||||
let outputCurrency = parseCurrencyFromURLParameter(parsedQs.outputCurrency, chainId)
|
||||
if (inputCurrency === outputCurrency) {
|
||||
if (typeof parsedQs.outputCurrency === 'string') {
|
||||
inputCurrency = ''
|
||||
} else {
|
||||
outputCurrency = ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
[Field.INPUT]: {
|
||||
address: inputCurrency
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
address: outputCurrency
|
||||
},
|
||||
typedValue: parseTokenAmountURLParameter(parsedQs.exactAmount),
|
||||
independentField: parseIndependentFieldURLParameter(parsedQs.exactField)
|
||||
}
|
||||
}
|
||||
|
||||
.addCase(replaceSwapState, (state, { payload: { typedValue, field, inputTokenAddress, outputTokenAddress } }) => {
|
||||
return {
|
||||
...initialState,
|
||||
[Field.INPUT]: {
|
||||
address: WETH[chainId as ChainId]?.address ?? ''
|
||||
}
|
||||
address: inputTokenAddress
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
address: outputTokenAddress
|
||||
},
|
||||
independentField: field,
|
||||
typedValue: typedValue
|
||||
}
|
||||
})
|
||||
.addCase(selectToken, (state, { payload: { address, field } }) => {
|
||||
|
||||
@@ -15,7 +15,7 @@ export const addTransaction = createAction<{
|
||||
chainId: number
|
||||
hash: string
|
||||
from: string
|
||||
approvalOfToken?: string
|
||||
approval?: { tokenAddress: string; spender: string }
|
||||
summary?: string
|
||||
}>('addTransaction')
|
||||
export const clearAllTransactions = createAction<{ chainId: number }>('clearAllTransactions')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import { useCallback } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
@@ -10,7 +10,7 @@ import { TransactionDetails, TransactionState } from './reducer'
|
||||
// helper that can take a ethers library transaction response and add it to the list of transactions
|
||||
export function useTransactionAdder(): (
|
||||
response: TransactionResponse,
|
||||
customData?: { summary?: string; approvalOfToken?: string }
|
||||
customData?: { summary?: string; approval?: { tokenAddress: string; spender: string } }
|
||||
) => void {
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
@@ -18,7 +18,7 @@ export function useTransactionAdder(): (
|
||||
return useCallback(
|
||||
(
|
||||
response: TransactionResponse,
|
||||
{ summary, approvalOfToken }: { summary?: string; approvalOfToken?: string } = {}
|
||||
{ summary, approval }: { summary?: string; approval?: { tokenAddress: string; spender: string } } = {}
|
||||
) => {
|
||||
if (!account) return
|
||||
if (!chainId) return
|
||||
@@ -27,7 +27,7 @@ export function useTransactionAdder(): (
|
||||
if (!hash) {
|
||||
throw Error('No transaction hash found.')
|
||||
}
|
||||
dispatch(addTransaction({ hash, from: account, chainId, approvalOfToken, summary }))
|
||||
dispatch(addTransaction({ hash, from: account, chainId, approval, summary }))
|
||||
},
|
||||
[dispatch, chainId, account]
|
||||
)
|
||||
@@ -51,15 +51,22 @@ export function useIsTransactionPending(transactionHash?: string): boolean {
|
||||
}
|
||||
|
||||
// returns whether a token has a pending approval transaction
|
||||
export function useHasPendingApproval(tokenAddress?: string): boolean {
|
||||
export function useHasPendingApproval(tokenAddress: string | undefined, spender: string | undefined): boolean {
|
||||
const allTransactions = useAllTransactions()
|
||||
return typeof tokenAddress !== 'string'
|
||||
? false
|
||||
: Object.keys(allTransactions).some(hash => {
|
||||
return useMemo(
|
||||
() =>
|
||||
typeof tokenAddress === 'string' &&
|
||||
typeof spender === 'string' &&
|
||||
Object.keys(allTransactions).some(hash => {
|
||||
if (allTransactions[hash]?.receipt) {
|
||||
return false
|
||||
} else {
|
||||
return allTransactions[hash]?.approvalOfToken === tokenAddress
|
||||
return (
|
||||
allTransactions[hash]?.approval?.tokenAddress === tokenAddress &&
|
||||
allTransactions[hash]?.approval?.spender === spender
|
||||
)
|
||||
}
|
||||
})
|
||||
}),
|
||||
[allTransactions, spender, tokenAddress]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const now = () => new Date().getTime()
|
||||
|
||||
export interface TransactionDetails {
|
||||
hash: string
|
||||
approvalOfToken?: string
|
||||
approval?: { tokenAddress: string; spender: string }
|
||||
summary?: string
|
||||
receipt?: SerializableTransactionReceipt
|
||||
addedTime: number
|
||||
@@ -26,12 +26,12 @@ const initialState: TransactionState = {}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(addTransaction, (state, { payload: { chainId, from, hash, approvalOfToken, summary } }) => {
|
||||
.addCase(addTransaction, (state, { payload: { chainId, from, hash, approval, summary } }) => {
|
||||
if (state[chainId]?.[hash]) {
|
||||
throw Error('Attempted to add existing transaction.')
|
||||
}
|
||||
state[chainId] = state[chainId] ?? {}
|
||||
state[chainId][hash] = { hash, approvalOfToken, summary, from, addedTime: now() }
|
||||
state[chainId][hash] = { hash, approval, summary, from, addedTime: now() }
|
||||
})
|
||||
.addCase(clearAllTransactions, (state, { payload: { chainId } }) => {
|
||||
if (!state[chainId]) return
|
||||
|
||||
@@ -16,6 +16,11 @@ export interface SerializedPair {
|
||||
export const updateVersion = createAction<void>('updateVersion')
|
||||
export const updateMatchesDarkMode = createAction<{ matchesDarkMode: boolean }>('updateMatchesDarkMode')
|
||||
export const updateUserDarkMode = createAction<{ userDarkMode: boolean }>('updateUserDarkMode')
|
||||
export const updateUserExpertMode = createAction<{ userExpertMode: boolean }>('updateUserExpertMode')
|
||||
export const updateUserSlippageTolerance = createAction<{ userSlippageTolerance: number }>(
|
||||
'updateUserSlippageTolerance'
|
||||
)
|
||||
export const updateUserDeadline = createAction<{ userDeadline: number }>('updateUserDeadline')
|
||||
export const addSerializedToken = createAction<{ serializedToken: SerializedToken }>('addSerializedToken')
|
||||
export const removeSerializedToken = createAction<{ chainId: number; address: string }>('removeSerializedToken')
|
||||
export const addSerializedPair = createAction<{ serializedPair: SerializedPair }>('addSerializedPair')
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
removeSerializedToken,
|
||||
SerializedPair,
|
||||
SerializedToken,
|
||||
updateUserDarkMode
|
||||
updateUserDarkMode,
|
||||
updateUserExpertMode,
|
||||
updateUserSlippageTolerance,
|
||||
updateUserDeadline
|
||||
} from './actions'
|
||||
import { BASES_TO_TRACK_LIQUIDITY_FOR, DUMMY_PAIRS_TO_PIN } from '../../constants'
|
||||
|
||||
@@ -63,6 +66,54 @@ export function useDarkModeManager(): [boolean, () => void] {
|
||||
return [darkMode, toggleSetDarkMode]
|
||||
}
|
||||
|
||||
export function useIsExpertMode(): boolean {
|
||||
const userExpertMode = useSelector<AppState, AppState['user']['userExpertMode']>(state => state.user.userExpertMode)
|
||||
return userExpertMode
|
||||
}
|
||||
|
||||
export function useExpertModeManager(): [boolean, () => void] {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const expertMode = useIsExpertMode()
|
||||
|
||||
const toggleSetExpertMode = useCallback(() => {
|
||||
dispatch(updateUserExpertMode({ userExpertMode: !expertMode }))
|
||||
}, [expertMode, dispatch])
|
||||
|
||||
return [expertMode, toggleSetExpertMode]
|
||||
}
|
||||
|
||||
export function useUserSlippageTolerance(): [number, (slippage: number) => void] {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const userSlippageTolerance = useSelector<AppState, AppState['user']['userSlippageTolerance']>(state => {
|
||||
return state.user.userSlippageTolerance
|
||||
})
|
||||
|
||||
const setUserSlippageTolerance = useCallback(
|
||||
(userSlippageTolerance: number) => {
|
||||
dispatch(updateUserSlippageTolerance({ userSlippageTolerance }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return [userSlippageTolerance, setUserSlippageTolerance]
|
||||
}
|
||||
|
||||
export function useUserDeadline(): [number, (slippage: number) => void] {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const userDeadline = useSelector<AppState, AppState['user']['userDeadline']>(state => {
|
||||
return state.user.userDeadline
|
||||
})
|
||||
|
||||
const setUserDeadline = useCallback(
|
||||
(userDeadline: number) => {
|
||||
dispatch(updateUserDeadline({ userDeadline }))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return [userDeadline, setUserDeadline]
|
||||
}
|
||||
|
||||
export function useAddUserToken(): (token: Token) => void {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
return useCallback(
|
||||
@@ -171,7 +222,7 @@ export function useAllDummyPairs(): Pair[] {
|
||||
|
||||
// pairs saved by users
|
||||
const savedSerializedPairs = useSelector<AppState, AppState['user']['pairs']>(({ user: { pairs } }) => pairs)
|
||||
const userPairs = useMemo(
|
||||
const userPairs: Pair[] = useMemo(
|
||||
() =>
|
||||
Object.values<SerializedPair>(savedSerializedPairs[chainId ?? -1] ?? {}).map(
|
||||
pair =>
|
||||
|
||||
29
src/state/user/reducer.test.ts
Normal file
29
src/state/user/reducer.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createStore, Store } from 'redux'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
|
||||
import { updateVersion } from './actions'
|
||||
import reducer, { initialState, UserState } from './reducer'
|
||||
|
||||
describe('swap reducer', () => {
|
||||
let store: Store<UserState>
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, initialState)
|
||||
})
|
||||
|
||||
describe('updateVersion', () => {
|
||||
it('has no timestamp originally', () => {
|
||||
expect(store.getState().lastUpdateVersionTimestamp).toBeUndefined()
|
||||
})
|
||||
it('sets the lastUpdateVersionTimestamp', () => {
|
||||
const time = new Date().getTime()
|
||||
store.dispatch(updateVersion())
|
||||
expect(store.getState().lastUpdateVersionTimestamp).toBeGreaterThanOrEqual(time)
|
||||
})
|
||||
it('sets allowed slippage and deadline', () => {
|
||||
store = createStore(reducer, { ...initialState, userDeadline: undefined, userSlippageTolerance: undefined })
|
||||
store.dispatch(updateVersion())
|
||||
expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW)
|
||||
expect(store.getState().userSlippageTolerance).toEqual(INITIAL_ALLOWED_SLIPPAGE)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { INITIAL_ALLOWED_SLIPPAGE, DEFAULT_DEADLINE_FROM_NOW } from '../../constants'
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
@@ -10,17 +10,29 @@ import {
|
||||
SerializedToken,
|
||||
updateMatchesDarkMode,
|
||||
updateUserDarkMode,
|
||||
updateVersion
|
||||
updateVersion,
|
||||
updateUserExpertMode,
|
||||
updateUserSlippageTolerance,
|
||||
updateUserDeadline
|
||||
} from './actions'
|
||||
|
||||
const currentTimestamp = () => new Date().getTime()
|
||||
|
||||
interface UserState {
|
||||
lastVersion: string
|
||||
export interface UserState {
|
||||
// the timestamp of the last updateVersion action
|
||||
lastUpdateVersionTimestamp?: number
|
||||
|
||||
userDarkMode: boolean | null // the user's choice for dark mode or light mode
|
||||
matchesDarkMode: boolean // whether the dark mode media query matches
|
||||
|
||||
userExpertMode: boolean
|
||||
|
||||
// user defined slippage tolerance in bips, used in all txns
|
||||
userSlippageTolerance: number
|
||||
|
||||
// deadline set by user in minutes, used in all txns
|
||||
userDeadline: number
|
||||
|
||||
tokens: {
|
||||
[chainId: number]: {
|
||||
[address: string]: SerializedToken
|
||||
@@ -48,35 +60,31 @@ function pairKey(token0Address: string, token1Address: string) {
|
||||
return `${token0Address};${token1Address}`
|
||||
}
|
||||
|
||||
const initialState: UserState = {
|
||||
lastVersion: '',
|
||||
|
||||
export const initialState: UserState = {
|
||||
userDarkMode: null,
|
||||
matchesDarkMode: false,
|
||||
|
||||
userExpertMode: false,
|
||||
userSlippageTolerance: INITIAL_ALLOWED_SLIPPAGE,
|
||||
userDeadline: DEFAULT_DEADLINE_FROM_NOW,
|
||||
tokens: {},
|
||||
pairs: {},
|
||||
|
||||
timestamp: currentTimestamp()
|
||||
}
|
||||
|
||||
const GIT_COMMIT_HASH: string | undefined = process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(updateVersion, state => {
|
||||
if (GIT_COMMIT_HASH && state.lastVersion !== GIT_COMMIT_HASH) {
|
||||
state.lastVersion = GIT_COMMIT_HASH
|
||||
|
||||
// Wed May 20, 2020 @ ~9pm central
|
||||
if (state.timestamp < 1590027589111) {
|
||||
// this should remove the user added token from 'eth' for mainnet
|
||||
if (state.tokens[ChainId.MAINNET]) {
|
||||
delete state.tokens[ChainId.MAINNET][WETH[ChainId.MAINNET].address]
|
||||
}
|
||||
}
|
||||
// slippage isnt being tracked in local storage, reset to default
|
||||
if (typeof state.userSlippageTolerance !== 'number') {
|
||||
state.userSlippageTolerance = INITIAL_ALLOWED_SLIPPAGE
|
||||
}
|
||||
state.timestamp = currentTimestamp()
|
||||
|
||||
// deadline isnt being tracked in local storage, reset to default
|
||||
if (typeof state.userDeadline !== 'number') {
|
||||
state.userDeadline = DEFAULT_DEADLINE_FROM_NOW
|
||||
}
|
||||
|
||||
state.lastUpdateVersionTimestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(updateUserDarkMode, (state, action) => {
|
||||
state.userDarkMode = action.payload.userDarkMode
|
||||
@@ -86,6 +94,18 @@ export default createReducer(initialState, builder =>
|
||||
state.matchesDarkMode = action.payload.matchesDarkMode
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(updateUserExpertMode, (state, action) => {
|
||||
state.userExpertMode = action.payload.userExpertMode
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(updateUserSlippageTolerance, (state, action) => {
|
||||
state.userSlippageTolerance = action.payload.userSlippageTolerance
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(updateUserDeadline, (state, action) => {
|
||||
state.userDeadline = action.payload.userDeadline
|
||||
state.timestamp = currentTimestamp()
|
||||
})
|
||||
.addCase(addSerializedToken, (state, { payload: { serializedToken } }) => {
|
||||
state.tokens[serializedToken.chainId] = state.tokens[serializedToken.chainId] || {}
|
||||
state.tokens[serializedToken.chainId][serializedToken.address] = serializedToken
|
||||
|
||||
@@ -44,10 +44,10 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
|
||||
/**
|
||||
* Returns a map of token addresses to their eventually consistent token balances for a single account.
|
||||
*/
|
||||
export function useTokenBalances(
|
||||
export function useTokenBalancesWithLoadingIndicator(
|
||||
address?: string,
|
||||
tokens?: (Token | undefined)[]
|
||||
): { [tokenAddress: string]: TokenAmount | undefined } {
|
||||
): [{ [tokenAddress: string]: TokenAmount | undefined }, boolean] {
|
||||
const validatedTokens: Token[] = useMemo(
|
||||
() => tokens?.filter((t?: Token): t is Token => isAddress(t?.address) !== false) ?? [],
|
||||
[tokens]
|
||||
@@ -57,20 +57,32 @@ export function useTokenBalances(
|
||||
|
||||
const balances = useMultipleContractSingleData(validatedTokenAddresses, ERC20_INTERFACE, 'balanceOf', [address])
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
address && validatedTokens.length > 0
|
||||
? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => {
|
||||
const value = balances?.[i]?.result?.[0]
|
||||
const amount = value ? JSBI.BigInt(value.toString()) : undefined
|
||||
if (amount) {
|
||||
memo[token.address] = new TokenAmount(token, amount)
|
||||
}
|
||||
return memo
|
||||
}, {})
|
||||
: {},
|
||||
[address, validatedTokens, balances]
|
||||
)
|
||||
const anyLoading = balances.some(callState => callState.loading)
|
||||
|
||||
return [
|
||||
useMemo(
|
||||
() =>
|
||||
address && validatedTokens.length > 0
|
||||
? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => {
|
||||
const value = balances?.[i]?.result?.[0]
|
||||
const amount = value ? JSBI.BigInt(value.toString()) : undefined
|
||||
if (amount) {
|
||||
memo[token.address] = new TokenAmount(token, amount)
|
||||
}
|
||||
return memo
|
||||
}, {})
|
||||
: {},
|
||||
[address, validatedTokens, balances]
|
||||
),
|
||||
anyLoading
|
||||
]
|
||||
}
|
||||
|
||||
export function useTokenBalances(
|
||||
address?: string,
|
||||
tokens?: (Token | undefined)[]
|
||||
): { [tokenAddress: string]: TokenAmount | undefined } {
|
||||
return useTokenBalancesWithLoadingIndicator(address, tokens)[0]
|
||||
}
|
||||
|
||||
// contains the hacky logic to treat the WETH token input as if it's ETH to
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled, {
|
||||
ThemeProvider as StyledComponentsThemeProvider,
|
||||
@@ -168,9 +169,15 @@ export const TYPE = {
|
||||
|
||||
export const FixedGlobalStyle = createGlobalStyle`
|
||||
@import url('https://rsms.me/inter/inter.css');
|
||||
html, body, input, textarea, button { font-family: 'Inter', sans-serif; letter-spacing: -0.018em;}
|
||||
|
||||
html, input, textarea, button {
|
||||
font-family: 'Inter', sans-serif;
|
||||
letter-spacing: -0.018em;
|
||||
}
|
||||
@supports (font-variation-settings: normal) {
|
||||
html, body, input, textarea, button { font-family: 'Inter var', sans-serif; }
|
||||
html, input, textarea, button {
|
||||
font-family: 'Inter var', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -197,4 +204,15 @@ html {
|
||||
color: ${({ theme }) => theme.text1};
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
background-position: 0 -30vh;
|
||||
background-repeat: no-repeat;
|
||||
background-image: ${({ theme }) =>
|
||||
`radial-gradient(50% 50% at 50% 50%, ${transparentize(0.9, theme.primary1)} 0%, ${transparentize(
|
||||
1,
|
||||
theme.bg1
|
||||
)} 100%)`};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -3,10 +3,10 @@ import { getAddress } from '@ethersproject/address'
|
||||
import { AddressZero } from '@ethersproject/constants'
|
||||
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
|
||||
import { ROUTER_ADDRESS } from '../constants'
|
||||
import { ChainId, JSBI, Percent, TokenAmount } from '@uniswap/sdk'
|
||||
import { ALL_TOKENS } from '../constants/tokens'
|
||||
import { ChainId, JSBI, Percent, TokenAmount, Token } from '@uniswap/sdk'
|
||||
|
||||
// returns the checksummed address if the address is valid, otherwise returns false
|
||||
export function isAddress(value: any): string | false {
|
||||
@@ -92,11 +92,15 @@ export function getRouterContract(_: number, library: Web3Provider, account?: st
|
||||
return getContract(ROUTER_ADDRESS, IUniswapV2Router02ABI, library, account)
|
||||
}
|
||||
|
||||
// account is optional
|
||||
export function getExchangeContract(pairAddress: string, library: Web3Provider, account?: string) {
|
||||
return getContract(pairAddress, IUniswapV2PairABI, library, account)
|
||||
}
|
||||
|
||||
export function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||
}
|
||||
|
||||
export function isDefaultToken(token?: Token): boolean {
|
||||
return Boolean(token && ALL_TOKENS[token.chainId]?.[token.address])
|
||||
}
|
||||
|
||||
export function isCustomAddedToken(allTokens: { [address: string]: Token }, token?: Token): boolean {
|
||||
const isDefault = isDefaultToken(token)
|
||||
return Boolean(token && allTokens[token.address] && !isDefault)
|
||||
}
|
||||
|
||||
40
src/utils/prices.test.ts
Normal file
40
src/utils/prices.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ChainId, JSBI, Pair, Route, Token, TokenAmount, Trade, TradeType } from '@uniswap/sdk'
|
||||
import { computeTradePriceBreakdown } from './prices'
|
||||
|
||||
describe('prices', () => {
|
||||
const token1 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000001', 18)
|
||||
const token2 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000002', 18)
|
||||
const token3 = new Token(ChainId.MAINNET, '0x0000000000000000000000000000000000000003', 18)
|
||||
|
||||
const pair12 = new Pair(new TokenAmount(token1, JSBI.BigInt(10000)), new TokenAmount(token2, JSBI.BigInt(20000)))
|
||||
const pair23 = new Pair(new TokenAmount(token2, JSBI.BigInt(20000)), new TokenAmount(token3, JSBI.BigInt(30000)))
|
||||
|
||||
describe('computeTradePriceBreakdown', () => {
|
||||
it('returns undefined for undefined', () => {
|
||||
expect(computeTradePriceBreakdown(undefined)).toEqual({
|
||||
priceImpactWithoutFee: undefined,
|
||||
realizedLPFee: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('correct realized lp fee for single hop', () => {
|
||||
expect(
|
||||
computeTradePriceBreakdown(
|
||||
new Trade(new Route([pair12], token1), new TokenAmount(token1, JSBI.BigInt(1000)), TradeType.EXACT_INPUT)
|
||||
).realizedLPFee
|
||||
).toEqual(new TokenAmount(token1, JSBI.BigInt(3)))
|
||||
})
|
||||
|
||||
it('correct realized lp fee for double hop', () => {
|
||||
expect(
|
||||
computeTradePriceBreakdown(
|
||||
new Trade(
|
||||
new Route([pair12, pair23], token1),
|
||||
new TokenAmount(token1, JSBI.BigInt(1000)),
|
||||
TradeType.EXACT_INPUT
|
||||
)
|
||||
).realizedLPFee
|
||||
).toEqual(new TokenAmount(token1, JSBI.BigInt(5)))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BLOCKED_PRICE_IMPACT_NON_EXPERT } from './../constants/index'
|
||||
import { Fraction, JSBI, Percent, TokenAmount, Trade } from '@uniswap/sdk'
|
||||
import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_LOW, ALLOWED_PRICE_IMPACT_MEDIUM } from '../constants'
|
||||
import { Field } from '../state/swap/actions'
|
||||
@@ -18,7 +19,7 @@ export function computeTradePriceBreakdown(
|
||||
: ONE_HUNDRED_PERCENT.subtract(
|
||||
trade.route.pairs.reduce<Fraction>(
|
||||
(currentFee: Fraction): Fraction => currentFee.multiply(INPUT_FRACTION_AFTER_FEE),
|
||||
INPUT_FRACTION_AFTER_FEE
|
||||
ONE_HUNDRED_PERCENT
|
||||
)
|
||||
)
|
||||
|
||||
@@ -51,7 +52,8 @@ export function computeSlippageAdjustedAmounts(
|
||||
}
|
||||
}
|
||||
|
||||
export function warningSeverity(priceImpact: Percent): 0 | 1 | 2 | 3 {
|
||||
export function warningSeverity(priceImpact: Percent): 0 | 1 | 2 | 3 | 4 {
|
||||
if (!priceImpact?.lessThan(BLOCKED_PRICE_IMPACT_NON_EXPERT)) return 4
|
||||
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_HIGH)) return 3
|
||||
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 2
|
||||
if (!priceImpact?.lessThan(ALLOWED_PRICE_IMPACT_LOW)) return 1
|
||||
|
||||
221
yarn.lock
221
yarn.lock
@@ -1295,7 +1295,7 @@
|
||||
"@ethersproject/logger" ">=5.0.0-beta.137"
|
||||
"@ethersproject/properties" ">=5.0.0-beta.140"
|
||||
|
||||
"@ethersproject/address@5.0.0-beta.134", "@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.0-beta.134":
|
||||
"@ethersproject/address@>=5.0.0-beta.128", "@ethersproject/address@^5.0.0-beta.134":
|
||||
version "5.0.0-beta.134"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.0.0-beta.134.tgz#9c1790c87b763dc547ac12e2dbc9fa78d0799a71"
|
||||
integrity sha512-FHhUVJTUIg2pXvOOhIt8sB1cQbcwrzZKzf9CPV7JM1auli20nGoYhyMFYGK7u++GXzTMJduIkU1OwlIBupewDw==
|
||||
@@ -1361,13 +1361,6 @@
|
||||
"@ethersproject/properties" ">=5.0.0-beta.140"
|
||||
bn.js "^4.4.0"
|
||||
|
||||
"@ethersproject/bytes@5.0.0-beta.136":
|
||||
version "5.0.0-beta.136"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.0-beta.136.tgz#3aa651df43b44c9e355eba993d8ab4440cb964bb"
|
||||
integrity sha512-yoi5Ul16ScMHVNsf+oCDGaAnj+rtXxITcneXPeDl8h0rk1VNIqb1WKKvooD5WtM0oAglyauuDahHIF+4+5G/Sg==
|
||||
dependencies:
|
||||
"@ethersproject/logger" ">=5.0.0-beta.129"
|
||||
|
||||
"@ethersproject/bytes@>=5.0.0-beta.129":
|
||||
version "5.0.0-beta.137"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.0.0-beta.137.tgz#a9a35e2b358886289225d28212f4071ae391c161"
|
||||
@@ -1711,7 +1704,7 @@
|
||||
"@ethersproject/sha2" ">=5.0.0-beta.129"
|
||||
"@ethersproject/strings" ">=5.0.0-beta.130"
|
||||
|
||||
"@ethersproject/strings@5.0.0-beta.136", "@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.0.0-beta.136":
|
||||
"@ethersproject/strings@>=5.0.0-beta.130", "@ethersproject/strings@^5.0.0-beta.136":
|
||||
version "5.0.0-beta.136"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.0.0-beta.136.tgz#053cbf4f9f96a7537cbc50300597f2d707907f51"
|
||||
integrity sha512-Hb9RvTrgGcOavHvtQZz+AuijB79BO3g1cfF2MeMfCU9ID4j3mbZv/olzDMS2pK9r4aERJpAS94AmlWzCgoY2LQ==
|
||||
@@ -2821,57 +2814,94 @@
|
||||
"@uniswap/lib" "1.1.1"
|
||||
"@uniswap/v2-core" "1.0.0"
|
||||
|
||||
"@walletconnect/browser@^1.0.0-beta.47":
|
||||
version "1.0.0-beta.47"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/browser/-/browser-1.0.0-beta.47.tgz#7e79015ed3b568e416b7532c3864cf7c160f3a3b"
|
||||
integrity sha512-FFT6zqdMIGjjWIFjRY1p/RPeUs5F21YzhrbsSemLyxlRumyQQ3Wotnq8mAKRWPHSzgXkg/GxbTAzIkxciMeuUg==
|
||||
"@walletconnect/client@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.0.8.tgz#0ab3bfa756e51b3dd480f8794b4c515870f79382"
|
||||
integrity sha512-lZkTNTeEvxQ1DNCUmXxWABi0lRAw2ztCFjivP33Am9wJy7VbJliP2H7ipSua3EWPTZ+YLlm95MM6PNvpX2sezA==
|
||||
dependencies:
|
||||
"@walletconnect/core" "^1.0.0-beta.47"
|
||||
"@walletconnect/types" "^1.0.0-beta.47"
|
||||
"@walletconnect/utils" "^1.0.0-beta.47"
|
||||
"@walletconnect/core" "^1.0.8"
|
||||
"@walletconnect/iso-crypto" "^1.0.8"
|
||||
"@walletconnect/types" "^1.0.8"
|
||||
"@walletconnect/utils" "^1.0.8"
|
||||
|
||||
"@walletconnect/core@^1.0.0-beta.47":
|
||||
version "1.0.0-beta.47"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.0.0-beta.47.tgz#56d6efb9d276b9247c251d24653ae25550f2d501"
|
||||
integrity sha512-PdwW9E6kjFnNt11GO2W9gHQY2EIPLYT7qTxN9ZPl1F38v5cWzZBpDQAPQ1QlcJ2kHpZ6V6QDDc/0heEaR//z0Q==
|
||||
"@walletconnect/core@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.0.8.tgz#98c73b5f694e62e77e490f845304c93b1268ff7f"
|
||||
integrity sha512-omZ01i9HMabGJ7RoUw9nAF+6zjjtZcif1zZFCCMMNOspSTyUSjLPFjGCZ66Gt8dA0/amPO6wbN9N6duqo2wxIA==
|
||||
dependencies:
|
||||
"@walletconnect/types" "^1.0.0-beta.47"
|
||||
"@walletconnect/utils" "^1.0.0-beta.47"
|
||||
"@walletconnect/socket-transport" "^1.0.8"
|
||||
"@walletconnect/types" "^1.0.8"
|
||||
"@walletconnect/utils" "^1.0.8"
|
||||
|
||||
"@walletconnect/qrcode-modal@^1.0.0-beta.47":
|
||||
version "1.0.0-beta.47"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.0.0-beta.47.tgz#42dc580c0542db2f468a479b6a0bdc93ced6cc05"
|
||||
integrity sha512-FV3FDbbYeRsTarwWUq4pxjPNsmfZT5f+t8TIH1Uva23fiEG3PcjfWwXuGmoh4vADbtGx8ctO7hSs1Doegtd8KA==
|
||||
"@walletconnect/http-connection@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/http-connection/-/http-connection-1.0.8.tgz#8dc4311806d049f515741a465016fa36a2401f01"
|
||||
integrity sha512-IsXBD12yX1Cw8RlOTJwYREdMRK/CdErIX31TnuqsV0B/Lxaxq1cQisuXbC8EQBtjczM5XU4gfnwgiYstjBrqMg==
|
||||
dependencies:
|
||||
"@walletconnect/types" "^1.0.8"
|
||||
"@walletconnect/utils" "^1.0.8"
|
||||
xhr2-cookies "1.1.0"
|
||||
|
||||
"@walletconnect/iso-crypto@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/iso-crypto/-/iso-crypto-1.0.8.tgz#ef3b1aa49ba4efee694e5ff7b736209469fea372"
|
||||
integrity sha512-EU7fKELVJH6cnyh51lfuC4yeB2TMYca2vCxnD0N1TTdpKqHuw/S9B14MXWy5sgVfG8KItEupZ9wIQu6t1U4TWg==
|
||||
dependencies:
|
||||
"@walletconnect/types" "^1.0.8"
|
||||
"@walletconnect/utils" "^1.0.8"
|
||||
eccrypto-js "5.2.0"
|
||||
|
||||
"@walletconnect/mobile-registry@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/mobile-registry/-/mobile-registry-1.0.8.tgz#dfb84fb8a1cf90698ea2c685406f3e7c9cb295ea"
|
||||
integrity sha512-LtMMlG2aRG+QoCuD+IrCVtzbm61T3izcygMWjyFyzbEAFSkbC8svRJ0b5XV45p/j7SMUCDAlrad1jZKeQYv2hA==
|
||||
|
||||
"@walletconnect/qrcode-modal@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.0.8.tgz#d4e5febfb21b5f4da45f338de71fed59065d00d7"
|
||||
integrity sha512-Tvd2gZWO7HPxxvhKfAgWawo+AHbmUaEDuCd2BeyWfvzzZiHYchdgPbTI200kQaGzw/EZ9iC2KkKZVzXjC83EXA==
|
||||
dependencies:
|
||||
"@walletconnect/mobile-registry" "^1.0.8"
|
||||
"@walletconnect/types" "^1.0.8"
|
||||
"@walletconnect/utils" "^1.0.8"
|
||||
preact "10.4.1"
|
||||
qr-image "3.2.0"
|
||||
qrcode-terminal "0.12.0"
|
||||
|
||||
"@walletconnect/types@^1.0.0-beta.47":
|
||||
version "1.0.0-beta.47"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.0.0-beta.47.tgz#d790b33902629e05d7e18f6cbb6774c4a2f0619f"
|
||||
integrity sha512-lxjBiNLLDOsyEaoB1nlBDrgznV0477udMfN4zvEuv+bNL+dxH27yQI1mM1VqIKIhrEaibjswLJGaweEMzgynoQ==
|
||||
|
||||
"@walletconnect/utils@^1.0.0-beta.47":
|
||||
version "1.0.0-beta.47"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.0.0-beta.47.tgz#b1ffa5e0d05d5f13aa76c72d9b9eca98085a4420"
|
||||
integrity sha512-il8QKvf8AaYpW8xC9mjXBiOH8CkCeV5W7CZAIfVxuJ46WV4XyIAxhEKvF8zGWGKRjz4LjFj3r3l1nyrxeIkrMA==
|
||||
"@walletconnect/socket-transport@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/socket-transport/-/socket-transport-1.0.8.tgz#f0c40e9a01d6f11b0f853b3e502845647c20cd20"
|
||||
integrity sha512-Ub3Ni43tKIWYhg/4+AohyYXnXSuS9tARLQVWbTGCOGolEbslGSA10e0jX+m/AV+J0S1CCcTXRhEFH3Hr6djasw==
|
||||
dependencies:
|
||||
"@ethersproject/address" "5.0.0-beta.134"
|
||||
"@ethersproject/bytes" "5.0.0-beta.136"
|
||||
"@ethersproject/strings" "5.0.0-beta.136"
|
||||
"@walletconnect/types" "^1.0.0-beta.47"
|
||||
bignumber.js "9.0.0"
|
||||
"@walletconnect/types" "^1.0.8"
|
||||
ws "7.3.0"
|
||||
|
||||
"@walletconnect/web3-provider@^1.0.0-beta.39":
|
||||
version "1.0.0-beta.47"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.0.0-beta.47.tgz#797c9903fe5b26b43c23247b9b32d7d743018d56"
|
||||
integrity sha512-mbtmDdp/RmsJzB7kkIFGDvfhQ7vIDSsKBTvpD7GUzXDi15yvQTNt9Ak7OUOe/9N7AO9X9gBf0J/lE+yqoBUiXA==
|
||||
"@walletconnect/types@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.0.8.tgz#3d510c08e250881447281c8e5e081997ba0e57a6"
|
||||
integrity sha512-2h27+Da5buAcJq/xvZpHJX8AWrKzhVpWt6FHeODPdT/P57+rbLQ1emHG8/vpzjSCkHYyhV+8oXQ9fgHkjnpsiQ==
|
||||
|
||||
"@walletconnect/utils@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.0.8.tgz#0798b4520ae1452b134bea757cc220c6cf27c805"
|
||||
integrity sha512-BzyXD4CPPpueL7u3EtLB4+VEc9aJHhnEL/SYRS7nBzGYaYIir3biYi7zb/EANey0mSjCoE2qzxtTmjEi8cBIZg==
|
||||
dependencies:
|
||||
"@walletconnect/browser" "^1.0.0-beta.47"
|
||||
"@walletconnect/qrcode-modal" "^1.0.0-beta.47"
|
||||
"@walletconnect/types" "^1.0.0-beta.47"
|
||||
web3-provider-engine "15.0.4"
|
||||
xhr2-cookies "1.1.0"
|
||||
"@walletconnect/types" "^1.0.8"
|
||||
detect-browser "5.1.0"
|
||||
enc-utils "2.1.0"
|
||||
js-sha3 "0.8.0"
|
||||
|
||||
"@walletconnect/web3-provider@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.0.8.tgz#ab41a522daa1da1b22a42498f90506e7d541b4dc"
|
||||
integrity sha512-u7i/1wp3PKS6eB/yiV5Z7okzPF8q322V9dsHBaRDYEGphyQlBb4EH0aDeecVmu1p95CvIauWhxCP5m+7SL/Dfw==
|
||||
dependencies:
|
||||
"@walletconnect/client" "^1.0.8"
|
||||
"@walletconnect/http-connection" "^1.0.8"
|
||||
"@walletconnect/qrcode-modal" "^1.0.8"
|
||||
"@walletconnect/types" "^1.0.8"
|
||||
"@walletconnect/utils" "^1.0.8"
|
||||
web3-provider-engine "15.0.7"
|
||||
|
||||
"@web3-react/abstract-connector@^6.0.7":
|
||||
version "6.0.7"
|
||||
@@ -2925,12 +2955,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/types/-/types-6.0.7.tgz#34a6204224467eedc6123abaf55fbb6baeb2809f"
|
||||
integrity sha512-ofGmfDhxmNT1/P/MgVa8IKSkCStFiyvXe+U5tyZurKdrtTDFU+wJ/LxClPDtFerWpczNFPUSrKcuhfPX1sI6+A==
|
||||
|
||||
"@web3-react/walletconnect-connector@^6.0.9":
|
||||
version "6.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/walletconnect-connector/-/walletconnect-connector-6.0.9.tgz#76cdbf39ca670ce1a14fa254d1e5fc5a6efbe5ed"
|
||||
integrity sha512-k+rjDgxaoUrMMVt4ssopVh/OMKVhgcpgeogqZnaMzCR1i07z6nH0gNrtVg0ddevbiLkMmnp4ieE8ilpZAgsDOw==
|
||||
"@web3-react/walletconnect-connector@^6.1.1":
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@web3-react/walletconnect-connector/-/walletconnect-connector-6.1.1.tgz#34b71959d997261bbffe1997bcddd23930ac2245"
|
||||
integrity sha512-jQDqDJogtsVmzAbvuf6BHdfALrmlroTpcV9etaIM+xDHwnaUt7M/0X4Q6veFGjBw8CDZAX9xxtXoXLxM2f3jVg==
|
||||
dependencies:
|
||||
"@walletconnect/web3-provider" "^1.0.0-beta.39"
|
||||
"@walletconnect/web3-provider" "^1.0.8"
|
||||
"@web3-react/abstract-connector" "^6.0.7"
|
||||
"@web3-react/types" "^6.0.7"
|
||||
tiny-invariant "^1.0.6"
|
||||
@@ -3186,6 +3216,11 @@ aes-js@3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d"
|
||||
integrity sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0=
|
||||
|
||||
aes-js@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a"
|
||||
integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==
|
||||
|
||||
aggregate-error@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0"
|
||||
@@ -4306,11 +4341,6 @@ big.js@^5.2.2:
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
bignumber.js@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075"
|
||||
integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==
|
||||
|
||||
binary-extensions@^1.0.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
|
||||
@@ -4350,7 +4380,7 @@ bluebird@3.7.2, bluebird@^3.5.5:
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.8, bn.js@^4.4.0, bn.js@^4.8.0:
|
||||
bn.js@4.11.8, bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.0, bn.js@^4.11.1, bn.js@^4.11.8, bn.js@^4.4.0, bn.js@^4.8.0:
|
||||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
|
||||
@@ -5937,6 +5967,11 @@ destroy@~1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
||||
|
||||
detect-browser@5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.1.0.tgz#0c51c66b747ad8f98a6832bf3026a5a23a7850ff"
|
||||
integrity sha512-WKa9p+/MNwmTiS+V2AS6eGxic+807qvnV3hC+4z2GTY+F42h1n8AynVTMMc4EJBC32qMs6yjOTpeDEQQt/AVqQ==
|
||||
|
||||
detect-indent@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
|
||||
@@ -6160,6 +6195,18 @@ ecc-jsbn@~0.1.1:
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
eccrypto-js@5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eccrypto-js/-/eccrypto-js-5.2.0.tgz#eb3b36e9978d316fedf50be46492bb0d3e240cf5"
|
||||
integrity sha512-pPb6CMapJ1LIzjLWxMqlrnfaEFap7qkk9wcO/b4AVSdxBQYlpOqvlPpq5SpUI4FdmfdhVD34AjN47fM8fryC4A==
|
||||
dependencies:
|
||||
aes-js "3.1.2"
|
||||
enc-utils "2.1.0"
|
||||
hash.js "1.1.7"
|
||||
js-sha3 "0.8.0"
|
||||
randombytes "2.1.0"
|
||||
secp256k1 "3.8.0"
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
@@ -6208,6 +6255,15 @@ emojis-list@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||
|
||||
enc-utils@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/enc-utils/-/enc-utils-2.1.0.tgz#f6c28c3d4bb38fb409a93185848cf361f4fde142"
|
||||
integrity sha512-VD0eunGDyzhojePzkORWDnW88gi6tIeGb5Z6QVHugux6mMAPiXyw94fb/7WdDQEWhKMSoYRyzFFUebCqeH20PA==
|
||||
dependencies:
|
||||
bn.js "4.11.8"
|
||||
is-typedarray "1.0.0"
|
||||
typedarray-to-buffer "3.1.5"
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
@@ -6600,7 +6656,7 @@ eth-json-rpc-errors@^1.0.1:
|
||||
dependencies:
|
||||
fast-safe-stringify "^2.0.6"
|
||||
|
||||
eth-json-rpc-errors@^2.0.1:
|
||||
eth-json-rpc-errors@^2.0.1, eth-json-rpc-errors@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/eth-json-rpc-errors/-/eth-json-rpc-errors-2.0.2.tgz#c1965de0301fe941c058e928bebaba2e1285e3c4"
|
||||
integrity sha512-uBCRM2w2ewusRHGxN8JhcuOb2RN3ueAOYH/0BhqdFmQkZx5lj5+fLKTz0mIVOzd4FG5/kUksCzCD7eTEim6gaA==
|
||||
@@ -7906,7 +7962,7 @@ hash.js@1.1.3:
|
||||
inherits "^2.0.3"
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
hash.js@^1.0.0, hash.js@^1.0.3:
|
||||
hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
|
||||
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
|
||||
@@ -8715,7 +8771,7 @@ is-symbol@^1.0.2:
|
||||
dependencies:
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-typedarray@~1.0.0:
|
||||
is-typedarray@1.0.0, is-typedarray@^1.0.0, is-typedarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
|
||||
@@ -9235,6 +9291,11 @@ js-sha3@0.5.7:
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7"
|
||||
integrity sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=
|
||||
|
||||
js-sha3@0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
|
||||
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
|
||||
|
||||
js-sha3@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.6.1.tgz#5b89f77a7477679877f58c4a075240934b1f95c0"
|
||||
@@ -11839,7 +11900,7 @@ postcss@^7, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, po
|
||||
source-map "^0.6.1"
|
||||
supports-color "^6.1.0"
|
||||
|
||||
preact@^10.3.3:
|
||||
preact@10.4.1, preact@^10.3.3:
|
||||
version "10.4.1"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.4.1.tgz#9b3ba020547673a231c6cf16f0fbaef0e8863431"
|
||||
integrity sha512-WKrRpCSwL2t3tpOOGhf2WfTpcmbpxaWtDbdJdKdjd0aEiTkvOmS4NBkG6kzlaAHI9AkQ3iVqbFWM3Ei7mZ4o1Q==
|
||||
@@ -12126,7 +12187,7 @@ ramda@0.26.1:
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
|
||||
integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||
randombytes@2.1.0, randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
|
||||
@@ -13146,7 +13207,7 @@ scrypt-js@3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.0.tgz#52361c1f272eeaab09ec1f806ea82078bca58b15"
|
||||
integrity sha512-7CC7aufwukEvqdmllR0ny0QaSg0+S22xKXrXz3ZahaV6J+fgD2YAtrjtImuoDWog17/Ty9Q4HBmnXEXJ3JkfQA==
|
||||
|
||||
secp256k1@^3.0.1:
|
||||
secp256k1@3.8.0, secp256k1@^3.0.1:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.8.0.tgz#28f59f4b01dbee9575f56a47034b7d2e3b3b352d"
|
||||
integrity sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==
|
||||
@@ -14375,6 +14436,13 @@ type@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
|
||||
integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
|
||||
|
||||
typedarray-to-buffer@3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||
integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
|
||||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typedarray@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
@@ -14721,17 +14789,17 @@ wbuf@^1.1.0, wbuf@^1.7.3:
|
||||
dependencies:
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
web3-provider-engine@15.0.4:
|
||||
version "15.0.4"
|
||||
resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-15.0.4.tgz#5c336bcad2274dff5218bc8db003fa4e9e464c24"
|
||||
integrity sha512-Ob9oK0TUZfVC7NXkB7CQSWAiCdCD/Xnlh2zTnV8NdJR8LCrMAy2i6JedU70JHaxw59y7mM4GnsYOTTGkquFnNQ==
|
||||
web3-provider-engine@15.0.7:
|
||||
version "15.0.7"
|
||||
resolved "https://registry.yarnpkg.com/web3-provider-engine/-/web3-provider-engine-15.0.7.tgz#2439cdb145140660eb1007e7c6acd2d2d867b432"
|
||||
integrity sha512-0NN0JTc4O/J9NFBtdqc4Ug+ujnniIBTCvauw3OlgZzfjnwr4irDU5CpviS5v33arYpC+WMnaDunad/OFrO/Wcw==
|
||||
dependencies:
|
||||
async "^2.5.0"
|
||||
backoff "^2.5.0"
|
||||
clone "^2.0.0"
|
||||
cross-fetch "^2.1.0"
|
||||
eth-block-tracker "^4.4.2"
|
||||
eth-json-rpc-errors "^1.0.1"
|
||||
eth-json-rpc-errors "^2.0.2"
|
||||
eth-json-rpc-filters "^4.1.1"
|
||||
eth-json-rpc-infura "^4.0.1"
|
||||
eth-json-rpc-middleware "^4.1.5"
|
||||
@@ -14869,9 +14937,9 @@ websocket-driver@>=0.5.1:
|
||||
websocket-extensions ">=0.1.1"
|
||||
|
||||
websocket-extensions@>=0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
|
||||
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
|
||||
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
|
||||
|
||||
whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
|
||||
version "1.0.5"
|
||||
@@ -15144,6 +15212,11 @@ ws@7.2.3:
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
|
||||
integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
|
||||
|
||||
ws@7.3.0:
|
||||
version "7.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd"
|
||||
integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==
|
||||
|
||||
ws@^5.1.1, ws@^5.2.0:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
|
||||
|
||||
Reference in New Issue
Block a user