Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28c916ff45 | ||
|
|
7adb4b6bd6 | ||
|
|
b2f0236ee8 | ||
|
|
4b57059353 | ||
|
|
6926f9a4ae | ||
|
|
7dec580944 | ||
|
|
5cf95680ef | ||
|
|
f8d6bab4ae | ||
|
|
c9721c42bf | ||
|
|
4414134bb2 | ||
|
|
44ba54e44a | ||
|
|
9ec3109f72 |
2
.env
2
.env
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
23
.github/workflows/release.yaml
vendored
23
.github/workflows/release.yaml
vendored
@@ -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 }}/
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# Uniswap Frontend
|
||||
|
||||
[](https://app.netlify.com/sites/uniswap/deploys)
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](https://prettier.io/)
|
||||
[](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
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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)`
|
||||
|
||||
20
src/hooks/useIsWindowVisible.ts
Normal file
20
src/hooks/useIsWindowVisible.ts
Normal 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
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
10
src/state/burn/actions.ts
Normal 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
187
src/state/burn/hooks.ts
Normal 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
51
src/state/burn/reducer.ts
Normal 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
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -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
13
src/state/mint/actions.ts
Normal 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
202
src/state/mint/hooks.ts
Normal 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])
|
||||
}
|
||||
40
src/state/mint/reducer.test.ts
Normal file
40
src/state/mint/reducer.test.ts
Normal 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
97
src/state/mint/reducer.ts
Normal 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: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -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')
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user