Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c1fe53e4b | ||
|
|
28c916ff45 | ||
|
|
7adb4b6bd6 | ||
|
|
b2f0236ee8 | ||
|
|
4b57059353 | ||
|
|
6926f9a4ae | ||
|
|
7dec580944 | ||
|
|
5cf95680ef | ||
|
|
f8d6bab4ae | ||
|
|
c9721c42bf | ||
|
|
4414134bb2 | ||
|
|
44ba54e44a | ||
|
|
9ec3109f72 | ||
|
|
e75793676a | ||
|
|
32006ded21 | ||
|
|
d4f1c579d8 | ||
|
|
95f3541807 | ||
|
|
da4ca73a1d | ||
|
|
e75bf8d003 | ||
|
|
236f68a459 | ||
|
|
9f07baaad2 | ||
|
|
c75464e1aa | ||
|
|
bc80585bb4 | ||
|
|
ad45b2b7bb | ||
|
|
63ac89e9f3 | ||
|
|
1b6ae0d3db | ||
|
|
7d67819604 | ||
|
|
7b9b332c42 |
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"
|
||||
|
||||
78
.github/workflows/release.yaml
vendored
78
.github/workflows/release.yaml
vendored
@@ -1,36 +1,49 @@
|
||||
name: Release
|
||||
# every morning
|
||||
on:
|
||||
# every morning
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
|
||||
# releases are triggered on changes to this file
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
paths:
|
||||
- '.github/workflows/release.yaml'
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create Release
|
||||
bump_version:
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
new_tag: ${{ steps.github_tag_action.outputs.new_tag }}
|
||||
changelog: ${{ steps.github_tag_action.outputs.changelog }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Bump version and push tag
|
||||
id: bump_version
|
||||
uses: mathieudutour/github-tag-action@v4
|
||||
id: github_tag_action
|
||||
uses: mathieudutour/github-tag-action@v4.5
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: .*
|
||||
|
||||
- name: Cancel this build if no new commits
|
||||
if: ${{ steps.bump_version.outputs.new_tag == null }}
|
||||
uses: andymckay/cancel-action@0.2
|
||||
create_release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: bump_version
|
||||
if: ${{ needs.bump_version.outputs.new_tag != null }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
run: yarn install --ignore-scripts --frozen-lockfile
|
||||
|
||||
- name: Build the IPFS bundle
|
||||
run: yarn ipfs-build
|
||||
@@ -39,14 +52,26 @@ jobs:
|
||||
id: upload
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2
|
||||
with:
|
||||
pin-name: Uniswap ${{ steps.bump_version.outputs.new_tag }}
|
||||
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
|
||||
path: './build'
|
||||
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
|
||||
id: update_dns
|
||||
run: npx vercel --token ${{ secrets.VERCEL_TOKEN }} --scope uniswap dns add uniswap.org _dnslink.app TXT "dnslink=/ipfs/${{ steps.upload.outputs.hash }}"
|
||||
uses: uniswap/replace-vercel-dns-records@v1.0.0
|
||||
with:
|
||||
domain: 'uniswap.org'
|
||||
subdomain: '_dnslink.app'
|
||||
record-type: 'TXT'
|
||||
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
|
||||
token: ${{ secrets.VERCEL_TOKEN }}
|
||||
team-name: 'uniswap'
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
@@ -54,26 +79,27 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ steps.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ steps.bump_version.outputs.new_tag }}
|
||||
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.
|
||||
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 }}/
|
||||
- https://ipfs.infura.io/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
- https://ipfs.io/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
|
||||
${{ steps.bump_version.outputs.changelog }}
|
||||
${{ needs.bump_version.outputs.changelog }}
|
||||
6
.github/workflows/tests.yaml
vendored
6
.github/workflows/tests.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn
|
||||
- run: yarn install
|
||||
- run: yarn integration-test
|
||||
|
||||
unit-tests:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn
|
||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
||||
- run: yarn test
|
||||
|
||||
lint:
|
||||
@@ -70,6 +70,6 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn
|
||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
||||
- run: yarn lint
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -1,9 +1,7 @@
|
||||
# Uniswap Frontend
|
||||
|
||||
[](https://app.netlify.com/sites/uniswap/deploys)
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](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
|
||||
|
||||
@@ -46,7 +43,7 @@ change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETW
|
||||
|
||||
Note that the front end only works properly on testnets where both
|
||||
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
|
||||
[eth-scan](https://github.com/MyCryptoHQ/eth-scan) are deployed.
|
||||
[multicall](https://github.com/makerdao/multicall) are deployed.
|
||||
The frontend will not work on other networks.
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
"@ethersproject/units": "^5.0.0-beta.132",
|
||||
"@ethersproject/wallet": "^5.0.0-beta.141",
|
||||
"@material-ui/core": "^4.9.5",
|
||||
"@mycrypto/eth-scan": "^2.1.0",
|
||||
"@popperjs/core": "^2.4.0",
|
||||
"@reach/dialog": "^0.2.8",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@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",
|
||||
@@ -54,6 +55,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",
|
||||
@@ -76,7 +78,6 @@
|
||||
"serve": "^11.3.0",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"styled-components": "^4.2.0",
|
||||
"swr": "0.1.18",
|
||||
"typescript": "^3.8.3",
|
||||
"use-media": "^1.4.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useCopyClipboard } from '../../hooks'
|
||||
import useCopyClipboard from '../../hooks/useCopyClipboard'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { CheckCircle, Copy } from 'react-feather'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import { useActiveWeb3React, useDebounce } from '../../hooks'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { Link, TYPE } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { useRef, useEffect } from 'react'
|
||||
import { Info, BookOpen, Code, PieChart, MessageCircle } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { useToggle } from '../../hooks'
|
||||
|
||||
const StyledMenuIcon = styled(MenuIcon)`
|
||||
path {
|
||||
|
||||
@@ -9,9 +9,9 @@ import '@reach/dialog/styles.css'
|
||||
import { transparentize } from 'polished'
|
||||
import { useGesture } from 'react-use-gesture'
|
||||
|
||||
// errors emitted, fix with https://github.com/styled-components/styled-components/pull/3006
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)`
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverlay {...rest} />)<{ mobile: boolean }>`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
@@ -41,7 +41,9 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)`
|
||||
|
||||
// destructure to not pass custom props to Dialog DOM element
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => <DialogContent {...rest} />)`
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
|
||||
<DialogContent aria-label="content" {...rest} />
|
||||
))`
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
border: 1px solid ${({ theme }) => theme.bg1};
|
||||
@@ -95,7 +97,7 @@ interface ModalProps {
|
||||
onDismiss: () => void
|
||||
minHeight?: number | false
|
||||
maxHeight?: number
|
||||
initialFocusRef?: React.Ref<any>
|
||||
initialFocusRef?: React.RefObject<any>
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -145,7 +147,7 @@ export default function Modal({
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile}
|
||||
mobile={true}
|
||||
>
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
@@ -163,6 +165,7 @@ export default function Modal({
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
ariaLabel="test"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
@@ -191,15 +194,9 @@ export default function Modal({
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile ? isMobile : undefined}
|
||||
mobile={false}
|
||||
>
|
||||
<StyledDialogContent
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
mobile={isMobile ? isMobile : undefined}
|
||||
>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
|
||||
@@ -3,14 +3,13 @@ import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom'
|
||||
import useBodyKeyDown from '../../hooks/useBodyKeyDown'
|
||||
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RowBetween } from '../Row'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
|
||||
import { useBodyKeyDown } from '../../hooks'
|
||||
|
||||
const tabOrder = [
|
||||
{
|
||||
path: '/swap',
|
||||
@@ -110,8 +109,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,9 +1,10 @@
|
||||
import { Placement } from '@popperjs/core'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { usePopper } from 'react-popper'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import Portal from '@reach/portal'
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
@@ -26,7 +27,6 @@ const fadeOut = keyframes`
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
position: relative;
|
||||
z-index: 9999;
|
||||
|
||||
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
|
||||
@@ -45,7 +45,6 @@ const ReferenceElement = styled.div`
|
||||
`
|
||||
|
||||
const Arrow = styled.div`
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 9998;
|
||||
@@ -98,42 +97,39 @@ const Arrow = styled.div`
|
||||
|
||||
export interface PopoverProps {
|
||||
content: React.ReactNode
|
||||
showPopup: boolean
|
||||
show: boolean
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
export default function Popover({ content, showPopup, children }: PopoverProps) {
|
||||
export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) {
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>(null)
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>(null)
|
||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement>(null)
|
||||
const { styles, update, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'auto',
|
||||
placement,
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset: [8, 8] } },
|
||||
{ name: 'arrow', options: { element: arrowElement } }
|
||||
]
|
||||
})
|
||||
|
||||
const portal = createPortal(
|
||||
<PopoverContainer show={showPopup} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
{content}
|
||||
<Arrow
|
||||
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||
ref={setArrowElement}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
</PopoverContainer>,
|
||||
document.getElementById('popover-container')
|
||||
)
|
||||
|
||||
useInterval(update, showPopup ? 100 : null)
|
||||
useInterval(update, show ? 100 : null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
|
||||
{portal}
|
||||
<Portal>
|
||||
<PopoverContainer show={show} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
{content}
|
||||
<Arrow
|
||||
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||
ref={setArrowElement}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
</PopoverContainer>
|
||||
</Portal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { HelpCircle as Question } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import Tooltip from '../Tooltip'
|
||||
@@ -23,22 +23,15 @@ const QuestionWrapper = styled.div`
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
|
||||
const [showPopup, setShowPopup] = useState<boolean>(false)
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
|
||||
return (
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
<Tooltip text={text} showPopup={showPopup && !disabled}>
|
||||
<QuestionWrapper
|
||||
onClick={() => {
|
||||
setShowPopup(true)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setShowPopup(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setShowPopup(false)
|
||||
}}
|
||||
>
|
||||
<Tooltip text={text} show={show && !disabled}>
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Question size={16} />
|
||||
</QuestionWrapper>
|
||||
</Tooltip>
|
||||
|
||||
@@ -18,19 +18,14 @@ export const FilterWrapper = styled(RowFixed)`
|
||||
`
|
||||
|
||||
export default function SortButton({
|
||||
title,
|
||||
toggleSortOrder,
|
||||
ascending
|
||||
}: {
|
||||
title: string
|
||||
toggleSortOrder: () => void
|
||||
ascending: boolean
|
||||
}) {
|
||||
return (
|
||||
<FilterWrapper onClick={toggleSortOrder}>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{ascending ? '↑' : '↓'}
|
||||
</Text>
|
||||
|
||||
@@ -57,7 +57,7 @@ function SearchModal({
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH()[account] ?? {}
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
@@ -155,7 +155,8 @@ function SearchModal({
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
showPopup={tooltipOpen}
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type={'text'}
|
||||
@@ -175,11 +176,7 @@ function SearchModal({
|
||||
{isTokenView ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
{isTokenView && (
|
||||
<SortButton
|
||||
ascending={invertSearchOrder}
|
||||
toggleSortOrder={() => setInvertSearchOrder(iso => !iso)}
|
||||
title="Your Balances"
|
||||
/>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
)}
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
@@ -210,7 +207,7 @@ function SearchModal({
|
||||
<div>
|
||||
{isTokenView ? (
|
||||
<Text fontWeight={500} color={theme.text2} fontSize={14}>
|
||||
<StyledLink onClick={openTooltip}>Having trouble importing a token?</StyledLink>
|
||||
<StyledLink onClick={openTooltip}>Having trouble finding a token?</StyledLink>
|
||||
</Text>
|
||||
) : (
|
||||
<Text fontWeight={500}>
|
||||
|
||||
@@ -42,10 +42,10 @@ function getTokenComparator(
|
||||
}
|
||||
|
||||
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const weth = WETH[chainId]
|
||||
const balances = useAllTokenBalancesTreatingWETHasETH()
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances[account] ?? {}), [account, balances, weth])
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
|
||||
return useMemo(() => {
|
||||
if (inverted) {
|
||||
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import styled from 'styled-components'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { URI_AVAILABLE } from '@web3-react/walletconnect-connector'
|
||||
import usePrevious from '../../hooks/usePrevious'
|
||||
import { useWalletModalOpen, useWalletModalToggle } from '../../state/application/hooks'
|
||||
|
||||
import Modal from '../Modal'
|
||||
@@ -11,7 +12,6 @@ import AccountDetails from '../AccountDetails'
|
||||
import PendingView from './PendingView'
|
||||
import Option from './Option'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { usePrevious } from '../../hooks'
|
||||
import { Link } from '../../theme'
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
|
||||
import { darken, lighten } from 'polished'
|
||||
import { Activity } from 'react-feather'
|
||||
import useENSName from '../../hooks/useENSName'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { TransactionDetails } from '../../state/transactions/reducer'
|
||||
|
||||
@@ -19,7 +20,6 @@ import { Spinner } from '../../theme'
|
||||
import LightCircle from '../../assets/svg/lightcircle.svg'
|
||||
|
||||
import { RowBetween } from '../Row'
|
||||
import { useENSName } from '../../hooks'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
|
||||
@@ -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)`
|
||||
|
||||
6
src/constants/abis/erc20.ts
Normal file
6
src/constants/abis/erc20.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import ERC20_ABI from './erc20.json'
|
||||
|
||||
const ERC20_INTERFACE = new Interface(ERC20_ABI)
|
||||
|
||||
export default ERC20_INTERFACE
|
||||
143
src/constants/multicall/abi.json
Normal file
143
src/constants/multicall/abi.json
Normal file
@@ -0,0 +1,143 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getCurrentBlockTimestamp",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "timestamp",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "callData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "calls",
|
||||
"type": "tuple[]"
|
||||
}
|
||||
],
|
||||
"name": "aggregate",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "blockNumber",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "returnData",
|
||||
"type": "bytes[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getLastBlockHash",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "blockHash",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "addr",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "getEthBalance",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getCurrentBlockDifficulty",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "difficulty",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getCurrentBlockGasLimit",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "gaslimit",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getCurrentBlockCoinbase",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "coinbase",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "blockNumber",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getBlockHash",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "blockHash",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
12
src/constants/multicall/index.ts
Normal file
12
src/constants/multicall/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import MULTICALL_ABI from './abi.json'
|
||||
|
||||
const MULTICALL_NETWORKS: { [chainId in ChainId]: string } = {
|
||||
[ChainId.MAINNET]: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441',
|
||||
[ChainId.ROPSTEN]: '0x53C43764255c17BD724F74c4eF150724AC50a3ed',
|
||||
[ChainId.KOVAN]: '0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A',
|
||||
[ChainId.RINKEBY]: '0x42Ad527de7d4e9d9d011aC45B31D8551f8Fe9821',
|
||||
[ChainId.GÖRLI]: '0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e'
|
||||
}
|
||||
|
||||
export { MULTICALL_ABI, MULTICALL_NETWORKS }
|
||||
@@ -1,26 +1,17 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Token, TokenAmount } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
|
||||
import { useTokenContract } from '../hooks'
|
||||
import { useTokenContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
|
||||
function getTokenAllowance(contract: Contract, token: Token): (owner: string, spender: string) => Promise<TokenAmount> {
|
||||
return async (owner: string, spender: string): Promise<TokenAmount> =>
|
||||
contract
|
||||
.allowance(owner, spender)
|
||||
.then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString()))
|
||||
}
|
||||
|
||||
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount {
|
||||
export function useTokenAllowance(token?: Token, owner?: string, spender?: string): TokenAmount | undefined {
|
||||
const contract = useTokenContract(token?.address, false)
|
||||
|
||||
const shouldFetch = !!contract && typeof owner === 'string' && typeof spender === 'string'
|
||||
const { data, mutate } = useSWR(
|
||||
shouldFetch ? [owner, spender, token.address, token.chainId, SWRKeys.Allowances] : null,
|
||||
getTokenAllowance(contract, token)
|
||||
)
|
||||
useKeepSWRDataLiveAsBlocksArrive(mutate)
|
||||
const inputs = useMemo(() => [owner, spender], [owner, spender])
|
||||
const allowance = useSingleCallResult(contract, 'allowance', inputs)
|
||||
|
||||
return data
|
||||
return useMemo(() => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), [
|
||||
token,
|
||||
allowance
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
|
||||
import { usePairContract } from '../hooks'
|
||||
|
||||
function getReserves(contract: Contract, tokenA: Token, tokenB: Token): () => Promise<Pair | null> {
|
||||
return async (): Promise<Pair | null> =>
|
||||
contract
|
||||
.getReserves()
|
||||
.then(
|
||||
({ reserve0, reserve1 }: { reserve0: { toString: () => string }; reserve1: { toString: () => string } }) => {
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||
}
|
||||
)
|
||||
.catch(() => {
|
||||
return null
|
||||
})
|
||||
}
|
||||
import { usePairContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
|
||||
/*
|
||||
* if loading, return undefined
|
||||
@@ -26,13 +10,15 @@ function getReserves(contract: Contract, tokenA: Token, tokenB: Token): () => Pr
|
||||
* if pair already created (even if 0 reserves), return pair
|
||||
*/
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
const pairAddress = !!tokenA && !!tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const contract = usePairContract(pairAddress, false)
|
||||
const reserves = useSingleCallResult(contract, 'getReserves')
|
||||
|
||||
const shouldFetch = !!contract
|
||||
const key = shouldFetch ? [pairAddress, tokenA.chainId, SWRKeys.Reserves] : null
|
||||
const { data, mutate } = useSWR(key, getReserves(contract, tokenA, tokenB))
|
||||
useKeepSWRDataLiveAsBlocksArrive(mutate)
|
||||
|
||||
return data
|
||||
return useMemo(() => {
|
||||
if (!pairAddress || !contract || !tokenA || !tokenB) return undefined
|
||||
if (!reserves) return null
|
||||
const { reserve0, reserve1 } = reserves
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
|
||||
}, [contract, pairAddress, reserves, tokenA, tokenB])
|
||||
}
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Token, TokenAmount } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { useTokenContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
|
||||
import { SWRKeys, useKeepSWRDataLiveAsBlocksArrive } from '.'
|
||||
import { useTokenContract } from '../hooks'
|
||||
|
||||
function getTotalSupply(contract: Contract, token: Token): () => Promise<TokenAmount> {
|
||||
return async (): Promise<TokenAmount> =>
|
||||
contract
|
||||
.totalSupply()
|
||||
.then((totalSupply: { toString: () => string }) => new TokenAmount(token, totalSupply.toString()))
|
||||
}
|
||||
|
||||
export function useTotalSupply(token?: Token): TokenAmount {
|
||||
// returns undefined if input token is undefined, or fails to get token contract,
|
||||
// or contract total supply cannot be fetched
|
||||
export function useTotalSupply(token?: Token): TokenAmount | undefined {
|
||||
const contract = useTokenContract(token?.address, false)
|
||||
|
||||
const shouldFetch = !!contract
|
||||
const { data, mutate } = useSWR(
|
||||
shouldFetch ? [token.address, token.chainId, SWRKeys.TotalSupply] : null,
|
||||
getTotalSupply(contract, token)
|
||||
)
|
||||
useKeepSWRDataLiveAsBlocksArrive(mutate)
|
||||
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.[0]
|
||||
|
||||
return data
|
||||
return token && totalSupply ? new TokenAmount(token, totalSupply.toString()) : undefined
|
||||
}
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Token, TokenAmount, Pair, Trade, ChainId, WETH, Route, TradeType, Percent } from '@uniswap/sdk'
|
||||
import useSWR from 'swr'
|
||||
import { ChainId, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../hooks'
|
||||
import { useV1FactoryContract } from '../hooks'
|
||||
import { SWRKeys } from '.'
|
||||
import { useETHBalances, useTokenBalances } from '../state/wallet/hooks'
|
||||
|
||||
function getV1PairAddress(contract: Contract): (tokenAddress: string) => Promise<string> {
|
||||
return async (tokenAddress: string): Promise<string> => contract.getExchange(tokenAddress)
|
||||
}
|
||||
|
||||
function useV1PairAddress(tokenAddress: string) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
import { useV1FactoryContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useETHBalances, useTokenBalance } from '../state/wallet/hooks'
|
||||
|
||||
function useV1PairAddress(tokenAddress?: string): string | undefined {
|
||||
const contract = useV1FactoryContract()
|
||||
|
||||
const shouldFetch = chainId === ChainId.MAINNET && typeof tokenAddress === 'string' && !!contract
|
||||
const { data } = useSWR(shouldFetch ? [tokenAddress, SWRKeys.V1PairAddress] : null, getV1PairAddress(contract), {
|
||||
// don't need to update this data
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false
|
||||
})
|
||||
|
||||
return data
|
||||
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
|
||||
return useSingleCallResult(contract, 'getExchange', inputs)?.[0]
|
||||
}
|
||||
|
||||
function useMockV1Pair(token?: Token) {
|
||||
@@ -30,37 +17,35 @@ function useMockV1Pair(token?: Token) {
|
||||
|
||||
// will only return an address on mainnet, and not for WETH
|
||||
const v1PairAddress = useV1PairAddress(isWETH ? undefined : token?.address)
|
||||
const tokenBalance = useTokenBalances(v1PairAddress, [token])[token?.address]
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress]
|
||||
const tokenBalance = useTokenBalance(v1PairAddress, token)
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
||||
|
||||
return tokenBalance && ETHBalance
|
||||
? new Pair(tokenBalance, new TokenAmount(WETH[token?.chainId], ETHBalance.toString()))
|
||||
return tokenBalance && ETHBalance && token
|
||||
? new Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
|
||||
: undefined
|
||||
}
|
||||
|
||||
export function useV1TradeLinkIfBetter(
|
||||
isExactIn?: boolean,
|
||||
inputToken?: Token,
|
||||
outputToken?: Token,
|
||||
input?: Token,
|
||||
output?: Token,
|
||||
exactAmount?: TokenAmount,
|
||||
v2Trade?: Trade,
|
||||
minimumDelta: Percent = new Percent('0')
|
||||
): string {
|
||||
): string | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const input = inputToken
|
||||
const output = outputToken
|
||||
const mainnet = chainId === ChainId.MAINNET
|
||||
const isMainnet: boolean = chainId === ChainId.MAINNET
|
||||
|
||||
// get the mock v1 pairs
|
||||
const inputPair = useMockV1Pair(input)
|
||||
const outputPair = useMockV1Pair(output)
|
||||
|
||||
const inputIsWETH = mainnet && input?.equals(WETH[ChainId.MAINNET])
|
||||
const outputIsWETH = mainnet && output?.equals(WETH[ChainId.MAINNET])
|
||||
const inputIsWETH = isMainnet && input?.equals(WETH[ChainId.MAINNET])
|
||||
const outputIsWETH = isMainnet && output?.equals(WETH[ChainId.MAINNET])
|
||||
|
||||
// construct a direct or through ETH v1 route
|
||||
let pairs: Pair[]
|
||||
let pairs: Pair[] = []
|
||||
if (inputIsWETH && outputPair) {
|
||||
pairs = [outputPair]
|
||||
} else if (outputIsWETH && inputPair) {
|
||||
@@ -71,8 +56,8 @@ export function useV1TradeLinkIfBetter(
|
||||
pairs = [inputPair, outputPair]
|
||||
}
|
||||
|
||||
const route = pairs && new Route(pairs, input)
|
||||
let v1Trade: Trade
|
||||
const route = input && pairs && pairs.length > 0 && new Route(pairs, input)
|
||||
let v1Trade: Trade | undefined
|
||||
try {
|
||||
v1Trade =
|
||||
route && exactAmount
|
||||
@@ -86,16 +71,16 @@ export function useV1TradeLinkIfBetter(
|
||||
// discount the v1 output amount by minimumDelta
|
||||
const discountedV1Output = v1Trade?.outputAmount.multiply(new Percent('1').subtract(minimumDelta))
|
||||
// check if the discounted v1 amount is still greater than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !!!v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
||||
v1HasBetterTrade = !v2Trade || discountedV1Output.greaterThan(v2Trade.outputAmount)
|
||||
} else {
|
||||
// inflate the v1 amount by minimumDelta
|
||||
const inflatedV1Input = v1Trade?.inputAmount.multiply(new Percent('1').add(minimumDelta))
|
||||
// check if the inflated v1 amount is still less than v2, short-circuiting if no v2 trade exists
|
||||
v1HasBetterTrade = !!!v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
||||
v1HasBetterTrade = !v2Trade || inflatedV1Input.lessThan(v2Trade.inputAmount)
|
||||
}
|
||||
}
|
||||
|
||||
return v1HasBetterTrade
|
||||
return v1HasBetterTrade && input && output
|
||||
? `https://v1.uniswap.exchange/swap?inputCurrency=${inputIsWETH ? 'ETH' : input.address}&outputCurrency=${
|
||||
outputIsWETH ? 'ETH' : output.address
|
||||
}`
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { responseInterface } from 'swr'
|
||||
|
||||
import { useBlockNumber } from '../state/application/hooks'
|
||||
|
||||
export enum SWRKeys {
|
||||
Allowances,
|
||||
Reserves,
|
||||
TotalSupply,
|
||||
V1PairAddress
|
||||
}
|
||||
|
||||
export function useKeepSWRDataLiveAsBlocksArrive(mutate: responseInterface<any, any>['mutate']) {
|
||||
// because we don't care about the referential identity of mutate, just bind it to a ref
|
||||
const mutateRef = useRef(mutate)
|
||||
useEffect(() => {
|
||||
mutateRef.current = mutate
|
||||
})
|
||||
// then, whenever a new block arrives, trigger a mutation
|
||||
const blockNumber = useBlockNumber()
|
||||
useEffect(() => {
|
||||
mutateRef.current()
|
||||
}, [blockNumber])
|
||||
}
|
||||
4
src/data/tsconfig.json
Normal file
4
src/data/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
|
||||
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
import IUniswapV1Factory from '../constants/abis/v1_factory.json'
|
||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { injected } from '../connectors'
|
||||
import { NetworkContextName, V1_FACTORY_ADDRESS } from '../constants'
|
||||
import { getContract, isAddress } from '../utils'
|
||||
import { NetworkContextName } from '../constants'
|
||||
|
||||
export function useActiveWeb3React() {
|
||||
const context = useWeb3ReactCore<Web3Provider>()
|
||||
@@ -64,7 +57,7 @@ export function useInactiveListener(suppress = false) {
|
||||
const handleChainChanged = () => {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(error => {
|
||||
console.log(error)
|
||||
console.error('Failed to activate after chain changed', error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +65,7 @@ export function useInactiveListener(suppress = false) {
|
||||
if (accounts.length > 0) {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(error => {
|
||||
console.log(error)
|
||||
console.error('Failed to activate after accounts changed', error)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,7 +73,7 @@ export function useInactiveListener(suppress = false) {
|
||||
const handleNetworkChanged = () => {
|
||||
// eat errors
|
||||
activate(injected, undefined, true).catch(error => {
|
||||
console.log(error)
|
||||
console.error('Failed to activate after networks changed', error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -99,157 +92,3 @@ export function useInactiveListener(suppress = false) {
|
||||
return
|
||||
}, [active, error, suppress, activate])
|
||||
}
|
||||
|
||||
// modified from https://usehooks.com/useDebounce/
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
// modified from https://usehooks.com/useKeyPress/
|
||||
export function useBodyKeyDown(targetKey: string, onKeyDown: () => void, suppressOnKeyDown = false) {
|
||||
const downHandler = useCallback(
|
||||
event => {
|
||||
const {
|
||||
target: { tagName },
|
||||
key
|
||||
} = event
|
||||
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
|
||||
event.preventDefault()
|
||||
onKeyDown()
|
||||
}
|
||||
},
|
||||
[targetKey, onKeyDown, suppressOnKeyDown]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', downHandler)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', downHandler)
|
||||
}
|
||||
}, [downHandler])
|
||||
}
|
||||
|
||||
export function useENSName(address?: string): string | null {
|
||||
const { library } = useActiveWeb3React()
|
||||
|
||||
const [ENSName, setENSName] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || !address) return
|
||||
if (isAddress(address)) {
|
||||
let stale = false
|
||||
library
|
||||
.lookupAddress(address)
|
||||
.then(name => {
|
||||
if (!stale) {
|
||||
if (name) {
|
||||
setENSName(name)
|
||||
} else {
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setENSName(null)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [library, address])
|
||||
|
||||
return ENSName
|
||||
}
|
||||
|
||||
// returns null on errors
|
||||
function useContract(address?: string, ABI?: any, withSignerIfPossible = true): Contract | null {
|
||||
const { library, account } = useActiveWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!address || !ABI || !library) return null
|
||||
try {
|
||||
return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [address, ABI, library, withSignerIfPossible, account])
|
||||
}
|
||||
|
||||
export function useV1FactoryContract(): Contract | null {
|
||||
return useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
|
||||
}
|
||||
|
||||
// returns null on errors
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useCopyClipboard(timeout = 500): [boolean, (toCopy: string) => void] {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const staticCopy = useCallback(text => {
|
||||
const didCopy = copy(text)
|
||||
setIsCopied(didCopy)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied) {
|
||||
const hide = setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, timeout)
|
||||
|
||||
return () => {
|
||||
clearTimeout(hide)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [isCopied, setIsCopied, timeout])
|
||||
|
||||
return [isCopied, staticCopy]
|
||||
}
|
||||
|
||||
// modified from https://usehooks.com/usePrevious/
|
||||
export function usePrevious<T>(value: T) {
|
||||
// The ref object is a generic container whose current property is mutable ...
|
||||
// ... and can hold any value, similar to an instance property on a class
|
||||
const ref = useRef<T>()
|
||||
|
||||
// Store current value in ref
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value]) // Only re-run if value changes
|
||||
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current
|
||||
}
|
||||
|
||||
export function useToggle(initialState = false): [boolean, () => void] {
|
||||
const [state, setState] = useState(initialState)
|
||||
const toggle = useCallback(() => setState(state => !state), [])
|
||||
return [state, toggle]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder, useHasPendingApproval } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin } from '../utils'
|
||||
import { useTokenContract, useActiveWeb3React } from './index'
|
||||
import { useTokenContract } from './useContract'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
export enum ApprovalState {
|
||||
UNKNOWN,
|
||||
|
||||
25
src/hooks/useBodyKeyDown.ts
Normal file
25
src/hooks/useBodyKeyDown.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
// modified from https://usehooks.com/useKeyPress/
|
||||
export default function useBodyKeyDown(targetKey: string, onKeyDown: () => void, suppressOnKeyDown = false) {
|
||||
const downHandler = useCallback(
|
||||
event => {
|
||||
const {
|
||||
target: { tagName },
|
||||
key
|
||||
} = event
|
||||
if (key === targetKey && tagName === 'BODY' && !suppressOnKeyDown) {
|
||||
event.preventDefault()
|
||||
onKeyDown()
|
||||
}
|
||||
},
|
||||
[targetKey, onKeyDown, suppressOnKeyDown]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', downHandler)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', downHandler)
|
||||
}
|
||||
}, [downHandler])
|
||||
}
|
||||
42
src/hooks/useContract.ts
Normal file
42
src/hooks/useContract.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { useMemo } from 'react'
|
||||
import { V1_FACTORY_ADDRESS } from '../constants'
|
||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||
import IUniswapV1Factory from '../constants/abis/v1_factory.json'
|
||||
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
|
||||
import { getContract } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
// returns null on errors
|
||||
function useContract(address?: string, ABI?: any, withSignerIfPossible = true): Contract | null {
|
||||
const { library, account } = useActiveWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!address || !ABI || !library) return null
|
||||
try {
|
||||
return getContract(address, ABI, library, withSignerIfPossible && account ? account : undefined)
|
||||
} catch (error) {
|
||||
console.error('Failed to get contract', error)
|
||||
return null
|
||||
}
|
||||
}, [address, ABI, library, withSignerIfPossible, account])
|
||||
}
|
||||
|
||||
export function useV1FactoryContract(): Contract | null {
|
||||
return useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
|
||||
}
|
||||
|
||||
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
|
||||
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useMulticallContract(): Contract | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
return useContract(MULTICALL_NETWORKS[chainId as ChainId], MULTICALL_ABI, false)
|
||||
}
|
||||
26
src/hooks/useCopyClipboard.ts
Normal file
26
src/hooks/useCopyClipboard.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export default function useCopyClipboard(timeout = 500): [boolean, (toCopy: string) => void] {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const staticCopy = useCallback(text => {
|
||||
const didCopy = copy(text)
|
||||
setIsCopied(didCopy)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied) {
|
||||
const hide = setTimeout(() => {
|
||||
setIsCopied(false)
|
||||
}, timeout)
|
||||
|
||||
return () => {
|
||||
clearTimeout(hide)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [isCopied, setIsCopied, timeout])
|
||||
|
||||
return [isCopied, staticCopy]
|
||||
}
|
||||
22
src/hooks/useDebounce.ts
Normal file
22
src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// modified from https://usehooks.com/useDebounce/
|
||||
export default function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
45
src/hooks/useENSName.ts
Normal file
45
src/hooks/useENSName.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isAddress } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
/**
|
||||
* Does a reverse lookup for an address to find its ENS name.
|
||||
* Note this is not the same as looking up an ENS name to find an address.
|
||||
*/
|
||||
export default function useENSName(address?: string): string | null {
|
||||
const { library } = useActiveWeb3React()
|
||||
|
||||
const [ENSName, setENSName] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || !address) return
|
||||
const validated = isAddress(address)
|
||||
if (validated) {
|
||||
let stale = false
|
||||
library
|
||||
.lookupAddress(validated)
|
||||
.then(name => {
|
||||
if (!stale) {
|
||||
if (name) {
|
||||
setENSName(name)
|
||||
} else {
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setENSName(null)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
setENSName(null)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [library, address])
|
||||
|
||||
return ENSName
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export default function useInterval(callback: () => void, delay: null | number)
|
||||
}
|
||||
|
||||
if (delay !== null) {
|
||||
tick()
|
||||
const id = setInterval(tick, delay)
|
||||
return () => clearInterval(id)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
16
src/hooks/usePrevious.ts
Normal file
16
src/hooks/usePrevious.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
// modified from https://usehooks.com/usePrevious/
|
||||
export default function usePrevious<T>(value: T) {
|
||||
// The ref object is a generic container whose current property is mutable ...
|
||||
// ... and can hold any value, similar to an instance property on a class
|
||||
const ref = useRef<T>()
|
||||
|
||||
// Store current value in ref
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value]) // Only re-run if value changes
|
||||
|
||||
// Return previous value (happens before update in useEffect above)
|
||||
return ref.current
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../state/wallet/hooks'
|
||||
|
||||
import { calculateGasMargin, getSigner, isAddress } from '../utils'
|
||||
import { useENSName, useTokenContract, useActiveWeb3React } from './index'
|
||||
import { useTokenContract } from './useContract'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import useENSName from './useENSName'
|
||||
|
||||
// returns a callback for sending a token amount, treating WETH as ETH
|
||||
// returns null with invalid arguments
|
||||
|
||||
@@ -8,7 +8,8 @@ import { Field } from '../state/swap/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { computeSlippageAdjustedAmounts } from '../utils/prices'
|
||||
import { calculateGasMargin, getRouterContract, isAddress } from '../utils'
|
||||
import { useENSName, useActiveWeb3React } from './index'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import useENSName from './useENSName'
|
||||
|
||||
enum SwapType {
|
||||
EXACT_TOKENS_FOR_TOKENS,
|
||||
|
||||
7
src/hooks/useToggle.ts
Normal file
7
src/hooks/useToggle.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
export default function useToggle(initialState = false): [boolean, () => void] {
|
||||
const [state, setState] = useState(initialState)
|
||||
const toggle = useCallback(() => setState(state => !state), [])
|
||||
return [state, toggle]
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import store from './state'
|
||||
import ApplicationUpdater from './state/application/updater'
|
||||
import TransactionUpdater from './state/transactions/updater'
|
||||
import UserUpdater from './state/user/updater'
|
||||
import WalletUpdater from './state/wallet/updater'
|
||||
import MulticallUpdater from './state/multicall/updater'
|
||||
import ThemeProvider, { FixedGlobalStyle, ThemedGlobalStyle } from './theme'
|
||||
|
||||
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
|
||||
@@ -37,7 +37,7 @@ function Updaters() {
|
||||
<UserUpdater />
|
||||
<ApplicationUpdater />
|
||||
<TransactionUpdater />
|
||||
<WalletUpdater />
|
||||
<MulticallUpdater />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -245,7 +251,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
|
||||
const swapState = useSwapState()
|
||||
function _onTokenSelect(address: string) {
|
||||
// if no user balance - switch view to a send with swap
|
||||
const hasBalance = allBalances?.[account]?.[address]?.greaterThan('0') ?? false
|
||||
const hasBalance = allBalances?.[address]?.greaterThan('0') ?? false
|
||||
if (!hasBalance) {
|
||||
onTokenSelection(
|
||||
Field.INPUT,
|
||||
@@ -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,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDebounce, useActiveWeb3React } from '../../hooks'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
|
||||
import { updateBlockNumber } from './actions'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
@@ -7,6 +9,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 +41,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,12 +1,16 @@
|
||||
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
|
||||
import application from './application/reducer'
|
||||
import { updateVersion } from './user/actions'
|
||||
import user from './user/reducer'
|
||||
import wallet from './wallet/reducer'
|
||||
import swap from './swap/reducer'
|
||||
import transactions from './transactions/reducer'
|
||||
import { save, load } from 'redux-localstorage-simple'
|
||||
|
||||
import application from './application/reducer'
|
||||
import user from './user/reducer'
|
||||
import transactions from './transactions/reducer'
|
||||
import swap from './swap/reducer'
|
||||
import mint from './mint/reducer'
|
||||
import burn from './burn/reducer'
|
||||
import multicall from './multicall/reducer'
|
||||
|
||||
import { updateVersion } from './user/actions'
|
||||
|
||||
const PERSISTED_KEYS: string[] = ['user', 'transactions']
|
||||
|
||||
const store = configureStore({
|
||||
@@ -14,8 +18,10 @@ const store = configureStore({
|
||||
application,
|
||||
user,
|
||||
transactions,
|
||||
wallet,
|
||||
swap
|
||||
swap,
|
||||
mint,
|
||||
burn,
|
||||
multicall
|
||||
},
|
||||
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: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
41
src/state/multicall/actions.ts
Normal file
41
src/state/multicall/actions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
import { isAddress } from '../../utils'
|
||||
|
||||
export interface Call {
|
||||
address: string
|
||||
callData: string
|
||||
}
|
||||
|
||||
export function toCallKey(call: Call): string {
|
||||
return `${call.address}-${call.callData}`
|
||||
}
|
||||
|
||||
export function parseCallKey(callKey: string): Call {
|
||||
const pcs = callKey.split('-')
|
||||
if (pcs.length !== 2) {
|
||||
throw new Error(`Invalid call key: ${callKey}`)
|
||||
}
|
||||
const addr = isAddress(pcs[0])
|
||||
if (!addr) {
|
||||
throw new Error(`Invalid address: ${pcs[0]}`)
|
||||
}
|
||||
|
||||
if (!pcs[1].match(/^0x[a-fA-F0-9]*$/)) {
|
||||
throw new Error(`Invalid hex: ${pcs[1]}`)
|
||||
}
|
||||
|
||||
return {
|
||||
address: pcs[0],
|
||||
callData: pcs[1]
|
||||
}
|
||||
}
|
||||
|
||||
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('addMulticallListeners')
|
||||
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('removeMulticallListeners')
|
||||
export const updateMulticallResults = createAction<{
|
||||
chainId: number
|
||||
blockNumber: number
|
||||
results: {
|
||||
[callKey: string]: string | null
|
||||
}
|
||||
}>('updateMulticallResults')
|
||||
174
src/state/multicall/hooks.ts
Normal file
174
src/state/multicall/hooks.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { addMulticallListeners, Call, removeMulticallListeners, parseCallKey, toCallKey } from './actions'
|
||||
|
||||
export interface Result extends ReadonlyArray<any> {
|
||||
readonly [key: string]: any
|
||||
}
|
||||
|
||||
type MethodArg = string | number | BigNumber
|
||||
type MethodArgs = Array<MethodArg | MethodArg[]>
|
||||
|
||||
type OptionalMethodInputs = Array<MethodArg | MethodArg[] | undefined> | undefined
|
||||
|
||||
function isMethodArg(x: unknown): x is MethodArg {
|
||||
return ['string', 'number'].indexOf(typeof x) !== -1
|
||||
}
|
||||
|
||||
function isValidMethodArgs(x: unknown): x is MethodArgs | undefined {
|
||||
return (
|
||||
x === undefined || (Array.isArray(x) && x.every(y => isMethodArg(y) || (Array.isArray(y) && y.every(isMethodArg))))
|
||||
)
|
||||
}
|
||||
|
||||
// the lowest level call for subscribing to contract data
|
||||
function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const callResults = useSelector<AppState, AppState['multicall']['callResults']>(state => state.multicall.callResults)
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const serializedCallKeys: string = useMemo(
|
||||
() =>
|
||||
JSON.stringify(
|
||||
calls
|
||||
?.filter((c): c is Call => Boolean(c))
|
||||
?.map(toCallKey)
|
||||
?.sort() ?? []
|
||||
),
|
||||
[calls]
|
||||
)
|
||||
|
||||
const debouncedSerializedCallKeys = useDebounce(serializedCallKeys, 20)
|
||||
|
||||
// update listeners when there is an actual change that persists for at least 100ms
|
||||
useEffect(() => {
|
||||
const callKeys: string[] = JSON.parse(debouncedSerializedCallKeys)
|
||||
if (!chainId || callKeys.length === 0) return
|
||||
const calls = callKeys.map(key => parseCallKey(key))
|
||||
dispatch(
|
||||
addMulticallListeners({
|
||||
chainId,
|
||||
calls
|
||||
})
|
||||
)
|
||||
|
||||
return () => {
|
||||
dispatch(
|
||||
removeMulticallListeners({
|
||||
chainId,
|
||||
calls
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [chainId, dispatch, debouncedSerializedCallKeys])
|
||||
|
||||
return useMemo(() => {
|
||||
return calls.map<string | undefined>(call => {
|
||||
if (!chainId || !call) return undefined
|
||||
|
||||
const result = callResults[chainId]?.[toCallKey(call)]
|
||||
if (!result || !result.data || result.data === '0x') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return result.data
|
||||
})
|
||||
}, [callResults, calls, chainId])
|
||||
}
|
||||
|
||||
export function useSingleContractMultipleData(
|
||||
contract: Contract | null | undefined,
|
||||
methodName: string,
|
||||
callInputs: OptionalMethodInputs[]
|
||||
): (Result | undefined)[] {
|
||||
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
|
||||
|
||||
const calls = useMemo(
|
||||
() =>
|
||||
contract && fragment && callInputs && callInputs.length > 0
|
||||
? callInputs.map<Call>(inputs => {
|
||||
return {
|
||||
address: contract.address,
|
||||
callData: contract.interface.encodeFunctionData(fragment, inputs)
|
||||
}
|
||||
})
|
||||
: [],
|
||||
[callInputs, contract, fragment]
|
||||
)
|
||||
|
||||
const data = useCallsData(calls)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!fragment || !contract) return []
|
||||
return data.map(data => (data ? contract.interface.decodeFunctionResult(fragment, data) : undefined))
|
||||
}, [contract, data, fragment])
|
||||
}
|
||||
|
||||
export function useMultipleContractSingleData(
|
||||
addresses: (string | undefined)[],
|
||||
contractInterface: Interface,
|
||||
methodName: string,
|
||||
callInputs?: OptionalMethodInputs
|
||||
): (Result | undefined)[] {
|
||||
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
|
||||
const callData: string | undefined = useMemo(
|
||||
() =>
|
||||
fragment && isValidMethodArgs(callInputs)
|
||||
? contractInterface.encodeFunctionData(fragment, callInputs)
|
||||
: undefined,
|
||||
[callInputs, contractInterface, fragment]
|
||||
)
|
||||
|
||||
const calls = useMemo(
|
||||
() =>
|
||||
fragment && addresses && addresses.length > 0 && callData
|
||||
? addresses.map<Call | undefined>(address => {
|
||||
return address && callData
|
||||
? {
|
||||
address,
|
||||
callData
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
: [],
|
||||
[addresses, callData, fragment]
|
||||
)
|
||||
|
||||
const data = useCallsData(calls)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!fragment) return []
|
||||
return data.map(data => (data ? contractInterface.decodeFunctionResult(fragment, data) : undefined))
|
||||
}, [contractInterface, data, fragment])
|
||||
}
|
||||
|
||||
export function useSingleCallResult(
|
||||
contract: Contract | null | undefined,
|
||||
methodName: string,
|
||||
inputs?: OptionalMethodInputs
|
||||
): Result | undefined {
|
||||
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
|
||||
|
||||
const calls = useMemo<Call[]>(() => {
|
||||
return contract && fragment && isValidMethodArgs(inputs)
|
||||
? [
|
||||
{
|
||||
address: contract.address,
|
||||
callData: contract.interface.encodeFunctionData(fragment, inputs)
|
||||
}
|
||||
]
|
||||
: []
|
||||
}, [contract, fragment, inputs])
|
||||
|
||||
const data = useCallsData(calls)[0]
|
||||
return useMemo(() => {
|
||||
if (!contract || !fragment || !data) return undefined
|
||||
return contract.interface.decodeFunctionResult(fragment, data)
|
||||
}, [data, fragment, contract])
|
||||
}
|
||||
57
src/state/multicall/reducer.ts
Normal file
57
src/state/multicall/reducer.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { addMulticallListeners, removeMulticallListeners, toCallKey, updateMulticallResults } from './actions'
|
||||
|
||||
interface MulticallState {
|
||||
callListeners: {
|
||||
[chainId: number]: {
|
||||
[callKey: string]: number
|
||||
}
|
||||
}
|
||||
|
||||
callResults: {
|
||||
[chainId: number]: {
|
||||
[callKey: string]: {
|
||||
data: string | null
|
||||
blockNumber: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: MulticallState = {
|
||||
callListeners: {},
|
||||
callResults: {}
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(addMulticallListeners, (state, { payload: { calls, chainId } }) => {
|
||||
state.callListeners[chainId] = state.callListeners[chainId] ?? {}
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
state.callListeners[chainId][callKey] = (state.callListeners[chainId][callKey] ?? 0) + 1
|
||||
})
|
||||
})
|
||||
.addCase(removeMulticallListeners, (state, { payload: { chainId, calls } }) => {
|
||||
if (!state.callListeners[chainId]) return
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
if (state.callListeners[chainId][callKey] === 1) {
|
||||
delete state.callListeners[chainId][callKey]
|
||||
} else {
|
||||
state.callListeners[chainId][callKey]--
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(updateMulticallResults, (state, { payload: { chainId, results, blockNumber } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
Object.keys(results).forEach(callKey => {
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (current && current.blockNumber > blockNumber) return
|
||||
state.callResults[chainId][callKey] = {
|
||||
data: results[callKey],
|
||||
blockNumber
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
81
src/state/multicall/updater.tsx
Normal file
81
src/state/multicall/updater.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useMulticallContract } from '../../hooks/useContract'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
import chunkArray from '../../utils/chunkArray'
|
||||
import { useBlockNumber } from '../application/hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { parseCallKey, updateMulticallResults } from './actions'
|
||||
|
||||
// chunk calls so we do not exceed the gas limit
|
||||
const CALL_CHUNK_SIZE = 250
|
||||
|
||||
export default function Updater() {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const state = useSelector<AppState, AppState['multicall']>(state => state.multicall)
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const multicallContract = useMulticallContract()
|
||||
|
||||
const listeningKeys = useMemo(() => {
|
||||
if (!chainId || !state.callListeners[chainId]) return []
|
||||
return Object.keys(state.callListeners[chainId]).filter(callKey => state.callListeners[chainId][callKey] > 0)
|
||||
}, [state.callListeners, chainId])
|
||||
|
||||
const debouncedResults = useDebounce(state.callResults, 20)
|
||||
const debouncedListeningKeys = useDebounce(listeningKeys, 20)
|
||||
|
||||
const unserializedOutdatedCallKeys = useMemo(() => {
|
||||
if (!chainId || !debouncedResults[chainId]) return debouncedListeningKeys
|
||||
if (!latestBlockNumber) return []
|
||||
|
||||
return debouncedListeningKeys.filter(key => {
|
||||
const data = debouncedResults[chainId][key]
|
||||
return !data || data.blockNumber < latestBlockNumber
|
||||
})
|
||||
}, [chainId, debouncedResults, debouncedListeningKeys, latestBlockNumber])
|
||||
|
||||
const serializedOutdatedCallKeys = useMemo(() => JSON.stringify(unserializedOutdatedCallKeys.sort()), [
|
||||
unserializedOutdatedCallKeys
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys)
|
||||
if (!multicallContract || !chainId || outdatedCallKeys.length === 0) return
|
||||
const calls = outdatedCallKeys.map(key => parseCallKey(key))
|
||||
|
||||
const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE)
|
||||
|
||||
console.debug('Firing off chunked calls', chunkedCalls)
|
||||
|
||||
chunkedCalls.forEach((chunk, index) =>
|
||||
multicallContract
|
||||
.aggregate(chunk.map(obj => [obj.address, obj.callData]))
|
||||
.then(([resultsBlockNumber, returnData]: [BigNumber, string[]]) => {
|
||||
// accumulates the length of all previous indices
|
||||
const firstCallKeyIndex = chunkedCalls.slice(0, index).reduce<number>((memo, curr) => memo + curr.length, 0)
|
||||
const lastCallKeyIndex = firstCallKeyIndex + returnData.length
|
||||
|
||||
dispatch(
|
||||
updateMulticallResults({
|
||||
chainId,
|
||||
results: outdatedCallKeys
|
||||
.slice(firstCallKeyIndex, lastCallKeyIndex)
|
||||
.reduce<{ [callKey: string]: string | null }>((memo, callKey, i) => {
|
||||
memo[callKey] = returnData[i] ?? null
|
||||
return memo
|
||||
}, {}),
|
||||
blockNumber: resultsBlockNumber.toNumber()
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Failed to fetch multicall chunk', chunk, chainId, error)
|
||||
})
|
||||
)
|
||||
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface TokenBalanceListenerKey {
|
||||
address: string
|
||||
tokenAddress: string
|
||||
}
|
||||
|
||||
// used by components that care about balances of given tokens and accounts
|
||||
// being kept up to date
|
||||
export const startListeningForTokenBalances = createAction<TokenBalanceListenerKey[]>('startListeningForTokenBalances')
|
||||
export const stopListeningForTokenBalances = createAction<TokenBalanceListenerKey[]>('stopListeningForTokenBalances')
|
||||
export const startListeningForBalance = createAction<{ addresses: string[] }>('startListeningForBalance')
|
||||
export const stopListeningForBalance = createAction<{ addresses: string[] }>('stopListeningForBalance')
|
||||
|
||||
// these are used by the updater to update balances, and can also be used
|
||||
// for optimistic updates, e.g. when a transaction is confirmed that changes the
|
||||
// user's balances or allowances
|
||||
export const updateTokenBalances = createAction<{
|
||||
chainId: number
|
||||
blockNumber: number
|
||||
address: string
|
||||
tokenBalances: {
|
||||
[address: string]: string
|
||||
}
|
||||
}>('updateTokenBalances')
|
||||
|
||||
export const updateEtherBalances = createAction<{
|
||||
chainId: number
|
||||
blockNumber: number
|
||||
etherBalances: {
|
||||
[address: string]: string
|
||||
}
|
||||
}>('updateEtherBalances')
|
||||
@@ -1,65 +1,44 @@
|
||||
import { getAddress } from '@ethersproject/address'
|
||||
import { ChainId, JSBI, Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useMemo } from 'react'
|
||||
import ERC20_INTERFACE from '../../constants/abis/erc20'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useMulticallContract } from '../../hooks/useContract'
|
||||
import { isAddress } from '../../utils'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import {
|
||||
startListeningForBalance,
|
||||
startListeningForTokenBalances,
|
||||
stopListeningForBalance,
|
||||
stopListeningForTokenBalances,
|
||||
TokenBalanceListenerKey
|
||||
} from './actions'
|
||||
import { balanceKey } from './reducer'
|
||||
import { useSingleContractMultipleData, useMultipleContractSingleData } from '../multicall/hooks'
|
||||
|
||||
/**
|
||||
* Returns a map of the given addresses to their eventually consistent ETH balances.
|
||||
*/
|
||||
export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [address: string]: JSBI | undefined } {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const multicallContract = useMulticallContract()
|
||||
|
||||
const addresses: string[] = useMemo(
|
||||
() =>
|
||||
uncheckedAddresses
|
||||
? uncheckedAddresses
|
||||
.filter((a): a is string => isAddress(a) !== false)
|
||||
.map(getAddress)
|
||||
.map(isAddress)
|
||||
.filter((a): a is string => a !== false)
|
||||
.sort()
|
||||
: [],
|
||||
[uncheckedAddresses]
|
||||
)
|
||||
|
||||
// used so that we do a deep comparison in `useEffect`
|
||||
const serializedAddresses = JSON.stringify(addresses)
|
||||
const results = useSingleContractMultipleData(
|
||||
multicallContract,
|
||||
'getEthBalance',
|
||||
addresses.map(address => [address])
|
||||
)
|
||||
|
||||
// add the listeners on mount, remove them on dismount
|
||||
useEffect(() => {
|
||||
const addresses = JSON.parse(serializedAddresses)
|
||||
if (addresses.length === 0) return
|
||||
|
||||
dispatch(startListeningForBalance({ addresses }))
|
||||
return () => {
|
||||
dispatch(stopListeningForBalance({ addresses }))
|
||||
}
|
||||
}, [serializedAddresses, dispatch])
|
||||
|
||||
const rawBalanceMap = useSelector<AppState, AppState['wallet']['balances']>(({ wallet: { balances } }) => balances)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId) return {}
|
||||
return addresses.reduce<{ [address: string]: JSBI }>((map, address) => {
|
||||
const key = balanceKey({ address, chainId })
|
||||
const { value } = rawBalanceMap[key] ?? {}
|
||||
if (value) {
|
||||
map[address] = JSBI.BigInt(value)
|
||||
}
|
||||
return map
|
||||
}, {})
|
||||
}, [chainId, addresses, rawBalanceMap])
|
||||
return useMemo(
|
||||
() =>
|
||||
addresses.reduce<{ [address: string]: JSBI | undefined }>((memo, address, i) => {
|
||||
const value = results?.[i]?.[0]
|
||||
if (value) memo[address] = JSBI.BigInt(value.toString())
|
||||
return memo
|
||||
}, {}),
|
||||
[addresses, results]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,54 +48,29 @@ export function useTokenBalances(
|
||||
address?: string,
|
||||
tokens?: (Token | undefined)[]
|
||||
): { [tokenAddress: string]: TokenAmount | undefined } {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const validTokens: Token[] = useMemo(
|
||||
const validatedTokens: Token[] = useMemo(
|
||||
() => tokens?.filter((t?: Token): t is Token => isAddress(t?.address) !== false) ?? [],
|
||||
[tokens]
|
||||
)
|
||||
|
||||
// used so that we do a deep comparison in `useEffect`
|
||||
const serializedCombos: string = useMemo(() => {
|
||||
return JSON.stringify(
|
||||
!address || validTokens.length === 0
|
||||
? []
|
||||
: validTokens
|
||||
.map(t => t.address)
|
||||
.sort()
|
||||
.map(tokenAddress => ({ address, tokenAddress }))
|
||||
)
|
||||
}, [address, validTokens])
|
||||
const validatedTokenAddresses = useMemo(() => validatedTokens.map(vt => vt.address), [validatedTokens])
|
||||
|
||||
// keep the listeners up to date
|
||||
useEffect(() => {
|
||||
const combos: TokenBalanceListenerKey[] = JSON.parse(serializedCombos)
|
||||
if (combos.length === 0) return
|
||||
const balances = useMultipleContractSingleData(validatedTokenAddresses, ERC20_INTERFACE, 'balanceOf', [address])
|
||||
|
||||
dispatch(startListeningForTokenBalances(combos))
|
||||
return () => {
|
||||
dispatch(stopListeningForTokenBalances(combos))
|
||||
}
|
||||
}, [address, serializedCombos, dispatch])
|
||||
|
||||
const rawBalanceMap = useSelector<AppState, AppState['wallet']['balances']>(({ wallet: { balances } }) => balances)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!address || validTokens.length === 0 || !chainId) {
|
||||
return {}
|
||||
}
|
||||
return (
|
||||
validTokens.reduce<{ [address: string]: TokenAmount }>((map, token) => {
|
||||
const key = balanceKey({ address, chainId, tokenAddress: token.address })
|
||||
const { value } = rawBalanceMap[key] ?? {}
|
||||
if (value) {
|
||||
map[token.address] = new TokenAmount(token, JSBI.BigInt(value))
|
||||
}
|
||||
return map
|
||||
}, {}) ?? {}
|
||||
)
|
||||
}, [address, validTokens, chainId, rawBalanceMap])
|
||||
return useMemo(
|
||||
() =>
|
||||
address && validatedTokens.length > 0
|
||||
? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => {
|
||||
const value = balances?.[i]?.[0]
|
||||
const amount = value ? JSBI.BigInt(value.toString()) : undefined
|
||||
if (amount) {
|
||||
memo[token.address] = new TokenAmount(token, amount)
|
||||
}
|
||||
return memo
|
||||
}, {})
|
||||
: {},
|
||||
[address, validatedTokens, balances]
|
||||
)
|
||||
}
|
||||
|
||||
// contains the hacky logic to treat the WETH token input as if it's ETH to
|
||||
@@ -173,12 +127,10 @@ export function useTokenBalanceTreatingWETHasETH(account?: string, token?: Token
|
||||
}
|
||||
|
||||
// mimics useAllBalances
|
||||
export function useAllTokenBalancesTreatingWETHasETH(): {
|
||||
[account: string]: { [tokenAddress: string]: TokenAmount | undefined }
|
||||
} {
|
||||
export function useAllTokenBalancesTreatingWETHasETH(): { [tokenAddress: string]: TokenAmount | undefined } {
|
||||
const { account } = useActiveWeb3React()
|
||||
const allTokens = useAllTokens()
|
||||
const allTokensArray = useMemo(() => Object.values(allTokens ?? {}), [allTokens])
|
||||
const balances = useTokenBalancesTreatWETHAsETH(account ?? undefined, allTokensArray)
|
||||
return account ? { [account]: balances } : {}
|
||||
return balances ?? {}
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { isAddress } from '../../utils'
|
||||
import {
|
||||
startListeningForBalance,
|
||||
startListeningForTokenBalances,
|
||||
stopListeningForBalance,
|
||||
stopListeningForTokenBalances,
|
||||
updateEtherBalances,
|
||||
updateTokenBalances
|
||||
} from './actions'
|
||||
|
||||
// all address keys are checksummed and valid addresses starting with 0x
|
||||
interface WalletState {
|
||||
readonly tokenBalanceListeners: {
|
||||
readonly [address: string]: {
|
||||
// the number of listeners for each address/token combo
|
||||
readonly [tokenAddress: string]: number
|
||||
}
|
||||
}
|
||||
|
||||
readonly balanceListeners: {
|
||||
// the number of ether balance listeners for each address
|
||||
readonly [address: string]: number
|
||||
}
|
||||
|
||||
readonly balances: {
|
||||
readonly [balanceKey: string]: {
|
||||
readonly value: string
|
||||
readonly blockNumber: number | undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function balanceKey({
|
||||
chainId,
|
||||
address,
|
||||
tokenAddress
|
||||
}: {
|
||||
chainId: number
|
||||
address: string
|
||||
tokenAddress?: string // undefined for ETH
|
||||
}): string {
|
||||
return `${chainId}-${address}-${tokenAddress ?? 'ETH'}`
|
||||
}
|
||||
|
||||
const initialState: WalletState = {
|
||||
balanceListeners: {},
|
||||
tokenBalanceListeners: {},
|
||||
balances: {}
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(startListeningForTokenBalances, (state, { payload: combos }) => {
|
||||
combos.forEach(combo => {
|
||||
const [checksummedTokenAddress, checksummedAddress] = [isAddress(combo.tokenAddress), isAddress(combo.address)]
|
||||
if (!checksummedAddress || !checksummedTokenAddress) {
|
||||
console.error('invalid combo', combo)
|
||||
return
|
||||
}
|
||||
state.tokenBalanceListeners[checksummedAddress] = state.tokenBalanceListeners[checksummedAddress] ?? {}
|
||||
state.tokenBalanceListeners[checksummedAddress][checksummedTokenAddress] =
|
||||
(state.tokenBalanceListeners[checksummedAddress][checksummedTokenAddress] ?? 0) + 1
|
||||
})
|
||||
})
|
||||
.addCase(stopListeningForTokenBalances, (state, { payload: combos }) => {
|
||||
combos.forEach(combo => {
|
||||
const [checksummedTokenAddress, checksummedAddress] = [isAddress(combo.tokenAddress), isAddress(combo.address)]
|
||||
if (!checksummedAddress || !checksummedTokenAddress) {
|
||||
console.error('invalid combo', combo)
|
||||
return
|
||||
}
|
||||
if (!state.tokenBalanceListeners[checksummedAddress]) return
|
||||
if (!state.tokenBalanceListeners[checksummedAddress][checksummedTokenAddress]) return
|
||||
if (state.tokenBalanceListeners[checksummedAddress][checksummedTokenAddress] === 1) {
|
||||
delete state.tokenBalanceListeners[checksummedAddress][checksummedTokenAddress]
|
||||
} else {
|
||||
state.tokenBalanceListeners[checksummedAddress][checksummedTokenAddress]--
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(startListeningForBalance, (state, { payload: { addresses } }) => {
|
||||
addresses.forEach(address => {
|
||||
const checksummedAddress = isAddress(address)
|
||||
if (!checksummedAddress) {
|
||||
console.error('invalid address', address)
|
||||
return
|
||||
}
|
||||
state.balanceListeners[checksummedAddress] = (state.balanceListeners[checksummedAddress] ?? 0) + 1
|
||||
})
|
||||
})
|
||||
.addCase(stopListeningForBalance, (state, { payload: { addresses } }) => {
|
||||
addresses.forEach(address => {
|
||||
const checksummedAddress = isAddress(address)
|
||||
if (!checksummedAddress) {
|
||||
console.error('invalid address', address)
|
||||
return
|
||||
}
|
||||
if (!state.balanceListeners[checksummedAddress]) return
|
||||
if (state.balanceListeners[checksummedAddress] === 1) {
|
||||
delete state.balanceListeners[checksummedAddress]
|
||||
} else {
|
||||
state.balanceListeners[checksummedAddress]--
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(updateTokenBalances, (state, { payload: { chainId, address, blockNumber, tokenBalances } }) => {
|
||||
const checksummedAddress = isAddress(address)
|
||||
if (!checksummedAddress) return
|
||||
Object.keys(tokenBalances).forEach(tokenAddress => {
|
||||
const checksummedTokenAddress = isAddress(tokenAddress)
|
||||
if (!checksummedTokenAddress) return
|
||||
const balance = tokenBalances[checksummedTokenAddress]
|
||||
const key = balanceKey({ chainId, address: checksummedAddress, tokenAddress: checksummedTokenAddress })
|
||||
const data = state.balances[key]
|
||||
if (!data || data.blockNumber === undefined || data.blockNumber <= blockNumber) {
|
||||
state.balances[key] = { value: balance, blockNumber }
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(updateEtherBalances, (state, { payload: { etherBalances, chainId, blockNumber } }) => {
|
||||
Object.keys(etherBalances).forEach(address => {
|
||||
const checksummedAddress = isAddress(address)
|
||||
if (!checksummedAddress) return
|
||||
|
||||
const balance = etherBalances[checksummedAddress]
|
||||
const key = balanceKey({ chainId, address: checksummedAddress })
|
||||
const data = state.balances[key]
|
||||
if (!data || data.blockNumber === undefined || data.blockNumber <= blockNumber) {
|
||||
state.balances[key] = { value: balance, blockNumber }
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
@@ -1,107 +0,0 @@
|
||||
import { BalanceMap, getEtherBalances, getTokensBalance } from '@mycrypto/eth-scan'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useBlockNumber } from '../application/hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { updateEtherBalances, updateTokenBalances } from './actions'
|
||||
import { balanceKey } from './reducer'
|
||||
|
||||
function convertBalanceMapValuesToString(balanceMap: BalanceMap): { [key: string]: string } {
|
||||
return Object.keys(balanceMap).reduce<{ [key: string]: string }>((map, key) => {
|
||||
map[key] = balanceMap[key].toString()
|
||||
return map
|
||||
}, {})
|
||||
}
|
||||
|
||||
export default function Updater() {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const lastBlockNumber = useBlockNumber()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const ethBalanceListeners = useSelector<AppState, AppState['wallet']['balanceListeners']>(state => {
|
||||
return state.wallet.balanceListeners
|
||||
})
|
||||
const tokenBalanceListeners = useSelector<AppState, AppState['wallet']['tokenBalanceListeners']>(state => {
|
||||
return state.wallet.tokenBalanceListeners
|
||||
})
|
||||
const allBalances = useSelector<AppState, AppState['wallet']['balances']>(state => state.wallet.balances)
|
||||
|
||||
const activeETHListeners: string[] = useMemo(() => {
|
||||
return Object.keys(ethBalanceListeners).filter(address => ethBalanceListeners[address] > 0) // redundant check
|
||||
}, [ethBalanceListeners])
|
||||
|
||||
const activeTokenBalanceListeners: { [address: string]: string[] } = useMemo(() => {
|
||||
return Object.keys(tokenBalanceListeners).reduce<{ [address: string]: string[] }>((map, address) => {
|
||||
const tokenAddresses = Object.keys(tokenBalanceListeners[address]).filter(
|
||||
tokenAddress => tokenBalanceListeners[address][tokenAddress] > 0 // redundant check
|
||||
)
|
||||
map[address] = tokenAddresses
|
||||
return map
|
||||
}, {})
|
||||
}, [tokenBalanceListeners])
|
||||
|
||||
const ethBalancesNeedUpdate: string[] = useMemo(() => {
|
||||
if (!chainId || !lastBlockNumber) return []
|
||||
return activeETHListeners.filter(address => {
|
||||
const data = allBalances[balanceKey({ chainId, address })]
|
||||
if (!data || !data.blockNumber) return true
|
||||
return data.blockNumber < lastBlockNumber
|
||||
})
|
||||
}, [activeETHListeners, allBalances, chainId, lastBlockNumber])
|
||||
|
||||
const tokenBalancesNeedUpdate: { [address: string]: string[] } = useMemo(() => {
|
||||
if (!chainId || !lastBlockNumber) return {}
|
||||
return Object.keys(activeTokenBalanceListeners).reduce<{ [address: string]: string[] }>((map, address) => {
|
||||
const needsUpdate =
|
||||
activeTokenBalanceListeners[address]?.filter(tokenAddress => {
|
||||
const data = allBalances[balanceKey({ chainId, tokenAddress, address })]
|
||||
if (!data || !data.blockNumber) return true
|
||||
return data.blockNumber < lastBlockNumber
|
||||
}) ?? []
|
||||
if (needsUpdate.length > 0) {
|
||||
map[address] = needsUpdate
|
||||
}
|
||||
return map
|
||||
}, {})
|
||||
}, [activeTokenBalanceListeners, allBalances, chainId, lastBlockNumber])
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || !chainId || !lastBlockNumber || ethBalancesNeedUpdate.length === 0) return
|
||||
getEtherBalances(library, ethBalancesNeedUpdate)
|
||||
.then(balanceMap => {
|
||||
dispatch(
|
||||
updateEtherBalances({
|
||||
blockNumber: lastBlockNumber,
|
||||
chainId,
|
||||
etherBalances: convertBalanceMapValuesToString(balanceMap)
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('balance fetch failed', ethBalancesNeedUpdate, error)
|
||||
})
|
||||
}, [library, ethBalancesNeedUpdate, dispatch, lastBlockNumber, chainId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || !chainId || !lastBlockNumber) return
|
||||
Object.keys(tokenBalancesNeedUpdate).forEach(address => {
|
||||
if (tokenBalancesNeedUpdate[address].length === 0) return
|
||||
getTokensBalance(library, address, tokenBalancesNeedUpdate[address])
|
||||
.then(tokenBalanceMap => {
|
||||
dispatch(
|
||||
updateTokenBalances({
|
||||
address,
|
||||
chainId,
|
||||
blockNumber: lastBlockNumber,
|
||||
tokenBalances: convertBalanceMapValuesToString(tokenBalanceMap)
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`failed to get token balances`, address, tokenBalancesNeedUpdate[address], error)
|
||||
})
|
||||
})
|
||||
}, [library, tokenBalancesNeedUpdate, dispatch, lastBlockNumber, chainId])
|
||||
|
||||
return null
|
||||
}
|
||||
32
src/utils/chunkArray.test.ts
Normal file
32
src/utils/chunkArray.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import chunkArray from './chunkArray'
|
||||
|
||||
describe('#chunkArray', () => {
|
||||
it('size 1', () => {
|
||||
expect(chunkArray([1, 2, 3], 1)).toEqual([[1], [2], [3]])
|
||||
})
|
||||
it('size 0 throws', () => {
|
||||
expect(() => chunkArray([1, 2, 3], 0)).toThrow('maxChunkSize must be gte 1')
|
||||
})
|
||||
it('size gte items', () => {
|
||||
expect(chunkArray([1, 2, 3], 3)).toEqual([[1, 2, 3]])
|
||||
expect(chunkArray([1, 2, 3], 4)).toEqual([[1, 2, 3]])
|
||||
})
|
||||
it('size exact half', () => {
|
||||
expect(chunkArray([1, 2, 3, 4], 2)).toEqual([
|
||||
[1, 2],
|
||||
[3, 4]
|
||||
])
|
||||
})
|
||||
it('evenly distributes', () => {
|
||||
const chunked = chunkArray([...Array(100).keys()], 40)
|
||||
|
||||
expect(chunked).toEqual([
|
||||
[...Array(34).keys()],
|
||||
[...Array(34).keys()].map(i => i + 34),
|
||||
[...Array(32).keys()].map(i => i + 68)
|
||||
])
|
||||
|
||||
expect(chunked[0][0]).toEqual(0)
|
||||
expect(chunked[2][31]).toEqual(99)
|
||||
})
|
||||
})
|
||||
11
src/utils/chunkArray.ts
Normal file
11
src/utils/chunkArray.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// chunks array into chunks of maximum size
|
||||
// evenly distributes items among the chunks
|
||||
export default function chunkArray<T>(items: T[], maxChunkSize: number): T[][] {
|
||||
if (maxChunkSize < 1) throw new Error('maxChunkSize must be gte 1')
|
||||
if (items.length <= maxChunkSize) return [items]
|
||||
|
||||
const numChunks: number = Math.ceil(items.length / maxChunkSize)
|
||||
const chunkSize = Math.ceil(items.length / numChunks)
|
||||
|
||||
return [...Array(numChunks).keys()].map(ix => items.slice(ix * chunkSize, ix * chunkSize + chunkSize))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"strictNullChecks": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitUseStrict": true
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
||||
192
yarn.lock
192
yarn.lock
@@ -1349,21 +1349,6 @@
|
||||
"@ethersproject/properties" ">=5.0.0-beta.140"
|
||||
"@ethersproject/strings" ">=5.0.0-beta.136"
|
||||
|
||||
"@ethersproject/abi@^5.0.0-beta.146":
|
||||
version "5.0.0-beta.155"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.0.0-beta.155.tgz#b02cc0d54a44fd499be9778be53ed112220e3ecd"
|
||||
integrity sha512-Oy00vZtb/Yr6gL9SJdKj7lmcL3e/04K5Dpd20ej52rXuRDYddCn9yHSkYWRM8/ZFFepFqeXmZ3XVN0ixLOJwcA==
|
||||
dependencies:
|
||||
"@ethersproject/address" ">=5.0.0-beta.134"
|
||||
"@ethersproject/bignumber" ">=5.0.0-beta.138"
|
||||
"@ethersproject/bytes" ">=5.0.0-beta.137"
|
||||
"@ethersproject/constants" ">=5.0.0-beta.133"
|
||||
"@ethersproject/hash" ">=5.0.0-beta.133"
|
||||
"@ethersproject/keccak256" ">=5.0.0-beta.131"
|
||||
"@ethersproject/logger" ">=5.0.0-beta.137"
|
||||
"@ethersproject/properties" ">=5.0.0-beta.140"
|
||||
"@ethersproject/strings" ">=5.0.0-beta.136"
|
||||
|
||||
"@ethersproject/abstract-provider@>=5.0.0-beta.131":
|
||||
version "5.0.0-beta.139"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.0.0-beta.139.tgz#a3b52c5494dcf67d277e2c0443813d9de746f8b4"
|
||||
@@ -1468,7 +1453,7 @@
|
||||
"@ethersproject/properties" ">=5.0.0-beta.131"
|
||||
bn.js "^4.4.0"
|
||||
|
||||
"@ethersproject/bignumber@>=5.0.0-beta.138", "@ethersproject/bignumber@^5.0.0-beta.135":
|
||||
"@ethersproject/bignumber@>=5.0.0-beta.138":
|
||||
version "5.0.0-beta.139"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.0.0-beta.139.tgz#12a4fa5a76ee90f77932326311caf04e1de1cae0"
|
||||
integrity sha512-h1C1okCmPK3UVWwMGUbuCZykplJmD/TdknPQQHJWL/chK5MqBhyQ5o1Cay7mHXKCBnjWrR9BtwjfkAh76pYtFA==
|
||||
@@ -2308,15 +2293,6 @@
|
||||
call-me-maybe "^1.0.1"
|
||||
glob-to-regexp "^0.3.0"
|
||||
|
||||
"@mycrypto/eth-scan@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@mycrypto/eth-scan/-/eth-scan-2.1.0.tgz#9248b00000bff0b4ac9acda093d98eae0c84b93c"
|
||||
integrity sha512-ncbWZDz6lL/8iFklGYt5MG2iTtTzJ6V6P10ht6PoOMHFG/2HiAm9AbKzwP0twYRh/oa/p/nSg3SrZer0Zuk7ZA==
|
||||
dependencies:
|
||||
"@ethersproject/abi" "^5.0.0-beta.146"
|
||||
"@ethersproject/bignumber" "^5.0.0-beta.135"
|
||||
isomorphic-unfetch "^3.0.0"
|
||||
|
||||
"@nodelib/fs.stat@^1.1.2":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||
@@ -2385,33 +2361,34 @@
|
||||
penpal "3.0.7"
|
||||
pocket-js-core "0.0.3"
|
||||
|
||||
"@reach/component-component@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@reach/component-component/-/component-component-0.1.3.tgz#5d156319572dc38995b246f81878bc2577c517e5"
|
||||
integrity sha512-a1USH7L3bEfDdPN4iNZGvMEFuBfkdG+QNybeyDv8RloVFgZYRoM+KGXyy2KOfEnTUM8QWDRSROwaL3+ts5Angg==
|
||||
|
||||
"@reach/dialog@^0.2.8":
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/@reach/dialog/-/dialog-0.2.9.tgz#6375ec4adc1e22838aeede15f57d5eb5ac0e571c"
|
||||
integrity sha512-4plyTRt2X4bB9A5fDFXH0bxb4aipLyAuRVMpOuA1RekFgvkxFn65em2CWYSzRsVf3aTQ2cEnYET4a5H2Qu8a5Q==
|
||||
"@reach/dialog@^0.10.3":
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@reach/dialog/-/dialog-0.10.3.tgz#ba789809c3b194fff79d3bcb4a583c58e03edb83"
|
||||
integrity sha512-RMpUHNjRQhkjGzKt9/oLmDhwUBikW3JbEzgzZngq5MGY5kWRPwYInLDkEA8We4E43AbBsl5J/PRzQha9V+EEXw==
|
||||
dependencies:
|
||||
"@reach/component-component" "^0.1.3"
|
||||
"@reach/portal" "^0.2.1"
|
||||
"@reach/utils" "^0.2.3"
|
||||
react-focus-lock "^1.17.7"
|
||||
react-remove-scroll "^1.0.2"
|
||||
"@reach/portal" "^0.10.3"
|
||||
"@reach/utils" "^0.10.3"
|
||||
prop-types "^15.7.2"
|
||||
react-focus-lock "^2.3.1"
|
||||
react-remove-scroll "^2.3.0"
|
||||
tslib "^1.11.2"
|
||||
|
||||
"@reach/portal@^0.2.1":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.2.1.tgz#07720b999e0063a9e179c14dbdc60fd991cfc9fa"
|
||||
integrity sha512-pUQ0EtCcYm4ormEjJmdk4uzZCxOpaRHB8FDKJXy6q6GqRqQwZ4lAT1f2Tvw0DAmULmyZTpe1/heXY27Tdnct+Q==
|
||||
"@reach/portal@^0.10.3":
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.10.3.tgz#2eb408cc246d3eabbbf3b47ca4dc9c381cdb1d88"
|
||||
integrity sha512-t8c+jtDxMLSPRGg93sQd2s6dDNilh5/qdrwmx88ki7l9h8oIXqMxPP3kSkOqZ9cbVR0b2A68PfMhCDOwMGvkoQ==
|
||||
dependencies:
|
||||
"@reach/component-component" "^0.1.3"
|
||||
"@reach/utils" "^0.10.3"
|
||||
tslib "^1.11.2"
|
||||
|
||||
"@reach/utils@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.2.3.tgz#820f6a6af4301b4c5065cfc04bb89e6a3d1d723f"
|
||||
integrity sha512-zM9rA8jDchr05giMhL95dPeYkK67cBQnIhCVrOKKqgWGsv+2GE/HZqeptvU4zqs0BvIqsThwov+YxVNVh5csTQ==
|
||||
"@reach/utils@^0.10.3":
|
||||
version "0.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.10.3.tgz#e30f9b172d131161953df7dd01553c57ca4e78f8"
|
||||
integrity sha512-LoIZSfVAJMA+DnzAMCMfc/wAM39iKT8BQQ9gI1FODpxd8nPFP4cKisMuRXImh2/iVtG2Z6NzzCNgceJSrywqFQ==
|
||||
dependencies:
|
||||
"@types/warning" "^3.0.0"
|
||||
tslib "^1.11.2"
|
||||
warning "^4.0.3"
|
||||
|
||||
"@reduxjs/toolkit@^1.3.5":
|
||||
version "1.3.5"
|
||||
@@ -2817,6 +2794,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"
|
||||
@@ -3024,6 +3013,11 @@
|
||||
dependencies:
|
||||
pretty-format "^25.1.0"
|
||||
|
||||
"@types/warning@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
|
||||
integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI=
|
||||
|
||||
"@types/web3-provider-engine@^14.0.0":
|
||||
version "14.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/web3-provider-engine/-/web3-provider-engine-14.0.0.tgz#43adc3b39dc9812b82aef8cd2d66577665ad59b0"
|
||||
@@ -8305,7 +8299,7 @@ fancy-log@^1.3.2:
|
||||
parse-node-version "^1.0.0"
|
||||
time-stamp "^1.0.0"
|
||||
|
||||
fast-deep-equal@2.0.1, fast-deep-equal@^2.0.1:
|
||||
fast-deep-equal@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
|
||||
@@ -8616,7 +8610,7 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.3.6"
|
||||
|
||||
focus-lock@^0.6.3:
|
||||
focus-lock@^0.6.7:
|
||||
version "0.6.8"
|
||||
resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.6.8.tgz#61985fadfa92f02f2ee1d90bc738efaf7f3c9f46"
|
||||
integrity sha512-vkHTluRCoq9FcsrldC0ulQHiyBYgVJB2CX53I8r0nTC6KnEij7Of0jpBspjt3/CuNb6fyoj3aOh9J2HgQUM0og==
|
||||
@@ -8891,6 +8885,11 @@ get-caller-file@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-nonce@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3"
|
||||
integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==
|
||||
|
||||
get-own-enumerable-property-symbols@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664"
|
||||
@@ -10328,14 +10327,6 @@ isomorphic-fetch@2.2.1:
|
||||
node-fetch "^1.0.1"
|
||||
whatwg-fetch ">=0.10.0"
|
||||
|
||||
isomorphic-unfetch@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.0.0.tgz#de6d80abde487b17de2c400a7ef9e5ecc2efb362"
|
||||
integrity sha512-V0tmJSYfkKokZ5mgl0cmfQMTb7MLHsBMngTkbLY0eXvKqiVRRoZP04Ly+KhKrJfKtzC9E6Pp15Jo+bwh7Vi2XQ==
|
||||
dependencies:
|
||||
node-fetch "^2.2.0"
|
||||
unfetch "^4.0.0"
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
@@ -12318,11 +12309,6 @@ node-fetch@^1.0.1, node-fetch@~1.7.1:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-fetch@^2.2.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
||||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
|
||||
node-forge@0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
|
||||
@@ -14402,7 +14388,7 @@ react-app-polyfill@^1.0.6:
|
||||
regenerator-runtime "^0.13.3"
|
||||
whatwg-fetch "^3.0.0"
|
||||
|
||||
react-clientside-effect@^1.2.0:
|
||||
react-clientside-effect@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.2.tgz#6212fb0e07b204e714581dd51992603d1accc837"
|
||||
integrity sha512-nRmoyxeok5PBO6ytPvSjKp9xwXg9xagoTK1mMjwnQxqM9Hd7MNPl+LS1bOSOe+CV2+4fnEquc7H/S8QD3q697A==
|
||||
@@ -14473,15 +14459,17 @@ react-feather@^2.0.8:
|
||||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-focus-lock@^1.17.7:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-1.19.1.tgz#2f3429793edaefe2d077121f973ce5a3c7a0651a"
|
||||
integrity sha512-TPpfiack1/nF4uttySfpxPk4rGZTLXlaZl7ncZg/ELAk24Iq2B1UUaUioID8H8dneUXqznT83JTNDHDj+kwryw==
|
||||
react-focus-lock@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47"
|
||||
integrity sha512-j15cWLPzH0gOmRrUg01C09Peu8qbcdVqr6Bjyfxj80cNZmH+idk/bNBYEDSmkAtwkXI+xEYWSmHYqtaQhZ8iUQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
focus-lock "^0.6.3"
|
||||
focus-lock "^0.6.7"
|
||||
prop-types "^15.6.2"
|
||||
react-clientside-effect "^1.2.0"
|
||||
react-clientside-effect "^1.2.2"
|
||||
use-callback-ref "^1.2.1"
|
||||
use-sidecar "^1.0.1"
|
||||
|
||||
react-ga@^2.5.7:
|
||||
version "2.7.0"
|
||||
@@ -14520,21 +14508,24 @@ react-redux@^7.2.0:
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.9.0"
|
||||
|
||||
react-remove-scroll-bar@^1.1.5:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-1.2.0.tgz#07250b2bc581f56315759c454c9b159dd04ba49d"
|
||||
integrity sha512-8xSYR6xgW8QW65k38qB1Sh6ouTRjZ7BEteepR9tACd1rSaRyVYWabFxYLNOr4l1blZlqb81GEmDpUoPm7LsUTA==
|
||||
react-remove-scroll-bar@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.1.0.tgz#edafe9b42a42c0dad9bdd10712772a1f9a39d7b9"
|
||||
integrity sha512-5X5Y5YIPjIPrAoMJxf6Pfa7RLNGCgwZ95TdnVPgPuMftRfO8DaC7F4KP1b5eiO8hHbe7u+wZNDbYN5WUTpv7+g==
|
||||
dependencies:
|
||||
react-style-singleton "^1.1.0"
|
||||
react-style-singleton "^2.1.0"
|
||||
tslib "^1.0.0"
|
||||
|
||||
react-remove-scroll@^1.0.2:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-1.0.8.tgz#a5aadc56368345a51ba524582c842a773849f609"
|
||||
integrity sha512-AS6gFBO6T2CP0TgmDjq3Ip0Fz1HKyv+lzNrQAkJBSWGyOYaMWLMDy77mQJ7qEyy6fK0pI+Cz5x3X81/ux6SBew==
|
||||
react-remove-scroll@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.3.0.tgz#3af06fe2f7130500704b676cdef94452c08fe593"
|
||||
integrity sha512-UqVimLeAe+5EHXKfsca081hAkzg3WuDmoT9cayjBegd6UZVhlTEchleNp9J4TMGkb/ftLve7ARB5Wph+HJ7A5g==
|
||||
dependencies:
|
||||
react-remove-scroll-bar "^1.1.5"
|
||||
react-remove-scroll-bar "^2.1.0"
|
||||
react-style-singleton "^2.1.0"
|
||||
tslib "^1.0.0"
|
||||
use-callback-ref "^1.2.3"
|
||||
use-sidecar "^1.0.1"
|
||||
|
||||
react-router-dom@^5.0.0:
|
||||
version "5.1.2"
|
||||
@@ -14633,11 +14624,12 @@ react-spring@^8.0.27:
|
||||
"@babel/runtime" "^7.3.1"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-style-singleton@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-1.1.1.tgz#b2b698765519da812b80f55ab3c5fc5d849a2e63"
|
||||
integrity sha512-0JD+XC5veR3oxf7GzIXipr89sM8R3rWnOR/gpzIV0DnoRBrcTvvkqyMu9icDYqM/6CWJhYcH5Jdy6Nim7PmoTQ==
|
||||
react-style-singleton@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.0.tgz#7396885332e9729957f9df51f08cadbfc164e1c4"
|
||||
integrity sha512-DH4ED+YABC1dhvSDYGGreAHmfuTXj6+ezT3CmHoqIEfxNgEYfIMoOtmbRp42JsUst3IPqBTDL+8r4TF7EWhIHw==
|
||||
dependencies:
|
||||
get-nonce "^1.0.0"
|
||||
invariant "^2.2.4"
|
||||
tslib "^1.0.0"
|
||||
|
||||
@@ -16503,13 +16495,6 @@ swarm-js@0.1.39:
|
||||
tar "^4.0.2"
|
||||
xhr-request-promise "^0.1.2"
|
||||
|
||||
swr@0.1.18:
|
||||
version "0.1.18"
|
||||
resolved "https://registry.yarnpkg.com/swr/-/swr-0.1.18.tgz#be62df4cb8d188dc092305b35ecda1f3be8e61c1"
|
||||
integrity sha512-lD31JxsD0bXdT7dyGVIB7MHcwgFp+HbBBOLt075hJT0sEgW01E3+EuCeB6fsavxZ2UjUZ3f+SbNMo9c8pv9uiA==
|
||||
dependencies:
|
||||
fast-deep-equal "2.0.1"
|
||||
|
||||
symbol-observable@^1.1.0, symbol-observable@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
@@ -16873,6 +16858,11 @@ tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.2.tgz#9c79d83272c9a7aaf166f73915c9667ecdde3cc9"
|
||||
integrity sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==
|
||||
|
||||
tslib@^1.11.2, tslib@^1.9.3:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
|
||||
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
|
||||
|
||||
tsutils@^3.17.1:
|
||||
version "3.17.1"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
||||
@@ -17029,11 +17019,6 @@ undertaker@^1.2.1:
|
||||
object.reduce "^1.0.0"
|
||||
undertaker-registry "^1.0.0"
|
||||
|
||||
unfetch@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db"
|
||||
integrity sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
|
||||
@@ -17215,11 +17200,24 @@ usb@^1.6.0:
|
||||
nan "2.13.2"
|
||||
prebuild-install "^5.3.3"
|
||||
|
||||
use-callback-ref@^1.2.1, use-callback-ref@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.3.tgz#9f939dfb5740807bbf9dd79cdd4e99d27e827756"
|
||||
integrity sha512-DPBPh1i2adCZoIArRlTuKRy7yue7QogtEnfv0AKrWsY+GA+4EKe37zhRDouNnyWMoNQFYZZRF+2dLHsWE4YvJA==
|
||||
|
||||
use-media@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/use-media/-/use-media-1.4.0.tgz#e777bf1f382a7aacabbd1f9ce3da2b62e58b2a98"
|
||||
integrity sha512-XsgyUAf3nhzZmEfhc5MqLHwyaPjs78bgytpVJ/xDl0TF4Bptf3vEpBNBBT/EIKOmsOc8UbuECq3mrP3mt1QANA==
|
||||
|
||||
use-sidecar@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"
|
||||
integrity sha512-287RZny6m5KNMTb/Kq9gmjafi7lQL0YHO1lYolU6+tY1h9+Z3uCtkJJ3OSOq3INwYf2hBryCcDh4520AhJibMA==
|
||||
dependencies:
|
||||
detect-node "^2.0.4"
|
||||
tslib "^1.9.3"
|
||||
|
||||
use@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||
@@ -17450,7 +17448,7 @@ walletlink@^2.0.2:
|
||||
preact "^10.3.3"
|
||||
rxjs "^6.5.4"
|
||||
|
||||
warning@^4.0.2:
|
||||
warning@^4.0.2, warning@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
|
||||
Reference in New Issue
Block a user