Commit 9c1fe53e authored by Moody Salem's avatar Moody Salem Committed by GitHub

perf(ethereum): reduce number of calls by batching all polling node calls (#840)

* initial refactoring

* rebase lint error

* start implementing reducer

* multicall reducer

* working multicall!

* clean up performance, re-fix annoying error

* use multicall everywhere

* use multicall for balances

* fix lint warning

* Use checksummed address

* Fix strict warning

* get it to a working state with the more generic form

* convert useETHBalances

* Remove the eth-scan contract completely

* Remove the eth-scan contract completely more

* Default export

* Put the encoding/decoding in the methods that can do it most efficiently

* Avoid duplicate fetches via debounce

* Reduce delay to something less noticeable

* Return null if pair reserves are undefined to indicate it does not exist
parent 28c916ff
......@@ -43,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
......
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'
......
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)<{ mobile: boolean }>`
// 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)<{ mobile: boolean }>`
// 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};
......@@ -163,6 +165,7 @@ export default function Modal({
}}
>
<StyledDialogContent
ariaLabel="test"
style={props}
hidden={true}
minHeight={minHeight}
......
......@@ -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',
......
......@@ -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)
......
......@@ -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
......
......@@ -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'
......
import { Interface } from '@ethersproject/abi'
import ERC20_ABI from './erc20.json'
const ERC20_INTERFACE = new Interface(ERC20_ABI)
export default ERC20_INTERFACE
[
{
"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"
}
]
\ No newline at end of file
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 }
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
])
}
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])
}
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
}
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
}`
......
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])
}
{
"extends": "../../tsconfig.strict.json",
"include": ["**/*"]
}
\ No newline at end of file
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,
......
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])
}
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)
}
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]
}
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
}
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
}
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,
......
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 />
</>
)
}
......
......@@ -19,7 +19,8 @@ import Row, { RowBetween, RowFixed } from '../../components/Row'
import Slider from '../../components/Slider'
import TokenLogo from '../../components/TokenLogo'
import { ROUTER_ADDRESS, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { usePairContract, useActiveWeb3React } from '../../hooks'
import { useActiveWeb3React } from '../../hooks'
import { usePairContract } from '../../hooks/useContract'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { TYPE } from '../../theme'
......
......@@ -251,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,
......
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'
......
......@@ -4,10 +4,10 @@ import { save, load } from 'redux-localstorage-simple'
import application from './application/reducer'
import user from './user/reducer'
import transactions from './transactions/reducer'
import wallet from './wallet/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'
......@@ -18,10 +18,10 @@ const store = configureStore({
application,
user,
transactions,
wallet,
swap,
mint,
burn
burn,
multicall
},
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ states: PERSISTED_KEYS })
......
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')
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])
}
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
}
})
})
)
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
}
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')
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)
// 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)
const results = useSingleContractMultipleData(
multicallContract,
'getEthBalance',
addresses.map(address => [address])
)
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])
// keep the listeners up to date
useEffect(() => {
const combos: TokenBalanceListenerKey[] = JSON.parse(serializedCombos)
if (combos.length === 0) return
dispatch(startListeningForTokenBalances(combos))
return () => {
dispatch(stopListeningForTokenBalances(combos))
}
}, [address, serializedCombos, dispatch])
const validatedTokenAddresses = useMemo(() => validatedTokens.map(vt => vt.address), [validatedTokens])
const rawBalanceMap = useSelector<AppState, AppState['wallet']['balances']>(({ wallet: { balances } }) => balances)
const balances = useMultipleContractSingleData(validatedTokenAddresses, ERC20_INTERFACE, 'balanceOf', [address])
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 ?? {}
}
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 }
}
})
})
)
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
}
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)
})
})
// 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))
}
......@@ -7,6 +7,7 @@
"strictNullChecks": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitUseStrict": true
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true
}
}
\ No newline at end of file
......@@ -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"
......@@ -8323,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=
......@@ -10351,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"
......@@ -12341,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"
......@@ -16532,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"
......@@ -17063,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"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment