Commit 9006acb4 authored by Moody Salem's avatar Moody Salem Committed by GitHub

Balances rewrite (#761)

* Create the wallet store

* Get the updater completed

* Code complete

* Fix token balance bug

* Fix another bug in the hooks

* Final bug fix, blockNumber can be undefined

* Formalize the fact that block number can be undefined

* Woops add package

* Add more info to errors

* Replace balances in the v1 methods with the new ones

* Only return a balance value if it's present

* Address comments

* Trigger updateVersion before anything else
parent 1e1a0499
......@@ -17,10 +17,13 @@ An open source interface for Uniswap -- a protocol for decentralized exchange of
## Run Uniswap Locally
1. Download and unzip the `build.zip` file from the latest release in the [Releases tab](https://github.com/Uniswap/uniswap-frontend/releases/latest).
2. Serve the `build/` folder locally, and access the application via a browser.
For more information on running a local server see [https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server). This simple approach has one downside: refreshing the page will give a `404` because of how React handles client-side routing. To fix this issue, consider running `serve -s` courtesy of the [serve](https://github.com/zeit/serve) package.
For more information on running a local server see
[https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server).
This simple approach has one downside: refreshing the page will give a `404` because of how React handles client-side routing.
To fix this issue, consider running `serve -s` courtesy of the [serve](https://github.com/zeit/serve) package.
## Develop Uniswap Locally
......@@ -30,7 +33,7 @@ For more information on running a local server see [https://developer.mozilla.or
yarn
```
### Configure Environment
### Configure Environment (optional)
Copy `.env` to `.env.local` and change the appropriate variables.
......@@ -40,11 +43,19 @@ Copy `.env` to `.env.local` and change the appropriate variables.
yarn start
```
To run on a testnet, make a copy of `.env` named `.env.local`, change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`,
and change `REACT_APP_NETWORK_URL` to e.g. `"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
To have the frontend default to a different network, make a copy of `.env` named `.env.local`,
change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETWORK_URL` to e.g.
`"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
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.
The frontend is not expected to work with local testnets.
### Deployment
If deploying with Github Pages, be aware that there's some
[tricky client-side routing behavior with `create-react-app`](https://create-react-app.dev/docs/deployment#notes-on-client-side-routing).
As a single page application, all routes that do not match an asset must be redirect to `/index.html`.
See [create-react-app documentation.](https://create-react-app.dev/docs/deployment#notes-on-client-side-routing).
## Contributions
......
......@@ -3,6 +3,7 @@ import React, { useState, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import '@reach/tooltip/styles.css'
import { darken } from 'polished'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
......@@ -14,7 +15,6 @@ import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useWeb3React } from '../../hooks'
import { useTranslation } from 'react-i18next'
import { useAddressBalance } from '../../contexts/Balances'
const InputRow = styled.div<{ selected: boolean }>`
${({ theme }) => theme.flexRowNoWrap}
......@@ -163,7 +163,7 @@ export default function CurrencyInputPanel({
const [modalOpen, setModalOpen] = useState(false)
const { account } = useWeb3React()
const userTokenBalance = useAddressBalance(account, token)
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
const theme = useContext(ThemeContext)
return (
......
......@@ -8,6 +8,7 @@ import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint256 } from '@ethersproject/constants'
import { Contract } from '@ethersproject/contracts'
import { useUserAdvanced } from '../../state/application/hooks'
import { useTokenBalanceTreatingWETHasETH, useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { Field, SwapAction, useSwapStateReducer } from './swap-store'
import { Text } from 'rebass'
import Card, { BlueCard, GreyCard, YellowCard } from '../../components/Card'
......@@ -15,7 +16,6 @@ import { AutoColumn, ColumnCenter } from '../../components/Column'
import { AutoRow, RowBetween, RowFixed } from '../Row'
import { ROUTER_ADDRESS } from '../../constants'
import { useTokenAllowance } from '../../data/Allowances'
import { useAddressBalance, useAllBalances } from '../../contexts/Balances'
import { useAddUserToken, useFetchTokenByAddress } from '../../state/user/hooks'
import { useAllTokens, useToken } from '../../contexts/Tokens'
import { useHasPendingApproval, useTransactionAdder } from '../../state/transactions/hooks'
......@@ -152,12 +152,12 @@ function ExchangePage({ sendingInput = false, history, params }: ExchangePagePro
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
// all balances for detecting a swap with send
const allBalances = useAllBalances()
const allBalances = useAllTokenBalancesTreatingWETHasETH()
// get user- and token-specific lookup data
const userBalances = {
[Field.INPUT]: useAddressBalance(account, tokens[Field.INPUT]),
[Field.OUTPUT]: useAddressBalance(account, tokens[Field.OUTPUT])
[Field.INPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.INPUT]),
[Field.OUTPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.OUTPUT])
}
// parse the amount that the user typed
......
......@@ -2,6 +2,7 @@ import React from 'react'
import { Link as HistoryLink } from 'react-router-dom'
import styled from 'styled-components'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import Row from '../Row'
import Menu from '../Menu'
......@@ -13,7 +14,6 @@ import { WETH, ChainId } from '@uniswap/sdk'
import { isMobile } from 'react-device-detect'
import { YellowCard } from '../Card'
import { useWeb3React } from '../../hooks'
import { useAddressBalance } from '../../contexts/Balances'
import { useDarkModeManager } from '../../state/user/hooks'
import Logo from '../../assets/svg/logo.svg'
......@@ -148,7 +148,7 @@ const VersionToggle = styled.a`
export default function Header() {
const { account, chainId } = useWeb3React()
const userEthBalance = useAddressBalance(account, WETH[chainId])
const userEthBalance = useTokenBalanceTreatingWETHasETH(account, WETH[chainId])
const [isDark] = useDarkModeManager()
return (
......
import React, { useState, useEffect } from 'react'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { TokenAmount, JSBI, Token, Pair } from '@uniswap/sdk'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import Row from '../Row'
import TokenLogo from '../TokenLogo'
......@@ -15,7 +16,6 @@ import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../Button'
import { useToken } from '../../contexts/Tokens'
import { useWeb3React } from '@web3-react/core'
import { useAddressBalance } from '../../contexts/Balances'
import { usePairAdder } from '../../state/user/hooks'
import { usePair } from '../../data/Reserves'
......@@ -43,7 +43,7 @@ function PoolFinder({ history }: RouteComponentProps) {
}
}, [pair, addPair])
const position: TokenAmount = useAddressBalance(account, pair?.liquidityToken)
const position: TokenAmount = useTokenBalanceTreatingWETHasETH(account, pair?.liquidityToken)
const newPair: boolean =
pair === null ||
......
......@@ -5,8 +5,8 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'
import { Percent, Pair } from '@uniswap/sdk'
import { useWeb3React } from '@web3-react/core'
import { useAllBalances } from '../../contexts/Balances'
import { useTotalSupply } from '../../data/TotalSupply'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import Card, { GreyCard } from '../Card'
import TokenLogo from '../TokenLogo'
......@@ -37,7 +37,7 @@ interface PositionCardProps extends RouteComponentProps<{}> {
function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) {
const { account } = useWeb3React()
const allBalances = useAllBalances()
const allBalances = useAllTokenBalancesTreatingWETHasETH()
const token0 = pair?.token0
const token1 = pair?.token1
......
......@@ -5,6 +5,7 @@ import { JSBI, Token, WETH } from '@uniswap/sdk'
import { isMobile } from 'react-device-detect'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import { COMMON_BASES } from '../../constants'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { Link as StyledLink } from '../../theme/components'
import Card from '../../components/Card'
......@@ -29,7 +30,6 @@ import {
useAddUserToken,
useRemoveUserAddedToken
} from '../../state/user/hooks'
import { useAllBalances } from '../../contexts/Balances'
import { useTranslation } from 'react-i18next'
import { useToken, useAllTokens, ALL_TOKENS } from '../../contexts/Tokens'
import QuestionHelper from '../Question'
......@@ -178,7 +178,7 @@ function SearchModal({
const allTokens = useAllTokens()
const allPairs = useAllDummyPairs()
const allBalances = useAllBalances()
const allBalances = useAllTokenBalancesTreatingWETHasETH()
const [searchQuery, setSearchQuery] = useState('')
const [sortDirection, setSortDirection] = useState(true)
......
This diff is collapsed.
import { Web3Provider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts'
import { Token, TokenAmount } from '@uniswap/sdk'
import useSWR from 'swr'
import { useWeb3React } from '@web3-react/core'
import { BigNumber } from '@ethersproject/bignumber'
import { SWRKeys } from '.'
import { useTokenContract } from '../hooks'
function getTokenBalance(
contract: Contract,
token: Token
): (_: SWRKeys, __: number, ___: string, owner: string) => Promise<TokenAmount> {
return async (_, __, ___, owner: string): Promise<TokenAmount> =>
contract.balanceOf(owner).then((balance: { toString: () => string }) => new TokenAmount(token, balance.toString()))
}
export function useTokenBalance(token?: Token, owner?: string): TokenAmount {
const contract = useTokenContract(token?.address, false)
const shouldFetch = !!contract && typeof owner === 'string'
const { data } = useSWR(
shouldFetch ? [SWRKeys.TokenBalance, token.chainId, token.address, owner] : null,
getTokenBalance(contract, token),
{
dedupingInterval: 10 * 1000,
refreshInterval: 20 * 1000
}
)
return data
}
function getETHBalance(library: Web3Provider): (_: SWRKeys, owner: string) => Promise<BigNumber> {
return async (_, owner: string): Promise<BigNumber> => library.getBalance(owner)
}
export function useETHBalance(owner?: string): BigNumber {
const { library } = useWeb3React()
const shouldFetch = !!library && typeof owner === 'string'
const { data } = useSWR(shouldFetch ? [SWRKeys.ETHBalance, owner] : null, getETHBalance(library), {
dedupingInterval: 10 * 1000,
refreshInterval: 20 * 1000
})
return data
}
......@@ -6,8 +6,8 @@ import IUniswapV1Factory from '../constants/abis/v1_factory.json'
import { V1_FACTORY_ADDRESS } from '../constants'
import { useContract } from '../hooks'
import { SWRKeys } from '.'
import { useTokenBalance, useETHBalance } from './Balances'
import { AddressZero } from '@ethersproject/constants'
import { useETHBalances, useTokenBalances } from '../state/wallet/hooks'
function getV1PairAddress(contract: Contract): (_: SWRKeys, tokenAddress: string) => Promise<string> {
return async (_: SWRKeys, tokenAddress: string): Promise<string> => contract.getExchange(tokenAddress)
......@@ -30,8 +30,8 @@ function useMockV1Pair(token?: Token) {
const isWETH = token?.equals(WETH[ChainId.MAINNET])
const v1PairAddress = useV1PairAddress(mainnet && !isWETH ? token?.address : undefined)
const tokenBalance = useTokenBalance(token, v1PairAddress)
const ETHBalance = useETHBalance(v1PairAddress)
const tokenBalance = useTokenBalances(v1PairAddress, [token])[token?.address]
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress]
return tokenBalance && ETHBalance
? new Pair(tokenBalance, new TokenAmount(DUMMY_ETH, ETHBalance.toString()))
......
......@@ -2,7 +2,5 @@ export enum SWRKeys {
Allowances,
Reserves,
TotalSupply,
TokenBalance,
ETHBalance,
V1PairAddress
}
......@@ -7,7 +7,7 @@ import { Provider } from 'react-redux'
import { NetworkContextName } from './constants'
import { isMobile } from 'react-device-detect'
import BalancesContextProvider, { Updater as BalancesContextUpdater } from './contexts/Balances'
import WalletUpdater from './state/wallet/updater'
import App from './pages/App'
import store from './state'
import ApplicationUpdater from './state/application/updater'
......@@ -35,17 +35,13 @@ if (process.env.NODE_ENV === 'production') {
ReactGA.pageview(window.location.pathname + window.location.search)
function ContextProviders({ children }: { children: React.ReactNode }) {
return <BalancesContextProvider>{children}</BalancesContextProvider>
}
function Updaters() {
return (
<>
<UserUpdater />
<ApplicationUpdater />
<TransactionUpdater />
<BalancesContextUpdater />
<WalletUpdater />
</>
)
}
......@@ -56,15 +52,13 @@ ReactDOM.render(
<Web3ReactProvider getLibrary={getLibrary}>
<Web3ProviderNetwork getLibrary={getLibrary}>
<Provider store={store}>
<ContextProviders>
<Updaters />
<ThemeProvider>
<>
<ThemedGlobalStyle />
<App />
</>
</ThemeProvider>
</ContextProviders>
<Updaters />
<ThemeProvider>
<>
<ThemedGlobalStyle />
<App />
</>
</ThemeProvider>
</Provider>
</Web3ProviderNetwork>
</Web3ReactProvider>
......
......@@ -13,6 +13,7 @@ import PositionCard from '../../components/PositionCard'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { Text } from 'rebass'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { Plus } from 'react-feather'
import { BlueCard, GreyCard, LightCard } from '../../components/Card'
......@@ -21,7 +22,6 @@ import { ButtonPrimary, ButtonLight } from '../../components/Button'
import Row, { AutoRow, RowBetween, RowFlat, RowFixed } from '../../components/Row'
import { useToken } from '../../contexts/Tokens'
import { useAddressBalance } from '../../contexts/Balances'
import { useTokenAllowance } from '../../data/Allowances'
import { useTotalSupply } from '../../data/TotalSupply'
import { useWeb3React, useTokenContract } from '../../hooks'
......@@ -226,8 +226,8 @@ function AddLiquidity({ token0, token1 }: AddLiquidityProps) {
// get user-pecific and token-specific lookup data
const userBalances: { [field: number]: TokenAmount } = {
[Field.INPUT]: useAddressBalance(account, tokens[Field.INPUT]),
[Field.OUTPUT]: useAddressBalance(account, tokens[Field.OUTPUT])
[Field.INPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.INPUT]),
[Field.OUTPUT]: useTokenBalanceTreatingWETHasETH(account, tokens[Field.OUTPUT])
}
// track non relational amounts if first person to add liquidity
......
......@@ -10,6 +10,7 @@ import DoubleLogo from '../../components/DoubleLogo'
import PositionCard from '../../components/PositionCard'
import ConfirmationModal from '../../components/ConfirmationModal'
import CurrencyInputPanel from '../../components/CurrencyInputPanel'
import { useTokenBalance } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { Text } from 'rebass'
import { LightCard } from '../../components/Card'
......@@ -21,7 +22,6 @@ import Row, { RowBetween, RowFixed } from '../../components/Row'
import { useToken } from '../../contexts/Tokens'
import { useWeb3React } from '../../hooks'
import { useAllBalances } from '../../contexts/Balances'
import { usePairContract } from '../../hooks'
import { useTransactionAdder } from '../../state/transactions/hooks'
import { useTotalSupply } from '../../data/TotalSupply'
......@@ -174,8 +174,7 @@ export default function RemoveLiquidity({ token0, token1 }: { token0: string; to
// pool token data
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
const allBalances = useAllBalances()
const userLiquidity = allBalances?.[account]?.[pair?.liquidityToken?.address]
const userLiquidity = useTokenBalance(account, pair?.liquidityToken)
// input state
const [state, dispatch] = useReducer(reducer, initializeRemoveState(userLiquidity?.toExact(), token0, token1))
......
......@@ -6,6 +6,7 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'
import Question from '../../components/Question'
import SearchModal from '../../components/SearchModal'
import PositionCard from '../../components/PositionCard'
import { useTokenBalances } from '../../state/wallet/hooks'
import { Link, TYPE } from '../../theme'
import { Text } from 'rebass'
import { LightCard } from '../../components/Card'
......@@ -14,7 +15,6 @@ import { ButtonPrimary, ButtonSecondary } from '../../components/Button'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { useWeb3React } from '@web3-react/core'
import { useAllBalances, useAccountLPBalances } from '../../contexts/Balances'
import { usePair } from '../../data/Reserves'
import { useAllDummyPairs } from '../../state/user/hooks'
......@@ -40,18 +40,17 @@ function Supply({ history }: RouteComponentProps) {
const [showPoolSearch, setShowPoolSearch] = useState(false)
// initiate listener for LP balances
const allBalances = useAllBalances()
useAccountLPBalances(account)
const pairs = useAllDummyPairs()
const pairBalances = useTokenBalances(
account,
pairs?.map(p => p.liquidityToken)
)
const filteredExchangeList = pairs
.filter(pair => {
return (
allBalances &&
allBalances[account] &&
allBalances[account][pair.liquidityToken.address] &&
JSBI.greaterThan(allBalances[account][pair.liquidityToken.address].raw, JSBI.BigInt(0))
pairBalances?.[pair.liquidityToken.address] &&
JSBI.greaterThan(pairBalances[pair.liquidityToken.address].raw, JSBI.BigInt(0))
)
})
.map((pair, i) => {
......
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 transactions from './transactions/reducer'
import { save, load } from 'redux-localstorage-simple'
......@@ -10,12 +12,15 @@ const store = configureStore({
reducer: {
application,
user,
transactions
transactions,
wallet
},
middleware: [...getDefaultMiddleware(), save({ states: PERSISTED_KEYS })],
preloadedState: load({ states: PERSISTED_KEYS })
})
store.dispatch(updateVersion())
export default store
export type AppState = ReturnType<typeof store.getState>
......
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { AppDispatch } from '../index'
import { updateMatchesDarkMode, updateVersion } from './actions'
import { updateMatchesDarkMode } from './actions'
export default function Updater() {
const dispatch = useDispatch<AppDispatch>()
useEffect(() => {
dispatch(updateVersion())
}, [dispatch])
// keep dark mode in sync with the system
useEffect(() => {
const darkHandler = (match: MediaQueryListEvent) => {
......
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 { JSBI, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useEffect, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useAllTokens } from '../../contexts/Tokens'
import { useWeb3React } from '../../hooks'
import { isAddress } from '../../utils'
import { AppDispatch, AppState } from '../index'
import {
startListeningForBalance,
startListeningForTokenBalances,
stopListeningForBalance,
stopListeningForTokenBalances,
TokenBalanceListenerKey
} from './actions'
import { balanceKey } from './reducer'
/**
* 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 } = useWeb3React()
const addresses: string[] = useMemo(() => (uncheckedAddresses ? uncheckedAddresses.filter(isAddress) : []), [
uncheckedAddresses
])
// add the listeners on mount, remove them on dismount
useEffect(() => {
if (addresses.length === 0) return
dispatch(startListeningForBalance({ addresses }))
return () => dispatch(stopListeningForBalance({ addresses }))
}, [addresses, dispatch])
const rawBalanceMap = useSelector<AppState>(({ wallet: { balances } }) => balances)
return useMemo(() => {
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])
}
/**
* Returns a map of token addresses to their eventually consistent token balances for a single account.
*/
export function useTokenBalances(
address?: string,
tokens?: (Token | undefined)[]
): { [tokenAddress: string]: TokenAmount | undefined } {
const dispatch = useDispatch<AppDispatch>()
const { chainId } = useWeb3React()
const validTokens: Token[] = useMemo(() => tokens?.filter(t => isAddress(t?.address)) ?? [], [tokens])
// keep the listeners up to date
useEffect(() => {
if (address && validTokens.length > 0) {
const combos: TokenBalanceListenerKey[] = validTokens.map(token => ({ address, tokenAddress: token.address }))
dispatch(startListeningForTokenBalances(combos))
return () => dispatch(stopListeningForTokenBalances(combos))
}
}, [address, validTokens, dispatch])
const rawBalanceMap = useSelector<AppState>(({ wallet: { balances } }) => balances)
return useMemo(() => {
if (!address || validTokens.length === 0) {
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])
}
// contains the hacky logic to treat the WETH token input as if it's ETH to
// maintain compatibility until we handle them separately.
export function useTokenBalancesTreatWETHAsETH(
address?: string,
tokens?: (Token | undefined)[]
): { [tokenAddress: string]: TokenAmount | undefined } {
const { chainId } = useWeb3React()
const { tokensWithoutWETH, includesWETH } = useMemo(() => {
if (!tokens || tokens.length === 0) {
return { includesWETH: false, tokensWithoutWETH: [] }
}
let includesWETH = false
const tokensWithoutWETH = tokens.filter(t => {
const isWETH = t?.equals(WETH[chainId]) ?? false
if (isWETH) includesWETH = true
return !isWETH
})
return { includesWETH, tokensWithoutWETH }
}, [tokens, chainId])
const balancesWithoutWETH = useTokenBalances(address, tokensWithoutWETH)
const ETHBalance = useETHBalances(includesWETH ? [address] : [])
return useMemo(() => {
if (includesWETH) {
const weth = WETH[chainId]
const ethBalance = ETHBalance[address]
return {
...balancesWithoutWETH,
...(ethBalance && weth ? { [weth.address]: new TokenAmount(weth, ethBalance) } : null)
}
} else {
return balancesWithoutWETH
}
}, [balancesWithoutWETH, ETHBalance, includesWETH, address, chainId])
}
// get the balance for a single token/account combo
export function useTokenBalance(account?: string, token?: Token): TokenAmount | undefined {
return useTokenBalances(account, [token])?.[token?.address]
}
// mimics the behavior of useAddressBalance
export function useTokenBalanceTreatingWETHasETH(account?: string, token?: Token): TokenAmount | undefined {
const balances = useTokenBalancesTreatWETHAsETH(account, [token])
return token && token.address && balances?.[token.address]
}
// mimics useAllBalances
export function useAllTokenBalancesTreatingWETHasETH(): {
[account: string]: { [tokenAddress: string]: TokenAmount | undefined }
} {
const { account } = useWeb3React()
const allTokens = useAllTokens()
const allTokensArray = useMemo(() => Object.values(allTokens ?? {}), [allTokens])
const balances = useTokenBalancesTreatWETHAsETH(account, allTokensArray)
return account ? { [account]: balances } : {}
}
import { createReducer } from '@reduxjs/toolkit'
import { isAddress } from '../../utils'
import {
startListeningForBalance,
startListeningForTokenBalances,
stopListeningForBalance,
stopListeningForTokenBalances,
updateEtherBalances,
updateTokenBalances
} from './actions'
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 => {
if (!isAddress(combo.tokenAddress) || !isAddress(combo.address)) {
console.error('invalid combo', combo)
return
}
state.tokenBalanceListeners[combo.address] = state.tokenBalanceListeners[combo.address] ?? {}
state.tokenBalanceListeners[combo.address][combo.tokenAddress] =
(state.tokenBalanceListeners[combo.address][combo.tokenAddress] ?? 0) + 1
})
})
.addCase(stopListeningForTokenBalances, (state, { payload: combos }) => {
combos.forEach(combo => {
if (!isAddress(combo.tokenAddress) || !isAddress(combo.address)) {
console.error('invalid combo', combo)
return
}
if (!state.tokenBalanceListeners[combo.address]) return
if (!state.tokenBalanceListeners[combo.address][combo.tokenAddress]) return
if (state.tokenBalanceListeners[combo.address][combo.tokenAddress] === 1) {
delete state.tokenBalanceListeners[combo.address][combo.tokenAddress]
} else {
state.tokenBalanceListeners[combo.address][combo.tokenAddress]--
}
})
})
.addCase(startListeningForBalance, (state, { payload: { addresses } }) => {
addresses.forEach(address => {
if (!isAddress(address)) {
console.error('invalid address', address)
return
}
state.balanceListeners[address] = (state.balanceListeners[address] ?? 0) + 1
})
})
.addCase(stopListeningForBalance, (state, { payload: { addresses } }) => {
addresses.forEach(address => {
if (!isAddress(address)) {
console.error('invalid address', address)
return
}
if (!state.balanceListeners[address]) return
if (state.balanceListeners[address] === 1) {
delete state.balanceListeners[address]
} else {
state.balanceListeners[address]--
}
})
})
.addCase(updateTokenBalances, (state, { payload: { chainId, address, blockNumber, tokenBalances } }) => {
Object.keys(tokenBalances).forEach(tokenAddress => {
const balance = tokenBalances[tokenAddress]
const key = balanceKey({ chainId, address, tokenAddress })
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 balance = etherBalances[address]
const key = balanceKey({ chainId, address })
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 { useWeb3React } 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 } = useWeb3React()
const lastBlockNumber = useBlockNumber()
const dispatch = useDispatch<AppDispatch>()
const ethBalanceListeners = useSelector<AppState>(state => {
return state.wallet.balanceListeners
})
const tokenBalanceListeners = useSelector<AppState>(state => {
return state.wallet.tokenBalanceListeners
})
const allBalances = useSelector<AppState>(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(() => {
return activeETHListeners.filter(address => {
const data = allBalances[balanceKey({ chainId, address })]
return !data || data.blockNumber < lastBlockNumber
})
}, [activeETHListeners, allBalances, chainId, lastBlockNumber])
const tokenBalancesNeedUpdate: { [address: string]: string[] } = useMemo(() => {
return Object.keys(activeTokenBalanceListeners).reduce<{ [address: string]: string[] }>((map, address) => {
const needsUpdate =
activeTokenBalanceListeners[address]?.filter(tokenAddress => {
const data = allBalances[balanceKey({ chainId, tokenAddress, address })]
return !data || data.blockNumber < lastBlockNumber
}) ?? []
if (needsUpdate.length > 0) {
map[address] = needsUpdate
}
return map
}, {})
}, [activeTokenBalanceListeners, allBalances, chainId, lastBlockNumber])
useEffect(() => {
if (!library) return
if (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) 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
}
......@@ -1349,6 +1349,21 @@
"@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"
......@@ -1453,7 +1468,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.138", "@ethersproject/bignumber@^5.0.0-beta.135":
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==
......@@ -2293,6 +2308,15 @@
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"
......@@ -10322,6 +10346,14 @@ 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"
......@@ -12304,6 +12336,11 @@ 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"
......@@ -16984,6 +17021,11 @@ 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