Compare commits

...

12 Commits

Author SHA1 Message Date
Noah Zinsmeister
28c916ff45 Remove liquidity callback (#837)
* give add liquidity the reducer treatment

rename setDefaultsFromURL to setDefaultsFromURLSearch

* fix tests and crash

* rework DOM structure to make flow more natural

* allow slippage + deadline setting in add liquidity

* migrate burn

* disable token selection in mint

clear input between pairs

* reset fields between pairs

* tweak helper text

* address review comments
2020-05-27 13:13:31 -04:00
Moody Salem
7adb4b6bd6 chore(release): fix changelog commit hashes 2020-05-27 12:29:49 -04:00
Noah Zinsmeister
b2f0236ee8 Add liquidity callback (#830)
* give add liquidity the reducer treatment

rename setDefaultsFromURL to setDefaultsFromURLSearch

* fix tests and crash

* rework DOM structure to make flow more natural

* allow slippage + deadline setting in add liquidity

* disable token selection in mint

clear input between pairs
2020-05-27 11:42:25 -04:00
Moody Salem
4b57059353 fix(release): fix the dns record update 2020-05-27 09:58:28 -04:00
Moody Salem
6926f9a4ae fix(flatMap): don't use the native flatMap function, bump release plugin version 2020-05-27 09:29:53 -04:00
Moody Salem
7dec580944 chore(readme): prepare for more ipfs, use the latest version of github tag action 2020-05-27 09:14:12 -04:00
Moody Salem
5cf95680ef chore(release): fix cf-ipfs url 2020-05-27 09:01:20 -04:00
Moody Salem
f8d6bab4ae chore(release): limit release to release.yaml changes on v2 branch 2020-05-27 09:00:57 -04:00
Moody Salem
c9721c42bf perf(reduce call volume): save a bunch of calls to infura when the tab is not focused, change infura IDs for v2 2020-05-26 13:36:09 -04:00
Moody Salem
4414134bb2 chore(release): Fix release links 2020-05-26 13:17:53 -04:00
Moody Salem
44ba54e44a chore(release): add cf-ipfs.com 2020-05-26 13:03:50 -04:00
Moody Salem
9ec3109f72 chore(release): release text, convert cidv0 2020-05-26 13:01:38 -04:00
40 changed files with 1557 additions and 1623 deletions

2
.env
View File

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

View File

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

View File

@@ -6,6 +6,8 @@ on:
# releases are triggered on changes to this file
push:
branches:
- v2
paths:
- '.github/workflows/release.yaml'
@@ -22,7 +24,7 @@ jobs:
- name: Bump version and push tag
id: github_tag_action
uses: mathieudutour/github-tag-action@v4
uses: mathieudutour/github-tag-action@v4.5
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
release_branches: .*
@@ -55,13 +57,19 @@ jobs:
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
- name: Convert CIDv0 to CIDv1
id: convert_cidv0
uses: uniswap/convert-cidv0-cidv1@v1.0.0
with:
cidv0: ${{ steps.upload.outputs.hash }}
- name: Update DNS with new IPFS hash
uses: uniswap/replace-vercel-dns-records@v1.0.0
with:
domain: 'uniswap.org'
subdomain: '_dnslink.app'
record-type: 'TXT'
value: /ipfs/${{ steps.upload.outputs.hash }}
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
token: ${{ secrets.VERCEL_TOKEN }}
team-name: 'uniswap'
@@ -74,17 +82,20 @@ jobs:
tag_name: ${{ needs.bump_version.outputs.new_tag }}
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
body: |
Release built from commit
[`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
The IPFS hash of the bundle is `${{ steps.upload.outputs.hash }}`
The IPFS hash of the bundle is:
- CIDv0: `${{ steps.upload.outputs.hash }}`
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
Preferred URLs:
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.cf-ipfs.com/
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
- https://dweb.link/ipfs/${{ steps.upload.outputs.hash }}/
Other IPFS gateways:
- https://cloudflare-ipfs.com/ipfs/${{ steps.upload.outputs.hash }}/

View File

@@ -1,9 +1,7 @@
# Uniswap Frontend
[![Netlify Status](https://api.netlify.com/api/v1/badges/fa110555-b3c7-4eeb-b840-88a835009c62/deploy-status)](https://app.netlify.com/sites/uniswap/deploys)
[![Tests](https://github.com/Uniswap/uniswap-frontend/workflows/Tests/badge.svg)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
[![Styled With Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io/)
[![Release](https://github.com/Uniswap/uniswap-frontend/workflows/Release/badge.svg)](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ARelease)
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
@@ -17,10 +15,9 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
## Accessing the frontend
The front end is deployed to IPFS as well as to [uniswap.exchange](https://uniswap.exchange).
To access the front end via IPFS, use a link from the
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest).
To access the front end, use an IPFS gateway link from the
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
or visit [uniswap.exchange](https://uniswap.exchange).
## Development

View File

@@ -1,19 +1,19 @@
describe('Add Liquidity', () => {
it('loads the two correct tokens', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'MKR')
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('contain.text', 'ETH')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
})
it('does not crash if ETH is duplicated', () => {
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'ETH')
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('not.contain.text', 'ETH')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
})
it('token not in storage is loaded', () => {
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#add-liquidity-input-token0 .token-symbol-container').should('contain.text', 'SKL')
cy.get('#add-liquidity-input-token1 .token-symbol-container').should('contain.text', 'MKR')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
})
})

View File

@@ -1,19 +1,19 @@
describe('Remove Liquidity', () => {
it('loads the two correct tokens', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-token1-symbol').should('contain.text', 'MKR')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
it('does not crash if ETH is duplicated', () => {
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-token1-symbol').should('not.contain.text', 'ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH')
})
it('token not in storage is loaded', () => {
cy.visit('/remove/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#remove-liquidity-token0-symbol').should('contain.text', 'SKL')
cy.get('#remove-liquidity-token1-symbol').should('contain.text', 'MKR')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'SKL')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
})
})

View File

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

View File

@@ -20,6 +20,7 @@
"@reach/portal": "^0.10.3",
"@reduxjs/toolkit": "^1.3.5",
"@types/jest": "^25.2.1",
"@types/lodash.flatmap": "^4.5.6",
"@types/node": "^13.13.5",
"@types/qs": "^6.9.2",
"@types/react": "^16.9.34",
@@ -55,6 +56,7 @@
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
"jazzicon": "^1.5.0",
"lodash.flatmap": "^4.5.0",
"polished": "^3.3.2",
"prettier": "^1.17.0",
"qrcode.react": "^0.9.3",

View File

@@ -110,8 +110,8 @@ function NavigationTabs({ location: { pathname }, history }: RouteComponentProps
<QuestionHelper
text={
adding
? 'When you add liquidity, you are given pool tokens that represent your position in this pool. These tokens automatically earn fees proportional to your pool share and can be redeemed at any time.'
: 'Your liquidity is represented by a pool token (ERC20). Removing will convert your position back into tokens at the current rate and proportional to the amount of each token in the pool. Any fees you accrued are included in the token amounts you receive.'
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
}
/>
</RowBetween>

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react'
import React from 'react'
import Slider from '@material-ui/core/Slider'
import { withStyles } from '@material-ui/core/styles'
import { useDebounce } from '../../hooks'
const StyledSlider = withStyles({
root: {
@@ -51,36 +50,12 @@ const StyledSlider = withStyles({
interface InputSliderProps {
value: number
onChange: (val: number) => void
override?: boolean
onChange: (value: number) => void
}
export default function InputSlider({ value, onChange, override }: InputSliderProps) {
const [internalVal, setInternalVal] = useState<number>(value)
const debouncedInternalValue = useDebounce(internalVal, 100)
const handleChange = useCallback(
(e, val) => {
setInternalVal(val)
if (val !== debouncedInternalValue) {
onChange(val)
}
},
[setInternalVal, onChange, debouncedInternalValue]
)
useEffect(() => {
if (override) {
setInternalVal(value)
}
}, [override, value])
return (
<StyledSlider
value={typeof internalVal === 'number' ? internalVal : 0}
onChange={handleChange}
aria-labelledby="input-slider"
step={1}
/>
)
export default function InputSlider({ value, onChange }: InputSliderProps) {
function wrappedOnChange(_, value) {
onChange(value)
}
return <StyledSlider value={value} onChange={wrappedOnChange} aria-labelledby="input-slider" step={1} />
}

View File

@@ -1,22 +1,22 @@
import React, { useState, useEffect, useRef, useCallback, useContext } from 'react'
import React, { useState, useRef, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import QuestionHelper from '../QuestionHelper'
import { Text } from 'rebass'
import { TYPE } from '../../theme'
import { AutoColumn } from '../Column'
import { RowBetween, RowFixed } from '../Row'
import { darken } from 'polished'
import { useDebounce } from '../../hooks'
const WARNING_TYPE = Object.freeze({
none: 'none',
emptyInput: 'emptyInput',
invalidEntryBound: 'invalidEntryBound',
riskyEntryHigh: 'riskyEntryHigh',
riskyEntryLow: 'riskyEntryLow'
})
enum SlippageError {
InvalidInput = 'InvalidInput',
RiskyLow = 'RiskyLow',
RiskyHigh = 'RiskyHigh'
}
enum DeadlineError {
InvalidInput = 'InvalidInput'
}
const FancyButton = styled.button`
color: ${({ theme }) => theme.text1};
@@ -46,7 +46,7 @@ const Option = styled(FancyButton)<{ active: boolean }>`
color: ${({ active, theme }) => (active ? theme.white : theme.text1)};
`
const Input = styled.input<{ active?: boolean }>`
const Input = styled.input`
background: ${({ theme }) => theme.bg1};
flex-grow: 1;
font-size: 12px;
@@ -56,15 +56,8 @@ const Input = styled.input<{ active?: boolean }>`
&::-webkit-inner-spin-button {
-webkit-appearance: none;
}
color: ${({ active, theme, color }) => (color === 'red' ? theme.red1 : active ? 'initial' : theme.text1)};
cursor: ${({ active }) => (active ? 'initial' : 'inherit')};
text-align: ${({ active }) => (active ? 'right' : 'left')};
`
const BottomError = styled(Text)<{ show?: boolean }>`
font-size: 14px;
font-weight: 400;
padding-top: ${({ show }) => (show ? '12px' : '')};
color: ${({ theme, color }) => (color === 'red' ? theme.red1 : theme.text1)};
text-align: right;
`
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>`
@@ -89,12 +82,6 @@ const SlippageSelector = styled.div`
padding: 0 20px;
`
const Percent = styled.div`
color: ${({ color, theme }) => (color === 'faded' ? theme.bg1 : color === 'red' ? theme.red1 : 'inherit')};
font-size: 0, 8rem;
flex-grow: 0;
`
export interface SlippageTabsProps {
rawSlippage: number
setRawSlippage: (rawSlippage: number) => void
@@ -102,247 +89,154 @@ export interface SlippageTabsProps {
setDeadline: (deadline: number) => void
}
export default function SlippageTabs({ setRawSlippage, rawSlippage, deadline, setDeadline }: SlippageTabsProps) {
export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, setDeadline }: SlippageTabsProps) {
const theme = useContext(ThemeContext)
const [activeIndex, setActiveIndex] = useState(2)
const [warningType, setWarningType] = useState(WARNING_TYPE.none)
const inputRef = useRef<HTMLInputElement>()
const [userInput, setUserInput] = useState('')
const debouncedInput = useDebounce(userInput, 150)
const [slippageInput, setSlippageInput] = useState('')
const [deadlineInput, setDeadlineInput] = useState('')
const [initialSlippage] = useState(rawSlippage)
const slippageInputIsValid =
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
const [deadlineInput, setDeadlineInput] = useState(deadline / 60)
const updateSlippage = useCallback(
newSlippage => {
// round to 2 decimals to prevent ethers error
const numParsed = newSlippage * 100
// set both slippage values in parents
setRawSlippage(numParsed)
},
[setRawSlippage]
)
const checkBounds = useCallback(
slippageValue => {
setWarningType(WARNING_TYPE.none)
if (slippageValue === '' || slippageValue === '.') {
return setWarningType(WARNING_TYPE.emptyInput)
}
// check bounds and set errors
if (Number(slippageValue) < 0 || Number(slippageValue) > 50) {
return setWarningType(WARNING_TYPE.invalidEntryBound)
}
if (Number(slippageValue) >= 0 && Number(slippageValue) < 0.1) {
setWarningType(WARNING_TYPE.riskyEntryLow)
}
if (Number(slippageValue) > 5) {
setWarningType(WARNING_TYPE.riskyEntryHigh)
}
//update the actual slippage value in parent
updateSlippage(Number(slippageValue))
},
[updateSlippage]
)
function parseCustomDeadline(e) {
const val = e.target.value
const acceptableValues = [/^$/, /^\d+$/]
if (acceptableValues.some(re => re.test(val))) {
setDeadlineInput(val)
setDeadline(val * 60)
}
}
const setFromCustom = () => {
setActiveIndex(4)
inputRef.current.focus()
// if there's a value, evaluate the bounds
checkBounds(debouncedInput)
let slippageError: SlippageError
if (slippageInput !== '' && !slippageInputIsValid) {
slippageError = SlippageError.InvalidInput
} else if (slippageInputIsValid && rawSlippage < 50) {
slippageError = SlippageError.RiskyLow
} else if (slippageInputIsValid && rawSlippage > 500) {
slippageError = SlippageError.RiskyHigh
}
// used for slippage presets
const setFromFixed = useCallback(
(index, slippage) => {
// update slippage in parent, reset errors and input state
updateSlippage(slippage)
setWarningType(WARNING_TYPE.none)
setActiveIndex(index)
},
[updateSlippage]
)
let deadlineError: DeadlineError
if (deadlineInput !== '' && !deadlineInputIsValid) {
deadlineError = DeadlineError.InvalidInput
}
useEffect(() => {
switch (initialSlippage) {
case 10:
setFromFixed(1, 0.1)
break
case 50:
setFromFixed(2, 0.5)
break
case 100:
setFromFixed(3, 1)
break
default:
// restrict to 2 decimal places
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
// if its within accepted decimal limit, update the input state
if (acceptableValues.some(val => val.test('' + initialSlippage / 100))) {
setUserInput('' + initialSlippage / 100)
setActiveIndex(4)
}
}
}, [initialSlippage, setFromFixed])
function parseCustomSlippage(event) {
setSlippageInput(event.target.value)
// check that the theyve entered number and correct decimal
const parseInput = e => {
const input = e.target.value
let valueAsIntFromRoundedFloat: number
try {
valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(event.target.value) * 100).toString())
} catch {}
// restrict to 2 decimal places
const acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
// if its within accepted decimal limit, update the input state
if (acceptableValues.some(a => a.test(input))) {
setUserInput(input)
if (
typeof valueAsIntFromRoundedFloat === 'number' &&
!Number.isNaN(valueAsIntFromRoundedFloat) &&
valueAsIntFromRoundedFloat < 5000
) {
setRawSlippage(valueAsIntFromRoundedFloat)
}
}
useEffect(() => {
if (activeIndex === 4) {
checkBounds(debouncedInput)
function parseCustomDeadline(event) {
setDeadlineInput(event.target.value)
let valueAsInt: number
try {
valueAsInt = Number.parseInt(event.target.value) * 60
} catch {}
if (typeof valueAsInt === 'number' && !Number.isNaN(valueAsInt) && valueAsInt > 0) {
setDeadline(valueAsInt)
}
})
}
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>
<RowBetween>
<Option
onClick={() => {
setFromFixed(1, 0.1)
setSlippageInput('')
setRawSlippage(10)
}}
active={activeIndex === 1}
active={rawSlippage === 10}
>
0.1%
</Option>
<Option
onClick={() => {
setFromFixed(2, 0.5)
setSlippageInput('')
setRawSlippage(50)
}}
active={activeIndex === 2}
active={rawSlippage === 50}
>
0.5%
</Option>
<Option
onClick={() => {
setFromFixed(3, 1)
setSlippageInput('')
setRawSlippage(100)
}}
active={activeIndex === 3}
active={rawSlippage === 100}
>
1%
</Option>
<OptionCustom
active={activeIndex === 4}
warning={
warningType !== WARNING_TYPE.none &&
warningType !== WARNING_TYPE.emptyInput &&
warningType !== WARNING_TYPE.riskyEntryLow
}
onClick={() => {
setFromCustom()
}}
>
<OptionCustom active={![10, 50, 100].includes(rawSlippage)} warning={!slippageInputIsValid} tabIndex={-1}>
<RowBetween>
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && (
<span
role="img"
aria-label="warning"
style={{
color:
warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: warningType === WARNING_TYPE.riskyEntryLow
? '#F3841E'
: ''
}}
>
{!!slippageInput &&
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
<span role="img" aria-label="warning" style={{ color: '#F3841E' }}>
</span>
)}
) : null}
<Input
tabIndex={-1}
ref={inputRef}
active={activeIndex === 4}
placeholder={
activeIndex === 4
? !!userInput
? ''
: '0'
: activeIndex !== 4 && userInput !== ''
? userInput
: 'Custom'
}
value={activeIndex === 4 ? userInput : ''}
onChange={parseInput}
color={
warningType === WARNING_TYPE.emptyInput
? ''
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: ''
}
placeholder={(rawSlippage / 100).toFixed(2)}
value={slippageInput}
onBlur={() => {
parseCustomSlippage({ target: { value: (rawSlippage / 100).toFixed(2) } })
}}
onChange={parseCustomSlippage}
color={!slippageInputIsValid ? 'red' : ''}
/>
<Percent
color={
activeIndex !== 4
? 'faded'
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
? 'red'
: ''
}
>
%
</Percent>
%
</RowBetween>
</OptionCustom>
</RowBetween>
<RowBetween>
<BottomError
show={activeIndex === 4}
color={
warningType === WARNING_TYPE.emptyInput
? '#565A69'
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
? 'red'
: warningType === WARNING_TYPE.riskyEntryLow
? '#F3841E'
: ''
}
{!!slippageError && (
<RowBetween
style={{
fontSize: '14px',
paddingTop: '7px',
color: slippageError === SlippageError.InvalidInput ? 'red' : '#F3841E'
}}
>
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'}
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'}
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'}
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'}
</BottomError>
</RowBetween>
{slippageError === SlippageError.InvalidInput
? 'Enter a valid slippage percentage'
: slippageError === SlippageError.RiskyLow
? 'Your transaction may fail'
: 'Your transaction may be frontrun'}
</RowBetween>
)}
</SlippageSelector>
<AutoColumn gap="sm">
<RowFixed padding={'0 20px'}>
<TYPE.black fontSize={14} color={theme.text2}>
Deadline
</TYPE.black>
<QuestionHelper text="Deadline in minutes. If your transaction takes longer than this it will revert." />
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
</RowFixed>
<RowFixed padding={'0 20px'}>
<OptionCustom style={{ width: '80px' }}>
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
<Input
tabIndex={-1}
placeholder={'' + deadlineInput}
color={!!deadlineError ? 'red' : undefined}
onBlur={() => {
parseCustomDeadline({ target: { value: (deadline / 60).toString() } })
}}
placeholder={(deadline / 60).toString()}
value={deadlineInput}
onChange={parseCustomDeadline}
/>

View File

@@ -13,42 +13,23 @@ import { RowBetween, RowFixed } from '../Row'
import SlippageTabs, { SlippageTabsProps } from '../SlippageTabs'
import FormattedPriceImpact from './FormattedPriceImpact'
import TokenLogo from '../TokenLogo'
import flatMap from 'lodash.flatmap'
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
trade: Trade
onDismiss: () => void
}
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
function TradeSummary({ trade, allowedSlippage }: { trade: Trade; allowedSlippage: number }) {
const theme = useContext(ThemeContext)
const { priceImpactWithoutFee, realizedLPFee } = computeTradePriceBreakdown(trade)
const isExactIn = trade.tradeType === TradeType.EXACT_INPUT
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, slippageTabProps.rawSlippage)
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(trade, allowedSlippage)
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 />
<>
<AutoColumn style={{ padding: '0 20px' }}>
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{isExactIn ? 'Minimum received' : 'Maximum sold'}
</TYPE.black>
<QuestionHelper
text={
isExactIn
? 'Price can change between when a transaction is submitted and when it is executed. This is the minimum amount you will receive. A worse rate will cause your transaction to revert.'
: 'Price can change between when a transaction is submitted and when it is executed. This is the maximum amount you will pay. A worse rate will cause your transaction to revert.'
}
/>
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
</RowFixed>
<RowFixed>
<TYPE.black color={theme.text1} fontSize={14}>
@@ -82,16 +63,36 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
</AutoColumn>
<SectionBreak />
</>
)
}
export interface AdvancedSwapDetailsProps extends SlippageTabsProps {
trade?: Trade
onDismiss: () => void
}
export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: AdvancedSwapDetailsProps) {
const theme = useContext(ThemeContext)
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} />}
<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 execution price changes by more than this amount after you submit your trade." />
</RowFixed>
<SlippageTabs {...slippageTabProps} />
{trade.route.path.length > 2 && (
{trade?.route?.path?.length > 2 && (
<AutoColumn style={{ padding: '0 20px' }}>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
@@ -109,27 +110,28 @@ export function AdvancedSwapDetails({ trade, onDismiss, ...slippageTabProps }: A
justifyContent="space-evenly"
alignItems="center"
>
{trade.route.path
{flatMap(
trade.route.path,
// add a null in-between each item
.flatMap((token, i, array) => {
(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>
)
}
})}
}
).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>
)}

View File

@@ -1,30 +1,23 @@
import { Percent } from '@uniswap/sdk'
import React, { useContext } from 'react'
import { ChevronDown } from 'react-feather'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { CursorPointer } from '../../theme'
import { warningServerity } from '../../utils/prices'
import { AutoColumn } from '../Column'
import { RowBetween } from '../Row'
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
import { PriceSlippageWarningCard } from './PriceSlippageWarningCard'
import { AdvancedDropwdown, FixedBottom } from './styleds'
import { AdvancedDropdown } from './styleds'
export default function AdvancedSwapDetailsDropdown({
priceImpactWithoutFee,
showAdvanced,
setShowAdvanced,
...rest
}: Omit<AdvancedSwapDetailsProps, 'onDismiss'> & {
showAdvanced: boolean
setShowAdvanced: (showAdvanced: boolean) => void
priceImpactWithoutFee: Percent
}) {
const theme = useContext(ThemeContext)
const severity = warningServerity(priceImpactWithoutFee)
return (
<AdvancedDropwdown>
<AdvancedDropdown>
{showAdvanced ? (
<AdvancedSwapDetails {...rest} onDismiss={() => setShowAdvanced(false)} />
) : (
@@ -37,11 +30,6 @@ export default function AdvancedSwapDetailsDropdown({
</RowBetween>
</CursorPointer>
)}
<FixedBottom>
<AutoColumn gap="lg">
{severity > 2 && <PriceSlippageWarningCard priceSlippage={priceImpactWithoutFee} />}
</AutoColumn>
</FixedBottom>
</AdvancedDropwdown>
</AdvancedDropdown>
)
}

View File

@@ -1,12 +1,12 @@
import { Percent } from '@uniswap/sdk'
import React from 'react'
import { ONE_BIPS } from '../../constants'
import { warningServerity } from '../../utils/prices'
import { warningSeverity } from '../../utils/prices'
import { ErrorText } from './styleds'
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
return (
<ErrorText fontWeight={500} fontSize={14} severity={warningServerity(priceImpact)}>
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
</ErrorText>
)

View File

@@ -66,9 +66,9 @@ export default function SwapModalFooter({
<RowBetween>
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Min sent' : 'Maximum sold'}
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum sent' : 'Maximum sold'}
</TYPE.black>
<QuestionHelper text="A boundary is set so you are protected from large price movements after you submit your trade." />
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
</RowFixed>
<RowFixed>
<TYPE.black fontSize={14}>

View File

@@ -21,26 +21,16 @@ export const ArrowWrapper = styled.div`
}
`
export const FixedBottom = styled.div`
position: absolute;
margin-top: 1.5rem;
export const AdvancedDropdown = styled.div`
padding-top: calc(10px + 2rem);
padding-bottom: 10px;
margin-top: -2rem;
width: 100%;
margin-bottom: 40px;
`
export const AdvancedDropwdown = styled.div`
position: absolute;
margin-top: -12px;
max-width: 455px;
width: 100%;
margin-bottom: 100px;
padding: 10px 0;
padding-top: 36px;
max-width: 400px;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
color: ${({ theme }) => theme.text2};
background-color: ${({ theme }) => theme.advancedBG};
color: ${({ theme }) => theme.text2};
z-index: -1;
`
@@ -57,7 +47,7 @@ export const BottomGrouping = styled.div`
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 }>`
color: ${({ theme, severity }) =>
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.green1 : theme.text1};
severity === 3 ? theme.red1 : severity === 2 ? theme.yellow2 : severity === 1 ? theme.text1 : theme.green1};
`
export const InputGroup = styled(AutoColumn)`

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,10 @@ const BackgroundGradient = styled.div`
}
`
const Marginer = styled.div`
margin-top: 5rem;
`
let Router: React.ComponentType
if (process.env.PUBLIC_URL === '.') {
Router = HashRouter
@@ -99,6 +103,7 @@ export default function App() {
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Web3ReactManager>
<Marginer />
<Footer />
</BodyWrapper>
<BackgroundGradient />

View File

@@ -2,17 +2,15 @@ import React from 'react'
import styled from 'styled-components'
import NavigationTabs from '../components/NavigationTabs'
export const Body = styled.div`
const Body = styled.div`
position: relative;
max-width: 420px;
width: 100%;
/* min-height: 340px; */
background: ${({ 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: 30px;
padding: 1rem;
position: relative;
margin-bottom: 10rem;
`
/**

View File

@@ -4,12 +4,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div`
position: relative;
`
export const FixedBottom = styled.div`
position: absolute;
top: 100px;
width: 100%;
margin-bottom: 80px;
`
export const ClickableText = styled(Text)`
:hover {
cursor: pointer;

File diff suppressed because it is too large Load Diff

View File

@@ -30,14 +30,20 @@ import { useSendCallback } from '../../hooks/useSendCallback'
import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
import {
useDefaultsFromURLSearch,
useDerivedSwapInfo,
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
export default function Send({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search)
useDefaultsFromURLSearch(search)
// text translation
// const { t } = useTranslation()
@@ -173,7 +179,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const [showInverted, setShowInverted] = useState<boolean>(false)
// warnings on slippage
const severity = !sendingWithSwap ? 0 : warningServerity(priceImpactWithoutFee)
const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
function modalHeader() {
if (!sendingWithSwap) {
@@ -492,20 +498,26 @@ export default function Send({ location: { search } }: RouteComponentProps) {
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
</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>
)}
</>
)
}

View File

@@ -27,23 +27,30 @@ import { useApproveCallbackFromTrade, ApprovalState } from '../../hooks/useAppro
import { useSwapCallback } from '../../hooks/useSwapCallback'
import { useWalletModalToggle } from '../../state/application/hooks'
import { Field } from '../../state/swap/actions'
import { useDefaultsFromURL, useDerivedSwapInfo, useSwapActionHandlers, useSwapState } from '../../state/swap/hooks'
import {
useDefaultsFromURLSearch,
useDerivedSwapInfo,
useSwapActionHandlers,
useSwapState
} from '../../state/swap/hooks'
import { CursorPointer, TYPE } from '../../theme'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningServerity } from '../../utils/prices'
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
import AppBody from '../AppBody'
import { PriceSlippageWarningCard } from '../../components/swap/PriceSlippageWarningCard'
export default function Swap({ location: { search } }: RouteComponentProps) {
useDefaultsFromURL(search)
// text translation
// const { t } = useTranslation()
useDefaultsFromURLSearch(search)
const { chainId, account } = useActiveWeb3React()
const theme = useContext(ThemeContext)
// toggle wallet when disconnected
const toggleWalletModal = useWalletModalToggle()
// swap state
const { independentField, typedValue } = useSwapState()
const { bestTrade, tokenBalances, parsedAmounts, tokens, error, v1TradeLinkIfBetter } = useDerivedSwapInfo()
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
const isValid = !error
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
@@ -58,6 +65,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
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) : ''
}
const route = bestTrade?.route
const userHasSpecifiedInputOutput =
!!tokens[Field.INPUT] &&
@@ -69,13 +81,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
// check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
}
const { onSwitchTokens, onTokenSelection, onUserInput } = useSwapActionHandlers()
const maxAmountInput: TokenAmount =
!!tokenBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
@@ -88,7 +93,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
: tokenBalances[Field.INPUT]
: undefined
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
maxAmountInput && parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
@@ -130,7 +135,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const [showInverted, setShowInverted] = useState<boolean>(false)
// warnings on slippage
const priceImpactSeverity = warningServerity(priceImpactWithoutFee)
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
function modalHeader() {
return (
@@ -259,13 +264,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
</AutoColumn>
<BottomGrouping>
{!account ? (
<ButtonLight
onClick={() => {
toggleWalletModal()
}}
>
Connect Wallet
</ButtonLight>
<ButtonLight onClick={toggleWalletModal}>Connect Wallet</ButtonLight>
) : noRoute && userHasSpecifiedInputOutput ? (
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
@@ -294,20 +293,26 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
)}
<V1TradeLink v1TradeLinkIfBetter={v1TradeLinkIfBetter} />
</BottomGrouping>
{bestTrade && (
<AdvancedSwapDetailsDropdown
trade={bestTrade}
rawSlippage={allowedSlippage}
deadline={deadline}
showAdvanced={showAdvanced}
setShowAdvanced={setShowAdvanced}
priceImpactWithoutFee={priceImpactWithoutFee}
setDeadline={setDeadline}
setRawSlippage={setAllowedSlippage}
/>
)}
</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>
)}
</>
)
}

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'
import { useDebounce, useActiveWeb3React } from '../../hooks'
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
import { updateBlockNumber } from './actions'
import { useDispatch } from 'react-redux'
@@ -7,6 +8,7 @@ export default function Updater() {
const { library, chainId } = useActiveWeb3React()
const dispatch = useDispatch()
const windowVisible = useIsWindowVisible()
const [maxBlockNumber, setMaxBlockNumber] = useState<number | null>(null)
// because blocks arrive in bunches with longer polling periods, we just want
// to process the latest one.
@@ -38,8 +40,10 @@ export default function Updater() {
useEffect(() => {
if (!chainId || !debouncedMaxBlockNumber) return
dispatch(updateBlockNumber({ chainId, blockNumber: debouncedMaxBlockNumber }))
}, [chainId, debouncedMaxBlockNumber, dispatch])
if (windowVisible) {
dispatch(updateBlockNumber({ chainId, blockNumber: debouncedMaxBlockNumber }))
}
}, [chainId, debouncedMaxBlockNumber, windowVisible, dispatch])
return null
}

10
src/state/burn/actions.ts Normal file
View File

@@ -0,0 +1,10 @@
import { createAction } from '@reduxjs/toolkit'
export enum Field {
LIQUIDITY_PERCENT = 'LIQUIDITY_PERCENT',
LIQUIDITY = 'LIQUIDITY',
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
}
export const typeInput = createAction<{ field: Field; typedValue: string }>('typeInputBurn')

187
src/state/burn/hooks.ts Normal file
View File

@@ -0,0 +1,187 @@
import { useEffect, useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
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 { Token, Pair, TokenAmount, Percent, JSBI, Route } from '@uniswap/sdk'
import { usePair } from '../../data/Reserves'
import { useTokenBalances } from '../wallet/hooks'
import { tryParseAmount } from '../swap/hooks'
import { useTotalSupply } from '../../data/TotalSupply'
const ZERO = JSBI.BigInt(0)
export function useBurnState(): AppState['burn'] {
return useSelector<AppState, AppState['burn']>(state => state.burn)
}
export function useDerivedBurnInfo(): {
tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token }
pair?: Pair | null
route?: Route
parsedAmounts: {
[Field.LIQUIDITY_PERCENT]: Percent
[Field.LIQUIDITY]?: TokenAmount
[Field.TOKEN_A]?: TokenAmount
[Field.TOKEN_B]?: TokenAmount
}
error?: string
} {
const { account } = useActiveWeb3React()
const {
independentField,
typedValue,
[Field.TOKEN_A]: { address: tokenAAddress },
[Field.TOKEN_B]: { address: tokenBAddress }
} = useBurnState()
// tokens
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
const tokens: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: Token } = useMemo(
() => ({
[Field.TOKEN_A]: tokenA,
[Field.TOKEN_B]: tokenB
}),
[tokenA, tokenB]
)
// pair + totalsupply
const pair = usePair(tokens[Field.TOKEN_A], tokens[Field.TOKEN_B])
const noLiquidity =
pair === null || (!!pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// route
const route =
!noLiquidity && pair && tokens[Field.TOKEN_A] ? new Route([pair], tokens[Field.TOKEN_A] as Token) : undefined
// balances
const relevantTokenBalances = useTokenBalances(account ?? undefined, [pair?.liquidityToken])
const userLiquidity: undefined | TokenAmount = relevantTokenBalances?.[pair?.liquidityToken?.address ?? '']
// liquidity values
const totalSupply = useTotalSupply(pair?.liquidityToken)
const liquidityValues: { [field in Extract<Field, Field.TOKEN_A | Field.TOKEN_B>]?: TokenAmount } = {
[Field.TOKEN_A]:
pair &&
tokens[Field.TOKEN_A] &&
totalSupply &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(
tokens[Field.TOKEN_A] as Token,
pair.getLiquidityValue(tokens[Field.TOKEN_A] as Token, totalSupply, userLiquidity, false).raw
)
: undefined,
[Field.TOKEN_B]:
pair &&
tokens[Field.TOKEN_B] &&
totalSupply &&
userLiquidity &&
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
JSBI.greaterThanOrEqual(totalSupply.raw, userLiquidity.raw)
? new TokenAmount(
tokens[Field.TOKEN_B] as Token,
pair.getLiquidityValue(tokens[Field.TOKEN_B] as Token, totalSupply, userLiquidity, false).raw
)
: undefined
}
let percentToRemove: Percent = new Percent('0', '100')
// user specified a %
if (independentField === Field.LIQUIDITY_PERCENT) {
percentToRemove = new Percent(typedValue, '100')
}
// user specified a specific amount of liquidity tokens
else if (independentField === Field.LIQUIDITY) {
if (pair?.liquidityToken) {
const independentAmount = tryParseAmount(typedValue, pair.liquidityToken)
if (independentAmount && userLiquidity && !independentAmount.greaterThan(userLiquidity)) {
percentToRemove = new Percent(independentAmount.raw, userLiquidity.raw)
}
}
}
// user specified a specific amount of token a or b
else {
if (tokens[independentField]) {
const independentAmount = tryParseAmount(typedValue, tokens[independentField])
if (
independentAmount &&
liquidityValues[independentField] &&
!independentAmount.greaterThan(liquidityValues[independentField] as TokenAmount)
) {
percentToRemove = new Percent(independentAmount.raw, (liquidityValues[independentField] as TokenAmount).raw)
}
}
}
const parsedAmounts: {
[Field.LIQUIDITY_PERCENT]: Percent
[Field.LIQUIDITY]?: TokenAmount
[Field.TOKEN_A]?: TokenAmount
[Field.TOKEN_B]?: TokenAmount
} = {
[Field.LIQUIDITY_PERCENT]: percentToRemove,
[Field.LIQUIDITY]:
userLiquidity && percentToRemove && percentToRemove.greaterThan('0')
? new TokenAmount(userLiquidity.token, percentToRemove.multiply(userLiquidity.raw).quotient)
: undefined,
[Field.TOKEN_A]:
tokens[Field.TOKEN_A] && percentToRemove && percentToRemove.greaterThan('0') && liquidityValues[Field.TOKEN_A]
? new TokenAmount(
tokens[Field.TOKEN_A] as Token,
percentToRemove.multiply((liquidityValues[Field.TOKEN_A] as TokenAmount).raw).quotient
)
: undefined,
[Field.TOKEN_B]:
tokens[Field.TOKEN_B] && percentToRemove && percentToRemove.greaterThan('0') && liquidityValues[Field.TOKEN_B]
? new TokenAmount(
tokens[Field.TOKEN_B] as Token,
percentToRemove.multiply((liquidityValues[Field.TOKEN_B] as TokenAmount).raw).quotient
)
: undefined
}
let error: string | undefined
if (!account) {
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.LIQUIDITY] || !parsedAmounts[Field.TOKEN_A] || !parsedAmounts[Field.TOKEN_B]) {
error = error ?? 'Enter an amount'
}
return { tokens, pair, route, parsedAmounts, error }
}
export function useBurnActionHandlers(): {
onUserInput: (field: Field, typedValue: string) => void
} {
const dispatch = useDispatch<AppDispatch>()
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
dispatch(typeInput({ field, typedValue }))
},
[dispatch]
)
return {
onUserInput
}
}
// updates the burn state to use the appropriate tokens, given the route
export function useDefaultsFromURLMatchParams(params: { [k: string]: string }) {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
if (!chainId) return
dispatch(setDefaultsFromURLMatchParams({ chainId, params }))
}, [dispatch, chainId, params])
}

51
src/state/burn/reducer.ts Normal file
View File

@@ -0,0 +1,51 @@
import { createReducer } from '@reduxjs/toolkit'
import { Field, typeInput } from './actions'
import { setDefaultsFromURLMatchParams } from '../mint/actions'
import { parseTokens } from '../mint/reducer'
export interface MintState {
readonly independentField: Field
readonly typedValue: string
readonly [Field.TOKEN_A]: {
readonly address: string
}
readonly [Field.TOKEN_B]: {
readonly address: string
}
}
const initialState: MintState = {
independentField: Field.LIQUIDITY_PERCENT,
typedValue: '0',
[Field.TOKEN_A]: {
address: ''
},
[Field.TOKEN_B]: {
address: ''
}
}
export default createReducer<MintState>(initialState, builder =>
builder
.addCase(setDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => {
const tokens = parseTokens(chainId, params?.tokens ?? '')
return {
independentField: Field.LIQUIDITY_PERCENT,
typedValue: '0',
[Field.TOKEN_A]: {
address: tokens[0]
},
[Field.TOKEN_B]: {
address: tokens[1]
}
}
})
.addCase(typeInput, (state, { payload: { field, typedValue } }) => {
return {
...state,
independentField: field,
typedValue
}
})
)

View File

@@ -1,11 +1,15 @@
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import { save, load } from 'redux-localstorage-simple'
import application from './application/reducer'
import { updateVersion } from './user/actions'
import user from './user/reducer'
import transactions from './transactions/reducer'
import wallet from './wallet/reducer'
import swap from './swap/reducer'
import transactions from './transactions/reducer'
import { save, load } from 'redux-localstorage-simple'
import mint from './mint/reducer'
import burn from './burn/reducer'
import { updateVersion } from './user/actions'
const PERSISTED_KEYS: string[] = ['user', 'transactions']
@@ -15,7 +19,9 @@ const store = configureStore({
user,
transactions,
wallet,
swap
swap,
mint,
burn
},
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ states: PERSISTED_KEYS })

13
src/state/mint/actions.ts Normal file
View File

@@ -0,0 +1,13 @@
import { createAction } from '@reduxjs/toolkit'
import { RouteComponentProps } from 'react-router-dom'
export enum Field {
TOKEN_A = 'TOKEN_A',
TOKEN_B = 'TOKEN_B'
}
export const setDefaultsFromURLMatchParams = createAction<{
chainId: number
params: RouteComponentProps<{ [k: string]: string }>['match']['params']
}>('setDefaultsFromMatch')
export const typeInput = createAction<{ field: Field; typedValue: string; noLiquidity: boolean }>('typeInputMint')

202
src/state/mint/hooks.ts Normal file
View File

@@ -0,0 +1,202 @@
import { useEffect, useCallback, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Token, TokenAmount, Route, JSBI, Price, Percent, Pair } from '@uniswap/sdk'
import { useActiveWeb3React } from '../../hooks'
import { AppDispatch, AppState } from '../index'
import { setDefaultsFromURLMatchParams, Field, typeInput } from './actions'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { tryParseAmount } from '../swap/hooks'
const ZERO = JSBI.BigInt(0)
export function useMintState(): AppState['mint'] {
return useSelector<AppState, AppState['mint']>(state => state.mint)
}
export function useDerivedMintInfo(): {
dependentField: Field
tokens: { [field in Field]?: Token }
pair?: Pair | null
tokenBalances: { [field in Field]?: TokenAmount }
parsedAmounts: { [field in Field]?: TokenAmount }
price?: Price
noLiquidity?: boolean
liquidityMinted?: TokenAmount
poolTokenPercentage?: Percent
error?: string
} {
const { account } = useActiveWeb3React()
const {
independentField,
typedValue,
otherTypedValue,
[Field.TOKEN_A]: { address: tokenAAddress },
[Field.TOKEN_B]: { address: tokenBAddress }
} = useMintState()
const dependentField = independentField === Field.TOKEN_A ? Field.TOKEN_B : Field.TOKEN_A
// tokens
const tokenA = useTokenByAddressAndAutomaticallyAdd(tokenAAddress)
const tokenB = useTokenByAddressAndAutomaticallyAdd(tokenBAddress)
const tokens: { [field in Field]?: Token } = useMemo(
() => ({
[Field.TOKEN_A]: tokenA,
[Field.TOKEN_B]: tokenB
}),
[tokenA, tokenB]
)
// pair
const pair = usePair(tokens[Field.TOKEN_A], tokens[Field.TOKEN_B])
const noLiquidity =
pair === null || (!!pair && JSBI.equal(pair.reserve0.raw, ZERO) && JSBI.equal(pair.reserve1.raw, ZERO))
// route
const route = useMemo(
() =>
!noLiquidity && pair && tokens[independentField] ? new Route([pair], tokens[Field.TOKEN_A] as Token) : undefined,
[noLiquidity, pair, tokens, independentField]
)
// balances
const relevantTokenBalances = useTokenBalancesTreatWETHAsETH(account ?? undefined, [
tokens[Field.TOKEN_A],
tokens[Field.TOKEN_B]
])
const tokenBalances: { [field in Field]?: TokenAmount } = {
[Field.TOKEN_A]: relevantTokenBalances?.[tokens[Field.TOKEN_A]?.address ?? ''],
[Field.TOKEN_B]: relevantTokenBalances?.[tokens[Field.TOKEN_B]?.address ?? '']
}
// amounts
const independentAmount = tryParseAmount(typedValue, tokens[independentField])
const dependentAmount = useMemo(() => {
if (noLiquidity && otherTypedValue && tokens[dependentField]) {
return tryParseAmount(otherTypedValue, tokens[dependentField])
} else if (route && independentAmount) {
return dependentField === Field.TOKEN_B
? route.midPrice.quote(independentAmount)
: route.midPrice.invert().quote(independentAmount)
} else {
return
}
}, [noLiquidity, otherTypedValue, tokens, dependentField, independentAmount, route])
const parsedAmounts = {
[Field.TOKEN_A]: independentField === Field.TOKEN_A ? independentAmount : dependentAmount,
[Field.TOKEN_B]: independentField === Field.TOKEN_A ? dependentAmount : independentAmount
}
const price = useMemo(() => {
if (
noLiquidity &&
tokens[Field.TOKEN_A] &&
tokens[Field.TOKEN_B] &&
parsedAmounts[Field.TOKEN_A] &&
parsedAmounts[Field.TOKEN_B]
) {
return new Price(
tokens[Field.TOKEN_A] as Token,
tokens[Field.TOKEN_B] as Token,
(parsedAmounts[Field.TOKEN_A] as TokenAmount).raw,
(parsedAmounts[Field.TOKEN_B] as TokenAmount).raw
)
} else if (route) {
return route.midPrice
} else {
return
}
}, [noLiquidity, tokens, parsedAmounts, route])
// liquidity minted
const totalSupply = useTotalSupply(pair?.liquidityToken)
const liquidityMinted = useMemo(() => {
if (pair && totalSupply && parsedAmounts[Field.TOKEN_A] && parsedAmounts[Field.TOKEN_B]) {
return pair.getLiquidityMinted(
totalSupply,
parsedAmounts[Field.TOKEN_A] as TokenAmount,
parsedAmounts[Field.TOKEN_B] as TokenAmount
)
} else {
return
}
}, [pair, totalSupply, parsedAmounts])
const poolTokenPercentage = useMemo(() => {
if (liquidityMinted && totalSupply) {
return new Percent(liquidityMinted.raw, totalSupply.add(liquidityMinted).raw)
} else {
return
}
}, [liquidityMinted, totalSupply])
let error: string | undefined
if (!account) {
error = 'Connect Wallet'
}
if (!parsedAmounts[Field.TOKEN_A] || !parsedAmounts[Field.TOKEN_B]) {
error = error ?? 'Enter an amount'
}
if (
parsedAmounts[Field.TOKEN_A] &&
tokenBalances?.[Field.TOKEN_A]?.lessThan(parsedAmounts[Field.TOKEN_A] as TokenAmount)
) {
error = 'Insufficient ' + tokens[Field.TOKEN_A]?.symbol + ' balance'
}
if (
parsedAmounts[Field.TOKEN_B] &&
tokenBalances?.[Field.TOKEN_B]?.lessThan(parsedAmounts[Field.TOKEN_B] as TokenAmount)
) {
error = 'Insufficient ' + tokens[Field.TOKEN_B]?.symbol + ' balance'
}
return {
dependentField,
tokens,
pair,
tokenBalances,
parsedAmounts,
price,
noLiquidity,
liquidityMinted,
poolTokenPercentage,
error
}
}
export function useMintActionHandlers(): {
onUserInput: (field: Field, typedValue: string) => void
} {
const dispatch = useDispatch<AppDispatch>()
const { noLiquidity } = useDerivedMintInfo()
const onUserInput = useCallback(
(field: Field, typedValue: string) => {
dispatch(typeInput({ field, typedValue, noLiquidity: noLiquidity === true ? true : false }))
},
[dispatch, noLiquidity]
)
return {
onUserInput
}
}
// updates the mint state to use the appropriate tokens, given the route
export function useDefaultsFromURLMatchParams(params: { [k: string]: string }) {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
if (!chainId) return
dispatch(setDefaultsFromURLMatchParams({ chainId, params }))
}, [dispatch, chainId, params])
}

View File

@@ -0,0 +1,40 @@
import { ChainId, WETH } from '@uniswap/sdk'
import { createStore, Store } from 'redux'
import { Field, setDefaultsFromURLMatchParams } from './actions'
import reducer, { MintState } from './reducer'
describe('mint reducer', () => {
let store: Store<MintState>
beforeEach(() => {
store = createStore(reducer, {
independentField: Field.TOKEN_A,
typedValue: '',
otherTypedValue: '',
[Field.TOKEN_A]: { address: '' },
[Field.TOKEN_B]: { address: '' }
})
})
describe('setDefaultsFromURLMatchParams', () => {
test('ETH to DAI', () => {
store.dispatch(
setDefaultsFromURLMatchParams({
chainId: ChainId.MAINNET,
params: {
tokens: 'ETH-0x6b175474e89094c44da98b954eedeac495271d0f'
}
})
)
expect(store.getState()).toEqual({
independentField: Field.TOKEN_A,
typedValue: '',
otherTypedValue: '',
[Field.TOKEN_A]: { address: WETH[ChainId.MAINNET].address },
[Field.TOKEN_B]: { address: '0x6b175474e89094c44da98b954eedeac495271d0f' }
})
})
})
})

97
src/state/mint/reducer.ts Normal file
View File

@@ -0,0 +1,97 @@
import { createReducer } from '@reduxjs/toolkit'
import { ChainId, WETH } from '@uniswap/sdk'
import { isAddress } from '../../utils'
import { Field, setDefaultsFromURLMatchParams, typeInput } from './actions'
export interface MintState {
readonly independentField: Field
readonly typedValue: string
readonly otherTypedValue: string // for the case when there's no liquidity
readonly [Field.TOKEN_A]: {
readonly address: string
}
readonly [Field.TOKEN_B]: {
readonly address: string
}
}
const initialState: MintState = {
independentField: Field.TOKEN_A,
typedValue: '',
otherTypedValue: '',
[Field.TOKEN_A]: {
address: ''
},
[Field.TOKEN_B]: {
address: ''
}
}
export function parseTokens(chainId: number, tokens: string): string[] {
return (
tokens
// split by '-'
.split('-')
// map to addresses
.map((token): string =>
isAddress(token)
? token
: token.toLowerCase() === 'ETH'.toLowerCase()
? WETH[chainId as ChainId]?.address ?? ''
: ''
)
//remove duplicates
.filter((token, i, array) => array.indexOf(token) === i)
// add two empty elements for cases where the array is length 0
.concat(['', ''])
// only consider the first 2 elements
.slice(0, 2)
)
}
export default createReducer<MintState>(initialState, builder =>
builder
.addCase(setDefaultsFromURLMatchParams, (state, { payload: { chainId, params } }) => {
const tokens = parseTokens(chainId, params?.tokens ?? '')
return {
independentField: Field.TOKEN_A,
typedValue: '',
otherTypedValue: '',
[Field.TOKEN_A]: {
address: tokens[0]
},
[Field.TOKEN_B]: {
address: tokens[1]
}
}
})
.addCase(typeInput, (state, { payload: { field, typedValue, noLiquidity } }) => {
if (noLiquidity) {
// they're typing into the field they've last typed in
if (field === state.independentField) {
return {
...state,
independentField: field,
typedValue
}
}
// they're typing into a new field, store the other value
else {
return {
...state,
independentField: field,
typedValue,
otherTypedValue: state.typedValue
}
}
} else {
return {
...state,
independentField: field,
typedValue,
otherTypedValue: ''
}
}
})
)

View File

@@ -5,7 +5,7 @@ export enum Field {
OUTPUT = 'OUTPUT'
}
export const setDefaultsFromURL = createAction<{ chainId: number; queryString?: string }>('setDefaultsFromURL')
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')

View File

@@ -7,7 +7,7 @@ import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useTradeExactIn, useTradeExactOut } from '../../hooks/Trades'
import { AppDispatch, AppState } from '../index'
import { useTokenBalancesTreatWETHAsETH } from '../wallet/hooks'
import { Field, selectToken, setDefaultsFromURL, switchTokens, typeInput } from './actions'
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
import { useV1TradeLinkIfBetter } from '../../data/V1'
import { V1_TRADE_LINK_THRESHOLD } from '../../constants'
@@ -52,7 +52,7 @@ export function useSwapActionHandlers(): {
}
// try to parse a user entered amount for a given token
function tryParseAmount(value?: string, token?: Token): TokenAmount | undefined {
export function tryParseAmount(value?: string, token?: Token): TokenAmount | undefined {
if (!value || !token) {
return
}
@@ -155,11 +155,11 @@ export function useDerivedSwapInfo(): {
// updates the swap state to use the defaults for a given network whenever the query
// string updates
export function useDefaultsFromURL(search?: string) {
export function useDefaultsFromURLSearch(search?: string) {
const { chainId } = useActiveWeb3React()
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
if (!chainId) return
dispatch(setDefaultsFromURL({ chainId, queryString: search }))
dispatch(setDefaultsFromURLSearch({ chainId, queryString: search }))
}, [dispatch, search, chainId])
}

View File

@@ -1,6 +1,6 @@
import { ChainId, WETH } from '@uniswap/sdk'
import { createStore, Store } from 'redux'
import { Field, setDefaultsFromURL } from './actions'
import { Field, setDefaultsFromURLSearch } from './actions'
import reducer, { SwapState } from './reducer'
describe('swap reducer', () => {
@@ -18,7 +18,7 @@ describe('swap reducer', () => {
describe('setDefaultsFromURL', () => {
test('ETH to DAI', () => {
store.dispatch(
setDefaultsFromURL({
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString:
'?inputCurrency=ETH&outputCurrency=0x6b175474e89094c44da98b954eedeac495271d0f&exactAmount=20.5&exactField=outPUT'
@@ -35,7 +35,7 @@ describe('swap reducer', () => {
test('does not duplicate eth for invalid output token', () => {
store.dispatch(
setDefaultsFromURL({
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString: '?outputCurrency=invalid'
})
@@ -51,7 +51,7 @@ describe('swap reducer', () => {
test('output ETH only', () => {
store.dispatch(
setDefaultsFromURL({
setDefaultsFromURLSearch({
chainId: ChainId.MAINNET,
queryString: '?outputCurrency=eth&exactAmount=20.5'
})

View File

@@ -2,7 +2,7 @@ import { parse } from 'qs'
import { createReducer } from '@reduxjs/toolkit'
import { ChainId, WETH } from '@uniswap/sdk'
import { isAddress } from '../../utils'
import { Field, selectToken, setDefaultsFromURL, switchTokens, typeInput } from './actions'
import { Field, selectToken, setDefaultsFromURLSearch, switchTokens, typeInput } from './actions'
export interface SwapState {
readonly independentField: Field
@@ -47,7 +47,7 @@ function parseIndependentFieldURLParameter(urlParam: any): Field {
export default createReducer<SwapState>(initialState, builder =>
builder
.addCase(setDefaultsFromURL, (state, { payload: { queryString, chainId } }) => {
.addCase(setDefaultsFromURLSearch, (_, { payload: { queryString, chainId } }) => {
if (queryString && queryString.length > 1) {
const parsedQs = parse(queryString, { parseArrays: false, ignoreQueryPrefix: true })

View File

@@ -14,6 +14,7 @@ import {
SerializedToken,
updateUserDarkMode
} from './actions'
import flatMap from 'lodash.flatmap'
function serializeToken(token: Token): SerializedToken {
return {
@@ -165,10 +166,11 @@ export function useAllDummyPairs(): Pair[] {
const generatedPairs: Pair[] = useMemo(
() =>
Object.values(tokens)
// select only tokens on the current chain
.filter(token => token.chainId === chainId)
.flatMap(token => {
flatMap(
Object.values(tokens)
// select only tokens on the current chain
.filter(token => token.chainId === chainId),
token => {
// for each token on the current chain,
return (
bases
@@ -184,7 +186,8 @@ export function useAllDummyPairs(): Pair[] {
})
.filter(pair => !!pair) as Pair[]
)
}),
}
),
[tokens, chainId]
)

View File

@@ -51,7 +51,7 @@ export function computeSlippageAdjustedAmounts(
}
}
export function warningServerity(priceImpact: Percent): 0 | 1 | 2 | 3 {
export function warningSeverity(priceImpact: Percent): 0 | 1 | 2 | 3 {
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

View File

@@ -2818,6 +2818,18 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
"@types/lodash.flatmap@^4.5.6":
version "4.5.6"
resolved "https://registry.yarnpkg.com/@types/lodash.flatmap/-/lodash.flatmap-4.5.6.tgz#5f1ea80cebe403f0fbfcc1b5ad75cd09dd8b5785"
integrity sha512-ELNrUL9q+MB7AACaHivWIsKDFDgYhHE3/svXhqvDJgONtn2c467Cy87nEb7CEDvfaGCPv91lPaW596I8s5oiNQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.14.153"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.153.tgz#5cb7dded0649f1df97938ac5ffc4f134e9e9df98"
integrity sha512-lYniGRiRfZf2gGAR9cfRC3Pi5+Q1ziJCKqPmjZocigrSJUVPWf7st1BtSJ8JOeK0FLXVndQ1IjUjTco9CXGo/Q==
"@types/lodash@4.14.149":
version "4.14.149"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"