Compare commits

...

35 Commits

Author SHA1 Message Date
Moody Salem
d4911d1054 chore(ipfs migration): changes for ipfs url migration
- remove netlify stuff
- update rename to uniswap-interface
- always use hash router
2020-06-30 13:41:51 -04:00
Moody Salem
90df9c4ced improvement(layout): move header version switch, drop footer for mobile (#910)
* version switch tweaks

* Mobile layout and toggle tweaks

* Remove the entire footer

Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
2020-06-29 16:55:33 -04:00
Callil Capuozzo
14f15d1fd6 fix(i18n): Fix return characters and remove uneeded file (#912) 2020-06-29 14:15:45 -04:00
Moody Salem
69818ace1f fix(popover): animation getting stuck open on firefox 2020-06-28 13:55:54 -04:00
Moody Salem
42906d6709 add BAL 2020-06-27 12:27:10 -04:00
Moody Salem
2f8936a980 unused keys 2020-06-26 14:46:22 -04:00
Moody Salem
f5c4468c3c fix(token logo): fix persistent error state in token logo, clean up swap route code 2020-06-26 14:44:33 -04:00
Moody Salem
852e8f749f fix(swap routing): max hops back to 3 2020-06-26 14:12:12 -04:00
Moody Salem
6694e5e398 improvement(swap routing): consider more bases in the swap (#909)
* consider more bases in the swap

* all match type

* max hops 2, only 1 result
2020-06-26 13:27:38 -04:00
Noah Zinsmeister
2c9a50a372 remove trust deep link 2020-06-25 10:20:27 -04:00
Noah Zinsmeister
0fc0cba6de bump walletconnect 2020-06-25 10:17:49 -04:00
Moody Salem
041c86c04d fix dns variable 2020-06-24 20:07:50 -05:00
Moody Salem
123373e671 docs in release, trigger another release 2020-06-24 19:33:26 -05:00
Moody Salem
eb1732deee release text 2020-06-24 19:15:18 -05:00
Moody Salem
3c13321a71 point at a specific audited commit for the cloudflare update action 2020-06-24 19:12:26 -05:00
Moody Salem
58703f31a0 chore(release): update cloudflare's DNS instead of vercel's DNS 2020-06-24 19:09:02 -05:00
Moody Salem
58721fb191 improvement(remove liquidity): fix width of buttons on small screens 2020-06-24 11:55:19 -05:00
Noah Zinsmeister
678cd1a06f upgrade to walletconnect v1 (#903) 2020-06-23 16:18:04 -04:00
Moody Salem
a5ff3beb92 add a comment for the previous change 2020-06-22 16:12:02 -05:00
Moody Salem
35ccf425f6 improvement(modals): do not focus inputs automatically on mobile 2020-06-22 16:11:32 -05:00
Moody Salem
fe030412cd improvement(pair search modal): show exact symbol match pairs first, filter before sorting 2020-06-22 14:37:52 -05:00
Moody Salem
4d5a43351f fix(trustwallet confirm modal): fix the confirmation modal to not be obscured by the trustwallet bar 2020-06-15 23:09:33 -04:00
Moody Salem
ac1bc3b3a6 cleanup(modal): clean up modal code a bit 2020-06-15 16:46:38 -04:00
Noah Zinsmeister
d1063d50ed add COMP, mUSD, STAKE 2020-06-15 16:24:39 -04:00
Moody Salem
46fc74e90f chore(deploy): trigger a deploy and also trigger deploys on .env.production changes 2020-06-15 10:19:46 -04:00
Moody Salem
2c4f4092d8 improvement(advanced): always show advanced (#890)
* always show advanced

* fix test

* always show the price, less jitter

* show tokens in price field

* fix the dropdown sticking around when switching between swap/send

* lint

* fix ios scrolling the modal body into the viewport bug

* don't use react-spring for simple slide animation

* safer price impact constants
2020-06-15 10:13:12 -04:00
Moody Salem
aac7268dc8 just drop the git commit hash (more trouble than it's worth) 2020-06-15 09:36:39 -04:00
Callil Capuozzo
fd162a72ff feat(expert mode): Add expert mode (#828)
* Add expert mode scaffolding

* move advanced settings to settings tab, add expert mode

* update settings modal

* update font weight

* fix text

* update with modal
;

* add null checks

* update with input checking

* merge and add fixes

* update language and bg

Co-authored-by: ianlapham <ianlapham@gmail.com>
2020-06-12 15:26:28 -04:00
Noah Zinsmeister
e20936709c improvement(migration): improve v1 migration flow (#885)
* first stab at improving v1 migration flow

* lint errors

* improve UI

* fix loading indicator

* switch back to dedicated migration UI

* address comments

* make migrate consistent with new token behavior

* hooks -> utils
2020-06-12 14:04:42 -04:00
Moody Salem
2fda2c8c15 fix(background image): cuts off at the bottom when scrolling 2020-06-12 13:42:35 -04:00
Moody Salem
1f09757c49 fix(lp fee): correct the computation of the realized LP fee 2020-06-12 12:07:28 -04:00
Moody Salem
7e49babff7 improvement(token search): No automatic add (#888)
* no automatic add, major refactors

* fix nit with version link

* add/remove links in the token search modal

* close tooltip when user types

* remove skip
2020-06-12 11:15:18 -04:00
Noah Zinsmeister
b35653ade1 fix approval bugs (#887)
clear signature on input in remove
2020-06-11 19:34:53 -04:00
Ian Lapham
57b53013d1 improvement(add/remove liquidity): update approve flow on add + remove (#879)
* update approve flow on add + remove

* add confirm to remove page
2020-06-11 17:01:39 -04:00
Moody Salem
bafd3f3c05 feat(v1-support): Enable executing swaps on v1 or v2 (#883)
* swap-v1

* toggle the version switch based on the search query parameter

* rework some of the query parameter stuff in send/swap

* hide the url when they click it

* allow switching back to v2 via the toggle

* represent the v1 trade in the UI if they toggle it on

* show trade link in both directions (5% threshold for v1 link)

* input amounts should reflect v1/v2

* perform the approve on v1 exchange for v1 trades

* get swap on v1 working

* move some code around to reduce duplication

* fix ts error

* correct input allowance

* fix exact token to token on v1

* fix pending approvals to be specific to the spender

* google analytics for swap version

* disable the version switch on pages other than swap and send
2020-06-11 15:56:28 -04:00
91 changed files with 3090 additions and 2008 deletions

View File

@@ -10,6 +10,7 @@ on:
- v2
paths:
- '.github/workflows/release.yaml'
- '.env.production'
jobs:
bump_version:
@@ -46,7 +47,7 @@ jobs:
run: yarn install --ignore-scripts --frozen-lockfile
- name: Build the IPFS bundle
run: yarn ipfs-build
run: yarn build
- name: Pin to IPFS
id: upload
@@ -64,14 +65,14 @@ jobs:
cidv0: ${{ steps.upload.outputs.hash }}
- name: Update DNS with new IPFS hash
uses: uniswap/replace-vercel-dns-records@v1.0.0
env:
CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }}
RECORD_DOMAIN: 'uniswap.org'
RECORD_NAME: '_dnslink.app'
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
uses: textileio/cloudflare-update-dnslink@0fe7b7a1ffc865db3a4da9773f0f987447ad5848
with:
domain: 'uniswap.org'
subdomain: '_dnslink.app'
record-type: 'TXT'
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
token: ${{ secrets.VERCEL_TOKEN }}
team-name: 'uniswap'
cid: ${{ steps.upload.outputs.hash }}
- name: Create GitHub Release
id: create_release
@@ -86,9 +87,13 @@ jobs:
- CIDv0: `${{ steps.upload.outputs.hash }}`
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
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. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
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/

View File

@@ -1,6 +1,6 @@
# Uniswap Frontend
[![Tests](https://github.com/Uniswap/uniswap-frontend/workflows/Tests/badge.svg)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
[![Tests](https://github.com/Uniswap/uniswap-interface/workflows/Tests/badge.svg)](https://github.com/Uniswap/uniswap-interface/actions?query=workflow%3ATests)
[![Styled With Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/)
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
@@ -16,7 +16,7 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
## Accessing the frontend
To access the front end, use an IPFS gateway link from the
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest)
or visit [uniswap.exchange](https://uniswap.exchange).
## Development
@@ -54,4 +54,4 @@ CI checks will run against all PRs.
## Accessing Uniswap V1 interface
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways linked
from the [v1.0.0 release](https://github.com/Uniswap/uniswap-frontend/releases/tag/v1.0.0).
from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).

View File

@@ -37,7 +37,6 @@ describe('Swap', () => {
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
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')
})

View File

@@ -1,27 +0,0 @@
# block some countries
[[redirects]]
from = "/*"
to = "/451.html"
status = 451
force = true
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
headers = {Link="<https://uniswap.exchange>"}
# forward migrate
[[redirects]]
from = "https://migrate.uniswap.exchange/*"
to = "https://uniswap.exchange/migrate/v1"
status = 301
force = true
# forward v2 subdomain to apex
[[redirects]]
from = "https://v2.uniswap.exchange/*"
to = "https://uniswap.exchange/:splat"
status = 301
# support SPA setup
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

View File

@@ -1,7 +1,7 @@
{
"name": "@uniswap/interface",
"description": "Uniswap Interface",
"homepage": "https://uniswap.exchange",
"homepage": ".",
"private": true,
"devDependencies": {
"@ethersproject/address": "^5.0.0-beta.134",
@@ -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,8 +80,7 @@
},
"scripts": {
"start": "react-scripts start",
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",

View File

@@ -1,75 +0,0 @@
{
"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": "הכניסו כתובת טוקן כדי להמשיך"
}

View File

@@ -56,7 +56,7 @@
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
"totalSupplyIs0": "אין לך טוקנים של נזילות",
"tokenWorth": "שווי כל טוקן נזילות הינו",
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר",
"firstLiquidity": "אתה הראשוןה שמזרים נזילות למאגר",
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
"removeLiquidity": "הוצאה של נזילות",
"poolTokens": "טוקנים של מאגר הנזילות",

View File

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

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import { Send, Sun, Moon } from 'react-feather'
import { useDarkModeManager } from '../../state/user/hooks'
import { ButtonSecondary } from '../Button'
const FooterFrame = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
position: fixed;
right: 1rem;
bottom: 1rem;
${({ theme }) => theme.mediaWidth.upToExtraSmall`
display: none;
`};
`
export default function Footer() {
const [darkMode, toggleDarkMode] = useDarkModeManager()
return (
<FooterFrame>
<form action="https://forms.gle/DaLuqvJsVhVaAM3J9" target="_blank">
<ButtonSecondary p="8px 12px">
<Send size={16} style={{ marginRight: '8px' }} /> Feedback
</ButtonSecondary>
</form>
<ButtonSecondary onClick={toggleDarkMode} p="8px 12px" ml="0.5rem" width="min-content">
{darkMode ? <Sun size={16} /> : <Moon size={16} />}
</ButtonSecondary>
</FooterFrame>
)
}

View File

@@ -0,0 +1,70 @@
import { stringify } from 'qs'
import React, { useCallback, useMemo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import useToggledVersion, { Version } from '../../hooks/useToggledVersion'
const VersionLabel = styled.span<{ enabled: boolean }>`
padding: 0.35rem 0.6rem;
border-radius: 12px;
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
font-size: 1rem;
font-weight: ${({ theme, enabled }) => (enabled ? '500' : '400')};
:hover {
user-select: ${({ enabled }) => (enabled ? 'none' : 'initial')};
background: ${({ theme, enabled }) => (enabled ? theme.primary1 : 'none')};
color: ${({ theme, enabled }) => (enabled ? theme.white : theme.text1)};
}
`
interface VersionToggleProps extends React.ComponentProps<typeof Link> {
enabled: boolean
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const VersionToggle = styled(({ enabled, ...rest }: VersionToggleProps) => <Link {...rest} />)<VersionToggleProps>`
border-radius: 12px;
opacity: ${({ enabled }) => (enabled ? 1 : 0.5)};
cursor: ${({ enabled }) => (enabled ? 'pointer' : 'default')};
background: ${({ theme }) => theme.bg3};
color: ${({ theme }) => theme.primary1};
display: flex;
width: fit-content;
margin-left: 0.5rem;
text-decoration: none;
:hover {
text-decoration: none;
}
`
export default function VersionSwitch() {
const version = useToggledVersion()
const location = useLocation()
const query = useParsedQueryString()
const versionSwitchAvailable = location.pathname === '/swap' || location.pathname === '/send'
const toggleDest = useMemo(() => {
return versionSwitchAvailable
? {
...location,
search: `?${stringify({ ...query, use: version === Version.v1 ? undefined : Version.v1 })}`
}
: location
}, [location, query, version, versionSwitchAvailable])
const handleClick = useCallback(
e => {
if (!versionSwitchAvailable) e.preventDefault()
},
[versionSwitchAvailable]
)
return (
<VersionToggle enabled={versionSwitchAvailable} to={toggleDest} onClick={handleClick}>
<VersionLabel enabled={version === Version.v2 || !versionSwitchAvailable}>V2</VersionLabel>
<VersionLabel enabled={version === Version.v1 && versionSwitchAvailable}>V1</VersionLabel>
</VersionToggle>
)
}

View File

@@ -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`
@@ -47,6 +45,15 @@ const HeaderElement = styled.div`
align-items: center;
`
const HeaderElementWrap = styled.div`
display: flex;
align-items: center;
${({ theme }) => theme.mediaWidth.upToExtraSmall`
margin-top: 0.5rem;
`};
`
const Title = styled.div`
display: flex;
align-items: center;
@@ -72,6 +79,7 @@ const AccountElement = styled.div<{ active: boolean }>`
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
border-radius: 12px;
white-space: nowrap;
width: 100%;
:focus {
border: 1px solid blue;
@@ -82,10 +90,7 @@ const TestnetWrapper = styled.div`
white-space: nowrap;
width: fit-content;
margin-left: 10px;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
pointer-events: auto;
`
const NetworkCard = styled(YellowCard)`
@@ -122,33 +127,23 @@ const MigrateBanner = styled(AutoColumn)`
`};
`
const VersionLabel = styled.span<{ isV2?: boolean }>`
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
border-radius: 14px;
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
font-size: 0.825rem;
font-weight: 400;
:hover {
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
}
const HeaderControls = styled.div`
display: flex;
flex-direction: row;
align-items: flex-end;
${({ theme }) => theme.mediaWidth.upToSmall`
flex-direction: column;
`};
`
const VersionToggle = styled.a`
border-radius: 16px;
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 NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
[ChainId.MAINNET]: null,
[ChainId.RINKEBY]: 'Rinkeby',
[ChainId.ROPSTEN]: 'Ropsten',
[ChainId.GÖRLI]: 'Görli',
[ChainId.KOVAN]: 'Kovan'
}
export default function Header() {
const { account, chainId } = useActiveWeb3React()
@@ -169,7 +164,7 @@ export default function Header() {
</StyledInternalLink>
.
</MigrateBanner>
<RowBetween padding="1rem">
<RowBetween style={{ alignItems: 'flex-start' }} padding="1rem 1rem 0 1rem">
<HeaderElement>
<Title>
<UniIcon id="link" to="/">
@@ -187,34 +182,27 @@ export default function Header() {
</TitleText>
)}
</Title>
<TestnetWrapper style={{ pointerEvents: 'auto' }}>
{!isMobile && (
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
<VersionLabel isV2={true}>V2</VersionLabel>
<VersionLabel isV2={false}>V1</VersionLabel>
</VersionToggle>
)}
</TestnetWrapper>
</HeaderElement>
<HeaderElement>
<TestnetWrapper>
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
</TestnetWrapper>
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
{userEthBalance?.toSignificant(4)} ETH
</Text>
) : null}
<Web3Status />
</AccountElement>
<div style={{ pointerEvents: 'auto' }}>
<HeaderControls>
<HeaderElement>
<TestnetWrapper>
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
</TestnetWrapper>
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
{userEthBalance?.toSignificant(4)} ETH
</Text>
) : null}
<Web3Status />
</AccountElement>
</HeaderElement>
<HeaderElementWrap>
<VersionSwitch />
<Settings />
<Menu />
</div>
</HeaderElement>
</HeaderElementWrap>
</HeaderControls>
</RowBetween>
</HeaderFrame>
)

View File

@@ -77,9 +77,7 @@ const MenuItem = styled(ExternalLink)`
}
`
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
: 'https://github.com/Uniswap/uniswap-frontend'
const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
export default function Menu() {
const node = useRef<HTMLDivElement>()

View File

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

View File

@@ -2,36 +2,16 @@ import { Placement } from '@popperjs/core'
import { transparentize } from 'polished'
import React, { useState } from 'react'
import { usePopper } from 'react-popper'
import styled, { keyframes } from 'styled-components'
import styled from 'styled-components'
import useInterval from '../../hooks/useInterval'
import Portal from '@reach/portal'
const fadeIn = keyframes`
from {
opacity : 0;
}
to {
opacity : 1;
}
`
const fadeOut = keyframes`
from {
opacity : 1;
}
to {
opacity : 0;
}
`
const PopoverContainer = styled.div<{ show: boolean }>`
z-index: 9999;
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear;
transition: visibility 150ms linear;
visibility: ${props => (props.show ? 'visible' : 'hidden')};
opacity: ${props => (props.show ? 1 : 0)};
transition: visibility 150ms linear, opacity 150ms linear;
background: ${({ theme }) => theme.bg2};
border: 1px solid ${({ theme }) => theme.bg3};

View 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)

View File

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

View File

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

View 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)

View File

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

View 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>
)
}

View File

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

View File

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

View 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>
)
}

View File

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

View 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>
)
}

View File

@@ -6,9 +6,9 @@ import { WETH } from '@uniswap/sdk'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
const TOKEN_ICON_API = address =>
const getTokenLogoURL = address =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
const BAD_IMAGES = {}
const NO_LOGO_ADDRESSES: { [tokenAddress: string]: true } = {}
const Image = styled.img<{ size: string }>`
width: ${({ size }) => size};
@@ -44,20 +44,16 @@ export default function TokenLogo({
size?: string
style?: React.CSSProperties
}) {
const [error, setError] = useState(false)
const [, refresh] = useState<number>(0)
const { chainId } = useActiveWeb3React()
// mock rinkeby DAI
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
}
let path = ''
const validated = isAddress(address)
// hard code to show ETH instead of WETH in UI
if (address === WETH[chainId].address) {
if (validated === WETH[chainId].address) {
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
path = TOKEN_ICON_API(address)
} else if (!NO_LOGO_ADDRESSES[address] && validated) {
path = getTokenLogoURL(validated)
} else {
return (
<Emoji {...rest} size={size}>
@@ -75,8 +71,8 @@ export default function TokenLogo({
src={path}
size={size}
onError={() => {
BAD_IMAGES[address] = true
setError(true)
NO_LOGO_ADDRESSES[address] = true
refresh(i => i + 1)
}}
/>
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +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;
`
const StyledQRCode = styled(QRCode)`
border: 3px solid white;
`
interface WalletConnectDataProps {
uri?: string
size: number
}
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
return <QRCodeWrapper>{uri && <StyledQRCode size={size} value={uri} />}</QRCodeWrapper>
}

View File

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

View File

@@ -1,19 +1,16 @@
import { Trade, TradeType } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { ChevronUp, ChevronRight } from 'react-feather'
import { Text, Flex } from 'rebass'
import { ThemeContext } from 'styled-components'
import { Field } from '../../state/swap/actions'
import { CursorPointer, TYPE } from '../../theme'
import { useUserSlippageTolerance } from '../../state/user/hooks'
import { TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { SectionBreak } from './styleds'
import QuestionHelper from '../QuestionHelper'
import { RowBetween, RowFixed } from '../Row'
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
import FormattedPriceImpact from './FormattedPriceImpact'
import TokenLogo from '../TokenLogo'
import flatMap from 'lodash.flatmap'
import { SectionBreak } from './styleds'
import SwapRoute from './SwapRoute'
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
const theme = useContext(ThemeContext)
@@ -61,79 +58,37 @@ function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippag
</TYPE.black>
</RowBetween>
</AutoColumn>
<SectionBreak />
</>
)
}
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
export interface AdvancedSwapDetailsProps {
trade?: Trade
onDismiss: () => void
}
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext)
const [allowedSlippage] = useUserSlippageTolerance()
const showRoute = trade?.route?.path?.length > 2
return (
<AutoColumn gap="md">
<CursorPointer>
<RowBetween onClick={onDismiss} padding={'8px 20px'}>
<Text fontSize={16} color={theme.text2} fontWeight={500} style={{ userSelect: 'none' }}>
Hide Advanced
</Text>
<ChevronUp color={theme.text2} />
</RowBetween>
</CursorPointer>
<SectionBreak />
{trade && <TradeSummary trade={trade} allowedSlippage={slippageTabProps.rawSlippage} />}
<SlippageTabs {...slippageTabProps} />
{trade?.route?.path?.length > 2 && (
<AutoColumn style={{ padding: '0 20px' }}>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Route
</TYPE.black>
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
</RowFixed>
<Flex
px="1rem"
py="0.5rem"
my="0.5rem"
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
flexWrap="wrap"
width="100%"
justifyContent="space-evenly"
alignItems="center"
>
{flatMap(
trade.route.path,
// add a null in-between each item
(token, i, array) => {
const lastItem = i === array.length - 1
return lastItem ? [token] : [token, null]
}
).map((token, i) => {
// use null as an indicator to insert chevrons
if (token === null) {
return <ChevronRight key={i} color={theme.text2} />
} else {
return (
<Flex my="0.5rem" alignItems="center" key={token.address} style={{ flexShrink: 0 }}>
<TokenLogo address={token.address} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>
</Flex>
)
}
})}
</Flex>
</AutoColumn>
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
{showRoute && (
<>
<SectionBreak />
<AutoColumn style={{ padding: '0 24px' }}>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Route
</TYPE.black>
<QuestionHelper text="Routing through these tokens resulted in the best price for your trade." />
</RowFixed>
<SwapRoute trade={trade} />
</AutoColumn>
</>
)}
</AutoColumn>
)

View File

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

View 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>
)
}

View File

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

View File

@@ -0,0 +1,38 @@
import { Trade } from '@uniswap/sdk'
import React, { Fragment, memo, useContext } from 'react'
import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components'
import { TYPE } from '../../theme'
import TokenLogo from '../TokenLogo'
export default memo(function SwapRoute({ trade }: { trade: Trade }) {
const theme = useContext(ThemeContext)
return (
<Flex
px="1rem"
py="0.5rem"
my="0.5rem"
style={{ border: `1px solid ${theme.bg3}`, borderRadius: '1rem' }}
flexWrap="wrap"
width="100%"
justifyContent="space-evenly"
alignItems="center"
>
{trade.route.path.map((token, i, path) => {
const isLastItem: boolean = i === path.length - 1
return (
<Fragment key={i}>
<Flex my="0.5rem" alignItems="center" style={{ flexShrink: 0 }}>
<TokenLogo address={token.address} size="1.5rem" />
<TYPE.black fontSize={14} color={theme.text1} ml="0.5rem">
{token.symbol}
</TYPE.black>
</Flex>
{isLastItem ? null : <ChevronRight color={theme.text2} />}
</Fragment>
)
})}
</Flex>
)
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,40 @@
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
import { COMP, DAI, MKR, USDC, USDT } from './tokens/mainnet'
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
// used to construct intermediary pairs for trading
export const BASES_TO_CHECK_TRADES_AGAINST: { readonly [chainId in ChainId]: Token[] } = {
[ChainId.MAINNET]: [
WETH[ChainId.MAINNET],
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
],
// a list of tokens by chain
type ChainTokenList = {
readonly [chainId in ChainId]: Token[]
}
const WETH_ONLY: ChainTokenList = {
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
}
// used to construct intermediary pairs for trading
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
...WETH_ONLY,
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR]
}
// used for display in the default list when adding liquidity
export const SUGGESTED_BASES = BASES_TO_CHECK_TRADES_AGAINST
export const SUGGESTED_BASES: ChainTokenList = {
...WETH_ONLY,
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
}
// used to construct the list of all pairs we consider by default in the frontend
export const BASES_TO_TRACK_LIQUIDITY_FOR = BASES_TO_CHECK_TRADES_AGAINST
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
...WETH_ONLY,
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT]
}
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
[ChainId.MAINNET]: [
@@ -58,7 +71,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 +93,9 @@ const MAINNET_WALLETS = {
export const SUPPORTED_WALLETS =
process.env.REACT_APP_CHAIN_ID !== '1'
? MAINNET_WALLETS
? TESTNET_CAPABLE_WALLETS
: {
...MAINNET_WALLETS,
...TESTNET_CAPABLE_WALLETS,
...{
WALLET_CONNECT: {
connector: walletconnect,
@@ -90,7 +103,8 @@ export const SUPPORTED_WALLETS =
iconName: 'walletConnectIcon.svg',
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
href: null,
color: '#4196FC'
color: '#4196FC',
mobile: true
},
WALLET_LINK: {
connector: walletlink,
@@ -109,15 +123,6 @@ export const SUPPORTED_WALLETS =
mobile: true,
mobileOnly: true
},
TRUST_WALLET_LINK: {
name: 'Open in Trust Wallet',
iconName: 'trustWallet.png',
description: 'iOS and Android app.',
href: 'https://link.trustwallet.com/open_url?coin_id=60&url=https://uniswap.exchange/swap',
color: '#1C74CC',
mobile: true,
mobileOnly: true
},
FORTMATIC: {
connector: fortmatic,
name: 'Fortmatic',
@@ -151,12 +156,13 @@ export const ONE_BIPS = new Percent(JSBI.BigInt(1), JSBI.BigInt(10000))
export const BIPS_BASE = JSBI.BigInt(10000)
// used for warning states
export const ALLOWED_PRICE_IMPACT_LOW: Percent = new Percent(JSBI.BigInt(100), BIPS_BASE) // 1%
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
export const ALLOWED_PRICE_IMPACT_MEDIUM: Percent = new Percent(JSBI.BigInt(300), BIPS_BASE) // 3%
export const ALLOWED_PRICE_IMPACT_HIGH: Percent = new Percent(JSBI.BigInt(500), BIPS_BASE) // 5%
// if the price slippage exceeds this number, force the user to type 'confirm' to execute
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(2500), BIPS_BASE) // 25%
export const PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN: Percent = new Percent(JSBI.BigInt(1000), BIPS_BASE) // 10%
// for non expert mode disable swaps above this
export const BLOCKED_PRICE_IMPACT_NON_EXPERT: Percent = new Percent(JSBI.BigInt(1500), BIPS_BASE) // 15%
// used to ensure the user doesn't send so much ETH so they end up with <.01
export const MIN_ETH: JSBI = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(16)) // .01 ETH
export const V1_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))
export const BETTER_TRADE_LINK_THRESHOLD = new Percent(JSBI.BigInt(75), JSBI.BigInt(10000))

View File

@@ -1,5 +1,11 @@
import { Token, ChainId } from '@uniswap/sdk'
export const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD')
export const COMP = new Token(ChainId.MAINNET, '0xc00e94Cb662C3520282E6f5717214004A7f26888', 18, 'COMP', 'Compound')
export const MKR = new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker')
export default [
new Token(ChainId.MAINNET, '0xB6eD7644C69416d67B522e20bC294A9a9B405B31', 8, '0xBTC', '0xBitcoin Token'),
new Token(ChainId.MAINNET, '0xfC1E690f61EFd961294b3e1Ce3313fBD8aa4f85d', 18, 'aDAI', 'Aave Interest bearing DAI'),
@@ -10,6 +16,7 @@ export default [
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
new Token(ChainId.MAINNET, '0xba100000625a3754423978a60c9317c58a424e3D', 18, 'BAL', 'Balancer'),
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
new Token(ChainId.MAINNET, '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 18, 'BNT', 'Bancor Network Token'),
new Token(ChainId.MAINNET, '0x0327112423F3A68efdF1fcF402F6c5CB9f7C33fd', 18, 'BTC++', 'PieDAO BTC++'),
@@ -19,8 +26,9 @@ export default [
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
COMP,
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
DAI,
new Token(ChainId.MAINNET, '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', 18, 'DATA', 'Streamr DATAcoin'),
new Token(ChainId.MAINNET, '0xE0B7927c4aF23765Cb51314A0E0521A9645F0E2A', 9, 'DGD', 'DigixDAO'),
new Token(ChainId.MAINNET, '0x4f3AfEC4E5a3F2A6a1A411DEF7D7dFe50eE057bF', 9, 'DGX', 'Digix Gold Token'),
@@ -61,9 +69,10 @@ export default [
new Token(ChainId.MAINNET, '0xd15eCDCF5Ea68e3995b2D0527A0aE0a3258302F8', 18, 'MCX', 'MachiX Token'),
new Token(ChainId.MAINNET, '0xa3d58c4E56fedCae3a7c43A725aeE9A71F0ece4e', 18, 'MET', 'Metronome'),
new Token(ChainId.MAINNET, '0x80f222a749a2e18Eb7f676D371F19ad7EFEEe3b7', 18, 'MGN', 'Magnolia Token'),
new Token(ChainId.MAINNET, '0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2', 18, 'MKR', 'Maker'),
MKR,
new Token(ChainId.MAINNET, '0xec67005c4E498Ec7f55E092bd1d35cbC47C91892', 18, 'MLN', 'Melon Token'),
new Token(ChainId.MAINNET, '0x957c30aB0426e0C93CD8241E2c60392d08c6aC8e', 0, 'MOD', 'Modum Token'),
new Token(ChainId.MAINNET, '0xe2f2a5C287993345a840Db3B0845fbC70f5935a5', 18, 'mUSD', 'mStable USD'),
new Token(ChainId.MAINNET, '0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206', 18, 'NEXO', 'Nexo'),
new Token(ChainId.MAINNET, '0x1776e1F26f98b1A5dF9cD347953a26dd3Cb46671', 18, 'NMR', 'Numeraire'),
new Token(ChainId.MAINNET, '0x985dd3D42De1e256d09e1c10F112bCCB8015AD41', 18, 'OCEAN', 'OceanToken'),
@@ -94,6 +103,7 @@ export default [
new Token(ChainId.MAINNET, '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', 18, 'SNX', 'Synthetix Network Token'),
new Token(ChainId.MAINNET, '0x23B608675a2B2fB1890d3ABBd85c5775c51691d5', 18, 'SOCKS', 'Unisocks Edition 0'),
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
new Token(ChainId.MAINNET, '0x0Ae055097C6d159879521C384F1D2123D1f195e6', 18, 'STAKE', 'STAKE'),
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
@@ -109,9 +119,9 @@ export default [
new Token(ChainId.MAINNET, '0x0000000000085d4780B73119b644AE5ecd22b376', 18, 'TUSD', 'TrueUSD'),
new Token(ChainId.MAINNET, '0x8400D94A5cb0fa0D041a3788e395285d61c9ee5e', 8, 'UBT', 'UniBright'),
new Token(ChainId.MAINNET, '0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828', 18, 'UMA', 'UMA Voting Token v1'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
USDC,
new Token(ChainId.MAINNET, '0xA4Bdb11dc0a2bEC88d24A3aa1E6Bb17201112eBe', 6, 'USDS', 'StableUSD'),
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
USDT,
new Token(ChainId.MAINNET, '0xeb269732ab75A6fD61Ea60b06fE994cD32a83549', 18, 'USDx', 'dForce'),
new Token(ChainId.MAINNET, '0x8f3470A7388c05eE4e7AF3d01D8C722b0FF52374', 18, 'VERI', 'Veritaseum'),
new Token(ChainId.MAINNET, '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 8, 'WBTC', 'Wrapped BTC'),

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
import { useMemo } from 'react'
import { Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
import { Pair, Token, TokenAmount, Trade } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useActiveWeb3React } from './index'
import { usePairs } from '../data/Reserves'
import { useMemo } from 'react'
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
import { usePairs } from '../data/Reserves'
import { useActiveWeb3React } from './index'
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
const { chainId } = useActiveWeb3React()
const bases = useMemo(() => BASES_TO_CHECK_TRADES_AGAINST[chainId as ChainId] ?? [], [chainId])
const bases: Token[] = chainId ? BASES_TO_CHECK_TRADES_AGAINST[chainId] : []
const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
() => [
@@ -31,13 +31,16 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
// only pass along valid pairs, non-duplicated pairs
return useMemo(
() =>
allPairs
// filter out invalid pairs
.filter((p): p is Pair => !!p)
// filter out duplicated pairs
.filter(
(p, i, pairs) => i === pairs.findIndex(pair => pair?.liquidityToken.address === p.liquidityToken.address)
),
Object.values(
allPairs
// filter out invalid pairs
.filter((p): p is Pair => !!p)
// filter out duplicated pairs
.reduce<{ [pairAddress: string]: Pair }>((memo, curr) => {
memo[curr.liquidityToken.address] = memo[curr.liquidityToken.address] ?? curr
return memo
}, {})
),
[allPairs]
)
}
@@ -46,14 +49,11 @@ function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
* Returns the best trade for the exact amount of tokens in to the given token out
*/
export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade | null {
const inputToken = amountIn?.token
const outputToken = tokenOut
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
const allowedPairs = useAllCommonPairs(amountIn?.token, tokenOut)
return useMemo(() => {
if (amountIn && tokenOut && allowedPairs.length > 0) {
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut)[0] ?? null
return Trade.bestTradeExactIn(allowedPairs, amountIn, tokenOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
}
return null
}, [allowedPairs, amountIn, tokenOut])
@@ -63,14 +63,11 @@ export function useTradeExactIn(amountIn?: TokenAmount, tokenOut?: Token): Trade
* Returns the best trade for the token in to the exact amount of token out
*/
export function useTradeExactOut(tokenIn?: Token, amountOut?: TokenAmount): Trade | null {
const inputToken = tokenIn
const outputToken = amountOut?.token
const allowedPairs = useAllCommonPairs(inputToken, outputToken)
const allowedPairs = useAllCommonPairs(tokenIn, amountOut?.token)
return useMemo(() => {
if (tokenIn && amountOut && allowedPairs.length > 0) {
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut)[0] ?? null
return Trade.bestTradeExactOut(allowedPairs, tokenIn, amountOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
}
return null
}, [allowedPairs, tokenIn, amountOut])

View File

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

View File

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

View 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]
)
}

View File

@@ -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,11 +167,63 @@ 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: (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)
@@ -198,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
})
@@ -240,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
])
}

View 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
}

View File

@@ -3,15 +3,13 @@ import { initReactI18next } from 'react-i18next'
import XHR from 'i18next-xhr-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
const LOAD_PATH: string = process.env.PUBLIC_URL === '.' ? `./locales/{{lng}}.json` : '/locales/{{lng}}.json'
i18next
.use(XHR)
.use(LanguageDetector)
.use(initReactI18next)
.init({
backend: {
loadPath: LOAD_PATH
loadPath: `./locales/{{lng}}.json`
},
react: {
useSuspense: true

View File

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

View File

@@ -1,8 +1,7 @@
import React, { Suspense } from 'react'
import { BrowserRouter, HashRouter, Route, Switch } from 'react-router-dom'
import { HashRouter, Route, Switch } from 'react-router-dom'
import styled from 'styled-components'
import GoogleAnalyticsReporter from '../components/analytics/GoogleAnalyticsReporter'
import Footer from '../components/Footer'
import Header from '../components/Header'
import Popups from '../components/Popups'
import Web3ReactManager from '../components/Web3ReactManager'
@@ -11,6 +10,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,40 +49,14 @@ 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;
`
let Router: React.ComponentType
if (process.env.PUBLIC_URL === '.') {
Router = HashRouter
} else {
Router = BrowserRouter
}
export default function App() {
return (
<Suspense fallback={null}>
<Router>
<HashRouter>
<Route component={GoogleAnalyticsReporter} />
<Route component={DarkModeQueryParamReader} />
<AppWrapper>
@@ -103,15 +77,14 @@ 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>
</HashRouter>
</Suspense>
)
}

View File

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

View File

@@ -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..." />

View 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>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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} />
</>
)
}

View File

@@ -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} />
</>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
})
})
})
})

View File

@@ -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])
}

View File

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

View File

@@ -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 } }) => {

View File

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

View File

@@ -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]
)
}

View File

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

View File

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

View File

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

View 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)
})
})
})

View File

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

View File

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

View File

@@ -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%)`};
}
`

View File

@@ -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
View 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)))
})
})
})

View File

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

292
yarn.lock
View File

@@ -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,58 +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.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/client/-/client-1.0.11.tgz#ee58d2662e433cb67c5d2157c1b4525f864ab6ec"
integrity sha512-NVMDRUuLMqRPmzR7xjVfRcuXegvrCTtzuVyU8iYEnriZ3fFZcFj3PWVzN44RvLYQ4yUxWzeUkXIVRmmecOLMbQ==
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.11"
"@walletconnect/iso-crypto" "^1.0.11"
"@walletconnect/types" "^1.0.11"
"@walletconnect/utils" "^1.0.11"
"@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.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-1.0.11.tgz#486eaf680fb697d9f35b02fc91392eb8c4a60116"
integrity sha512-hrr7oFgQrQaNbCKlh+4lXVz9pLjt1RVMEyftA5Q+hWNdgrBV0NDvrp2SV7XaHBg/z/D37JA6we+zGPkkBZ8CRA==
dependencies:
"@walletconnect/types" "^1.0.0-beta.47"
"@walletconnect/utils" "^1.0.0-beta.47"
"@walletconnect/socket-transport" "^1.0.11"
"@walletconnect/types" "^1.0.11"
"@walletconnect/utils" "^1.0.11"
"@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.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/http-connection/-/http-connection-1.0.11.tgz#3c00ab02b4e6f4ffa1aa346b19569e585609dfa2"
integrity sha512-kT9tKfp0KfKO+WkufSEi2Ppcgni2LB1Qly66uV3xZEwqouY+8Fs7Rf/BQ9o8KmosnP9WxBjgO+S4OMDWNLHCdA==
dependencies:
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==
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/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==
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"
"@walletconnect/types" "^1.0.11"
"@walletconnect/utils" "^1.0.11"
xhr2-cookies "1.1.0"
"@walletconnect/iso-crypto@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/iso-crypto/-/iso-crypto-1.0.11.tgz#cb989e6257d4e8595f3bf15950ee82ec67727c11"
integrity sha512-yYww/lrbseTD+ZphQzkxUx4Ufyx4fotTv/XK62p4Qb6SYFDR2/1bXTsbN2KitfeF0rpomyF0ouWujOF671p23w==
dependencies:
"@walletconnect/types" "^1.0.11"
"@walletconnect/utils" "^1.0.11"
eccrypto-js "5.2.0"
"@walletconnect/mobile-registry@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/mobile-registry/-/mobile-registry-1.0.11.tgz#55a060fb113524e75ed675fce1ab50bb6c5aa1ce"
integrity sha512-E78BfSr4RNSUPl/4Qpfg4bPO+QynMqUj55X20S41z1aGIYhXNM33sUVWGkbxO5rHuHYLB9Z5O/ob0sENKCXAfA==
"@walletconnect/qrcode-modal@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/qrcode-modal/-/qrcode-modal-1.0.11.tgz#5b34d583c034aed74307350797c66ddce6193744"
integrity sha512-GsSQ/E3ixBEiQz3EOFypW2FCFIS6G37crpJunkLhefi9w2/CMeQ5bk4SIFKyGsAv6uEtwAcPVh7tNkoiGEsb2A==
dependencies:
"@walletconnect/mobile-registry" "^1.0.11"
"@walletconnect/types" "^1.0.11"
"@walletconnect/utils" "^1.0.11"
preact "10.4.1"
qrcode "1.4.4"
"@walletconnect/socket-transport@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/socket-transport/-/socket-transport-1.0.11.tgz#de1f473f37d7b6af813338cd17a4eb0f46553d19"
integrity sha512-96Xy8GHoO8nHxmGfUcLflkv2KtRNwkAkWay8uRAHLGpYQJ5kaKCvHfaSraNPvwKBwQydbWGn50n5aIFiR/lShg==
dependencies:
"@walletconnect/types" "^1.0.11"
ws "7.3.0"
"@walletconnect/types@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-1.0.11.tgz#6dd23eb3a8dd2824f76cc2c54217ecca8229d0a2"
integrity sha512-ysIQI6DsMELQAAt5zk2ZMKKyOwgk+XP4KeOhmDk+sIQskBugFl0ARl5iQZzGz9pcrHdlg1Fi7ucGw3UaExqIVA==
"@walletconnect/utils@^1.0.11":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-1.0.11.tgz#a6c8bb8b9cf9600684d5825d4b54e1d85ff61230"
integrity sha512-Hgcjq/YYmzrNenpNhftD+I2MqT/f73qwjPYWfubQs2zPN4Hd/xb4cC2fKqIMeuVmoee9MJaLhZGnr+dxcDaH4w==
dependencies:
"@walletconnect/types" "^1.0.11"
detect-browser "5.1.0"
enc-utils "2.1.0"
js-sha3 "0.8.0"
"@walletconnect/web3-provider@^1.0.8":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@walletconnect/web3-provider/-/web3-provider-1.0.11.tgz#462d84808379f5ec8747e46de217286d61837620"
integrity sha512-ppzYwfrVtl5j8zMOPl07v5w+Gb0ptspQm3TGUqVVClaIdXt96uCBBPxwi5bZa4pSXVKgJvI9EbKzaqUS8kVsyQ==
dependencies:
"@walletconnect/client" "^1.0.11"
"@walletconnect/http-connection" "^1.0.11"
"@walletconnect/qrcode-modal" "^1.0.11"
"@walletconnect/types" "^1.0.11"
"@walletconnect/utils" "^1.0.11"
web3-provider-engine "15.0.7"
"@web3-react/abstract-connector@^6.0.7":
version "6.0.7"
resolved "https://registry.yarnpkg.com/@web3-react/abstract-connector/-/abstract-connector-6.0.7.tgz#401b3c045f1e0fab04256311be49d5144e9badc6"
@@ -2925,12 +2954,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 +3215,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 +4340,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 +4379,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==
@@ -4569,12 +4598,30 @@ btoa@^1.2.1:
resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
buffer-alloc@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
dependencies:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@^1.0.0:
buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
buffer-from@^1.0.0, buffer-from@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
@@ -4606,6 +4653,14 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
buffer@^5.4.3:
version "5.6.0"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
@@ -5937,6 +5992,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"
@@ -5981,6 +6041,11 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
dijkstrajs@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
dir-glob@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
@@ -6160,6 +6225,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 +6285,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 +6686,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 +7992,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 +8801,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=
@@ -8752,6 +8838,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -9235,6 +9326,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"
@@ -11143,6 +11239,11 @@ pn@^1.1.0:
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
pngjs@^3.3.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
pnp-webpack-plugin@1.6.4:
version "1.6.4"
resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
@@ -11839,7 +11940,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==
@@ -12053,21 +12154,11 @@ q@^1.1.2:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qr-image@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/qr-image/-/qr-image-3.2.0.tgz#9fa8295beae50c4a149cf9f909a1db464a8672e8"
integrity sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug=
qr.js@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=
qrcode-terminal@0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819"
integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==
qrcode.react@^0.9.3:
version "0.9.3"
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-0.9.3.tgz#91de1287912bdc5ccfb3b091737b828d6ced60c5"
@@ -12076,6 +12167,19 @@ qrcode.react@^0.9.3:
prop-types "^15.6.0"
qr.js "0.0.0"
qrcode@1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
dependencies:
buffer "^5.4.3"
buffer-alloc "^1.2.0"
buffer-from "^1.1.1"
dijkstrajs "^1.0.1"
isarray "^2.0.1"
pngjs "^3.3.0"
yargs "^13.2.4"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -12126,7 +12230,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 +13250,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 +14479,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 +14832,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"
@@ -15144,6 +15255,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"
@@ -15265,7 +15381,7 @@ yargs@12.0.5:
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"
yargs@^13.3.0:
yargs@^13.2.4, yargs@^13.3.0:
version "13.3.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==