Skip to content

Commit

Permalink
fix(multicall): return loading states from the multicall hooks Uniswa…
Browse files Browse the repository at this point in the history
  • Loading branch information
moodysalem committed May 29, 2020
1 parent 2a88276 commit 49acebc
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 36 deletions.
2 changes: 1 addition & 1 deletion src/data/Allowances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function useTokenAllowance(token?: Token, owner?: string, spender?: strin
const contract = useTokenContract(token?.address, false)

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), [
token,
Expand Down
6 changes: 3 additions & 3 deletions src/data/Reserves.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { useSingleCallResult } from '../state/multicall/hooks'
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
const contract = usePairContract(pairAddress, false)
const reserves = useSingleCallResult(contract, 'getReserves')
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')

return useMemo(() => {
if (!pairAddress || !contract || !tokenA || !tokenB) return undefined
if (loading || !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])
}, [loading, reserves, tokenA, tokenB])
}
2 changes: 1 addition & 1 deletion src/data/TotalSupply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useSingleCallResult } from '../state/multicall/hooks'
export function useTotalSupply(token?: Token): TokenAmount | undefined {
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
}
8 changes: 6 additions & 2 deletions src/data/V1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ function useV1PairAddress(tokenAddress?: string): string | undefined {
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])

// will only return an address on mainnet, and not for WETH
Expand All @@ -21,7 +25,7 @@ function useMockV1Pair(token?: Token) {
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']

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
}

Expand Down
105 changes: 78 additions & 27 deletions src/state/multicall/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Interface } from '@ethersproject/abi'
import { Interface, FunctionFragment } 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 { useBlockNumber } from '../application/hooks'
import { AppDispatch, AppState } from '../index'
import { addMulticallListeners, Call, removeMulticallListeners, parseCallKey, toCallKey } from './actions'

Expand All @@ -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
function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
function useCallsData(calls: (Call | undefined)[]): CallResult[] {
const { chainId } = useActiveWeb3React()
const callResults = useSelector<AppState, AppState['multicall']['callResults']>(state => state.multicall.callResults)
const dispatch = useDispatch<AppDispatch>()
Expand Down Expand Up @@ -68,25 +77,64 @@ function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
}
}, [chainId, dispatch, debouncedSerializedCallKeys])

return useMemo(() => {
return calls.map<string | undefined>(call => {
if (!chainId || !call) return undefined
return useMemo(
() =>
calls.map<CallResult>(call => {
if (!chainId || !call) return INVALID_RESULT

const result = callResults[chainId]?.[toCallKey(call)]
let data
if (result?.data && result?.data !== '0x') {
data = result.data
}

return { valid: true, data, blockNumber: result?.blockNumber }
}),
[callResults, calls, chainId]
)
}

const result = callResults[chainId]?.[toCallKey(call)]
if (!result || !result.data || result.data === '0x') {
return undefined
}
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
}

return result.data
})
}, [callResults, calls, chainId])
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(
contract: Contract | null | undefined,
methodName: string,
callInputs: OptionalMethodInputs[]
): (Result | undefined)[] {
): CallState[] {
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])

const calls = useMemo(
Expand All @@ -102,20 +150,21 @@ export function useSingleContractMultipleData(
[callInputs, contract, fragment]
)

const data = useCallsData(calls)
const results = useCallsData(calls)

const latestBlockNumber = useBlockNumber()

return useMemo(() => {
if (!fragment || !contract) return []
return data.map(data => (data ? contract.interface.decodeFunctionResult(fragment, data) : undefined))
}, [contract, data, fragment])
return results.map(result => toCallState(result, contract?.interface, fragment, latestBlockNumber))
}, [fragment, contract, results, latestBlockNumber])
}

export function useMultipleContractSingleData(
addresses: (string | undefined)[],
contractInterface: Interface,
methodName: string,
callInputs?: OptionalMethodInputs
): (Result | undefined)[] {
): CallState[] {
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
const callData: string | undefined = useMemo(
() =>
Expand All @@ -140,19 +189,20 @@ export function useMultipleContractSingleData(
[addresses, callData, fragment]
)

const data = useCallsData(calls)
const results = useCallsData(calls)

const latestBlockNumber = useBlockNumber()

return useMemo(() => {
if (!fragment) return []
return data.map(data => (data ? contractInterface.decodeFunctionResult(fragment, data) : undefined))
}, [contractInterface, data, fragment])
return results.map(result => toCallState(result, contractInterface, fragment, latestBlockNumber))
}, [fragment, results, contractInterface, latestBlockNumber])
}

export function useSingleCallResult(
contract: Contract | null | undefined,
methodName: string,
inputs?: OptionalMethodInputs
): Result | undefined {
): CallState {
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])

const calls = useMemo<Call[]>(() => {
Expand All @@ -166,9 +216,10 @@ export function useSingleCallResult(
: []
}, [contract, fragment, inputs])

const data = useCallsData(calls)[0]
const result = useCallsData(calls)[0]
const latestBlockNumber = useBlockNumber()

return useMemo(() => {
if (!contract || !fragment || !data) return undefined
return contract.interface.decodeFunctionResult(fragment, data)
}, [data, fragment, contract])
return toCallState(result, contract?.interface, fragment, latestBlockNumber)
}, [result, contract, fragment, latestBlockNumber])
}
4 changes: 2 additions & 2 deletions src/state/wallet/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
return useMemo(
() =>
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())
return memo
}, {}),
Expand Down Expand Up @@ -61,7 +61,7 @@ export function useTokenBalances(
() =>
address && validatedTokens.length > 0
? 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
if (amount) {
memo[token.address] = new TokenAmount(token, amount)
Expand Down

0 comments on commit 49acebc

Please sign in to comment.