Commit d4011f73 authored by Moody Salem's avatar Moody Salem

fix(multicall): return loading states from the multicall hooks #842

parent 6fc31579
...@@ -8,7 +8,7 @@ export function useTokenAllowance(token?: Token, owner?: string, spender?: strin ...@@ -8,7 +8,7 @@ export function useTokenAllowance(token?: Token, owner?: string, spender?: strin
const contract = useTokenContract(token?.address, false) const contract = useTokenContract(token?.address, false)
const inputs = useMemo(() => [owner, spender], [owner, spender]) const inputs = useMemo(() => [owner, spender], [owner, spender])
const allowance = useSingleCallResult(contract, 'allowance', inputs) const allowance = useSingleCallResult(contract, 'allowance', inputs).result
return useMemo(() => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), [ return useMemo(() => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), [
token, token,
......
...@@ -12,13 +12,13 @@ import { useSingleCallResult } from '../state/multicall/hooks' ...@@ -12,13 +12,13 @@ import { useSingleCallResult } from '../state/multicall/hooks'
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null { 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 contract = usePairContract(pairAddress, false)
const reserves = useSingleCallResult(contract, 'getReserves') const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
return useMemo(() => { return useMemo(() => {
if (!pairAddress || !contract || !tokenA || !tokenB) return undefined if (loading || !tokenA || !tokenB) return undefined
if (!reserves) return null if (!reserves) return null
const { reserve0, reserve1 } = reserves const { reserve0, reserve1 } = reserves
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA] const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString())) return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
}, [contract, pairAddress, reserves, tokenA, tokenB]) }, [loading, reserves, tokenA, tokenB])
} }
...@@ -8,7 +8,7 @@ import { useSingleCallResult } from '../state/multicall/hooks' ...@@ -8,7 +8,7 @@ import { useSingleCallResult } from '../state/multicall/hooks'
export function useTotalSupply(token?: Token): TokenAmount | undefined { export function useTotalSupply(token?: Token): TokenAmount | undefined {
const contract = useTokenContract(token?.address, false) const contract = useTokenContract(token?.address, false)
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.[0] const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0]
return token && totalSupply ? new TokenAmount(token, totalSupply.toString()) : undefined return token && totalSupply ? new TokenAmount(token, totalSupply.toString()) : undefined
} }
...@@ -12,7 +12,11 @@ function useV1PairAddress(tokenAddress?: string): string | undefined { ...@@ -12,7 +12,11 @@ function useV1PairAddress(tokenAddress?: string): string | undefined {
return useSingleCallResult(contract, 'getExchange', inputs)?.[0] return useSingleCallResult(contract, 'getExchange', inputs)?.[0]
} }
function useMockV1Pair(token?: Token) { class MockV1Pair extends Pair {
readonly isV1: true = true
}
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
const isWETH = token?.equals(WETH[token?.chainId]) const isWETH = token?.equals(WETH[token?.chainId])
// will only return an address on mainnet, and not for WETH // will only return an address on mainnet, and not for WETH
...@@ -21,7 +25,7 @@ function useMockV1Pair(token?: Token) { ...@@ -21,7 +25,7 @@ function useMockV1Pair(token?: Token) {
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? ''] const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
return tokenBalance && ETHBalance && token return tokenBalance && ETHBalance && token
? new Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString())) ? new MockV1Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
: undefined : undefined
} }
......
import { Interface } from '@ethersproject/abi' import { Interface, FunctionFragment } from '@ethersproject/abi'
import { BigNumber } from '@ethersproject/bignumber' import { BigNumber } from '@ethersproject/bignumber'
import { Contract } from '@ethersproject/contracts' import { Contract } from '@ethersproject/contracts'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks' import { useActiveWeb3React } from '../../hooks'
import useDebounce from '../../hooks/useDebounce' import useDebounce from '../../hooks/useDebounce'
import { useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index' import { AppDispatch, AppState } from '../index'
import { addMulticallListeners, Call, removeMulticallListeners, parseCallKey, toCallKey } from './actions' import { addMulticallListeners, Call, removeMulticallListeners, parseCallKey, toCallKey } from './actions'
...@@ -27,8 +28,16 @@ function isValidMethodArgs(x: unknown): x is MethodArgs | undefined { ...@@ -27,8 +28,16 @@ function isValidMethodArgs(x: unknown): x is MethodArgs | undefined {
) )
} }
interface CallResult {
readonly valid: boolean
readonly data: string | undefined
readonly blockNumber: number | undefined
}
const INVALID_RESULT: CallResult = { valid: false, blockNumber: undefined, data: undefined }
// the lowest level call for subscribing to contract data // the lowest level call for subscribing to contract data
function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] { function useCallsData(calls: (Call | undefined)[]): CallResult[] {
const { chainId } = useActiveWeb3React() const { chainId } = useActiveWeb3React()
const callResults = useSelector<AppState, AppState['multicall']['callResults']>(state => state.multicall.callResults) const callResults = useSelector<AppState, AppState['multicall']['callResults']>(state => state.multicall.callResults)
const dispatch = useDispatch<AppDispatch>() const dispatch = useDispatch<AppDispatch>()
...@@ -68,25 +77,64 @@ function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] { ...@@ -68,25 +77,64 @@ function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
} }
}, [chainId, dispatch, debouncedSerializedCallKeys]) }, [chainId, dispatch, debouncedSerializedCallKeys])
return useMemo(() => { return useMemo(
return calls.map<string | undefined>(call => { () =>
if (!chainId || !call) return undefined calls.map<CallResult>(call => {
if (!chainId || !call) return INVALID_RESULT
const result = callResults[chainId]?.[toCallKey(call)] const result = callResults[chainId]?.[toCallKey(call)]
if (!result || !result.data || result.data === '0x') { let data
return undefined if (result?.data && result?.data !== '0x') {
data = result.data
} }
return result.data return { valid: true, data, blockNumber: result?.blockNumber }
}) }),
}, [callResults, calls, chainId]) [callResults, calls, chainId]
)
}
interface CallState {
readonly valid: boolean
// the result, or undefined if loading or errored/no data
readonly result: Result | undefined
// true if the result has never been fetched
readonly loading: boolean
// true if the result is not for the latest block
readonly syncing: boolean
// true if the call was made and is synced, but the return data is invalid
readonly error: boolean
}
const INVALID_CALL_STATE: CallState = { valid: false, result: undefined, loading: false, syncing: false, error: false }
const LOADING_CALL_STATE: CallState = { valid: true, result: undefined, loading: true, syncing: true, error: false }
function toCallState(
result: CallResult | undefined,
contractInterface: Interface | undefined,
fragment: FunctionFragment | undefined,
latestBlockNumber: number | undefined
): CallState {
if (!result) return INVALID_CALL_STATE
const { valid, data, blockNumber } = result
if (!valid) return INVALID_CALL_STATE
if (valid && !blockNumber) return LOADING_CALL_STATE
if (!contractInterface || !fragment || !latestBlockNumber) return LOADING_CALL_STATE
const success = data && data.length > 2
return {
valid: true,
loading: false,
syncing: (blockNumber ?? 0) < latestBlockNumber,
result: success && data ? contractInterface.decodeFunctionResult(fragment, data) : undefined,
error: !success
}
} }
export function useSingleContractMultipleData( export function useSingleContractMultipleData(
contract: Contract | null | undefined, contract: Contract | null | undefined,
methodName: string, methodName: string,
callInputs: OptionalMethodInputs[] callInputs: OptionalMethodInputs[]
): (Result | undefined)[] { ): CallState[] {
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName]) const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
const calls = useMemo( const calls = useMemo(
...@@ -102,12 +150,13 @@ export function useSingleContractMultipleData( ...@@ -102,12 +150,13 @@ export function useSingleContractMultipleData(
[callInputs, contract, fragment] [callInputs, contract, fragment]
) )
const data = useCallsData(calls) const results = useCallsData(calls)
const latestBlockNumber = useBlockNumber()
return useMemo(() => { return useMemo(() => {
if (!fragment || !contract) return [] return results.map(result => toCallState(result, contract?.interface, fragment, latestBlockNumber))
return data.map(data => (data ? contract.interface.decodeFunctionResult(fragment, data) : undefined)) }, [fragment, contract, results, latestBlockNumber])
}, [contract, data, fragment])
} }
export function useMultipleContractSingleData( export function useMultipleContractSingleData(
...@@ -115,7 +164,7 @@ export function useMultipleContractSingleData( ...@@ -115,7 +164,7 @@ export function useMultipleContractSingleData(
contractInterface: Interface, contractInterface: Interface,
methodName: string, methodName: string,
callInputs?: OptionalMethodInputs callInputs?: OptionalMethodInputs
): (Result | undefined)[] { ): CallState[] {
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName]) const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
const callData: string | undefined = useMemo( const callData: string | undefined = useMemo(
() => () =>
...@@ -140,19 +189,20 @@ export function useMultipleContractSingleData( ...@@ -140,19 +189,20 @@ export function useMultipleContractSingleData(
[addresses, callData, fragment] [addresses, callData, fragment]
) )
const data = useCallsData(calls) const results = useCallsData(calls)
const latestBlockNumber = useBlockNumber()
return useMemo(() => { return useMemo(() => {
if (!fragment) return [] return results.map(result => toCallState(result, contractInterface, fragment, latestBlockNumber))
return data.map(data => (data ? contractInterface.decodeFunctionResult(fragment, data) : undefined)) }, [fragment, results, contractInterface, latestBlockNumber])
}, [contractInterface, data, fragment])
} }
export function useSingleCallResult( export function useSingleCallResult(
contract: Contract | null | undefined, contract: Contract | null | undefined,
methodName: string, methodName: string,
inputs?: OptionalMethodInputs inputs?: OptionalMethodInputs
): Result | undefined { ): CallState {
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName]) const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
const calls = useMemo<Call[]>(() => { const calls = useMemo<Call[]>(() => {
...@@ -166,9 +216,10 @@ export function useSingleCallResult( ...@@ -166,9 +216,10 @@ export function useSingleCallResult(
: [] : []
}, [contract, fragment, inputs]) }, [contract, fragment, inputs])
const data = useCallsData(calls)[0] const result = useCallsData(calls)[0]
const latestBlockNumber = useBlockNumber()
return useMemo(() => { return useMemo(() => {
if (!contract || !fragment || !data) return undefined return toCallState(result, contract?.interface, fragment, latestBlockNumber)
return contract.interface.decodeFunctionResult(fragment, data) }, [result, contract, fragment, latestBlockNumber])
}, [data, fragment, contract])
} }
...@@ -33,7 +33,7 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [ ...@@ -33,7 +33,7 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
return useMemo( return useMemo(
() => () =>
addresses.reduce<{ [address: string]: JSBI | undefined }>((memo, address, i) => { addresses.reduce<{ [address: string]: JSBI | undefined }>((memo, address, i) => {
const value = results?.[i]?.[0] const value = results?.[i]?.result?.[0]
if (value) memo[address] = JSBI.BigInt(value.toString()) if (value) memo[address] = JSBI.BigInt(value.toString())
return memo return memo
}, {}), }, {}),
...@@ -61,7 +61,7 @@ export function useTokenBalances( ...@@ -61,7 +61,7 @@ export function useTokenBalances(
() => () =>
address && validatedTokens.length > 0 address && validatedTokens.length > 0
? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => { ? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => {
const value = balances?.[i]?.[0] const value = balances?.[i]?.result?.[0]
const amount = value ? JSBI.BigInt(value.toString()) : undefined const amount = value ? JSBI.BigInt(value.toString()) : undefined
if (amount) { if (amount) {
memo[token.address] = new TokenAmount(token, amount) memo[token.address] = new TokenAmount(token, amount)
......
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