Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: type multicall usage #380

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 38 additions & 15 deletions src/hooks/multicall.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Contract } from '@ethersproject/contracts'
import { CallState as CallStateBase } from '@uniswap/redux-multicall'
import { useWeb3React } from '@web3-react/core'
import { Interface } from 'ethers/lib/utils'
Copy link
Contributor Author

@zzmp zzmp Jan 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be @ethersproject/contract-interfaces or something similar, not from the ethers umbrella package.

import useBlockNumber from 'hooks/useBlockNumber'
import multicall from 'state/multicall'

Expand All @@ -7,33 +10,53 @@ export { NEVER_RELOAD } from '@uniswap/redux-multicall' // re-export for conveni

// Create wrappers for hooks so consumers don't need to get latest block themselves

type MulticallParams<T extends (chainId: number | undefined, latestBlock: number | undefined, ...args: any) => any> =
Parameters<T> extends [any, any, ...infer P] ? P : never
interface CallState<C extends Contract, M extends keyof C['functions']> extends Omit<CallStateBase, 'result'> {
result: C['functions'][M] extends (...args: any) => Promise<infer T> ? T | undefined : never
}

type MultipleContractParams<
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that *ContractParams is actually lossy, because it loses the names/documentation around the parameters and only retains their typing. Arguably, this should be explicit so that names are retained for a better devX. wdyt?

M extends string,
T extends (
chainId: number | undefined,
latestBlock: number | undefined,
addresses: (string | undefined)[],
contractInterface: Interface,
methodName: M,
...args: any
) => any
> = Parameters<T> extends [any, any, ...infer P] ? P : never

export function useMultipleContractSingleData(
...args: MulticallParams<typeof multicall.hooks.useMultipleContractSingleData>
export function useMultipleContractSingleData<C extends Contract, M extends string>(
...args: MultipleContractParams<M, typeof multicall.hooks.useMultipleContractSingleData>
) {
const { chainId, latestBlock } = useCallContext()
return multicall.hooks.useMultipleContractSingleData(chainId, latestBlock, ...args)
return multicall.hooks.useMultipleContractSingleData(chainId, latestBlock, ...args) as CallState<C, M>[]
}

export function useSingleCallResult(...args: MulticallParams<typeof multicall.hooks.useSingleCallResult>) {
const { chainId, latestBlock } = useCallContext()
return multicall.hooks.useSingleCallResult(chainId, latestBlock, ...args)
}
type SingleContractParams<
C extends Contract,
M extends string,
T extends (
chainId: number | undefined,
latestBlock: number | undefined,
contract: C | null | undefined,
methodName: M,
...args: any
) => any
> = Parameters<T> extends [any, any, ...infer P] ? P : never

export function useSingleContractMultipleData(
...args: MulticallParams<typeof multicall.hooks.useSingleContractMultipleData>
export function useSingleCallResult<C extends Contract, M extends string>(
...args: SingleContractParams<C, M, typeof multicall.hooks.useSingleCallResult>
) {
const { chainId, latestBlock } = useCallContext()
return multicall.hooks.useSingleContractMultipleData(chainId, latestBlock, ...args)
return multicall.hooks.useSingleCallResult(chainId, latestBlock, ...args) as CallState<C, M>
}

export function useSingleContractWithCallData(
...args: MulticallParams<typeof multicall.hooks.useSingleContractWithCallData>
export function useSingleContractMultipleData<C extends Contract, M extends string>(
...args: SingleContractParams<C, M, typeof multicall.hooks.useSingleContractMultipleData>
) {
const { chainId, latestBlock } = useCallContext()
return multicall.hooks.useSingleContractWithCallData(chainId, latestBlock, ...args)
return multicall.hooks.useSingleContractMultipleData(chainId, latestBlock, ...args) as CallState<C, M>[]
}

function useCallContext() {
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useArgentWalletContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import useIsArgentWallet from './useIsArgentWallet'
export function useArgentWalletContract(): ArgentWalletContract | null {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type is now implied, and can be removed from the function definition.

const { account } = useWeb3React()
const isArgentWallet = useIsArgentWallet()
return useContract(
return useContract<ArgentWalletContract>(
isArgentWallet ? account ?? undefined : undefined,
ArgentWalletContractABI,
true
) as ArgentWalletContract
)
}
12 changes: 6 additions & 6 deletions src/hooks/useContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ENS_PUBLIC_RESOLVER_ABI from 'abis/ens-public-resolver.json'
import ENS_ABI from 'abis/ens-registrar.json'
import ERC20_ABI from 'abis/erc20.json'
import ERC20_BYTES32_ABI from 'abis/erc20_bytes32.json'
import { ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Erc20, Weth } from 'abis/types'
import { ArgentWalletDetector, Eip2612, EnsPublicResolver, EnsRegistrar, Erc20, Erc20Bytes32, Weth } from 'abis/types'
import WETH_ABI from 'abis/weth.json'
import { ARGENT_WALLET_DETECTOR_ADDRESS, ENS_REGISTRAR_ADDRESSES, MULTICALL_ADDRESS } from 'constants/addresses'
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
Expand All @@ -18,7 +18,7 @@ import { getContract } from 'utils'
const { abi: MulticallABI } = UniswapInterfaceMulticallJson

// returns null on errors
export function useContract<T extends Contract = Contract>(
export function useContract<T extends Contract>(
addressOrAddressMap: string | { [chainId: number]: string } | undefined,
ABI: any,
withSignerIfPossible = true
Expand Down Expand Up @@ -65,12 +65,12 @@ export function useENSResolverContract(address: string | undefined, withSignerIf
return useContract<EnsPublicResolver>(address, ENS_PUBLIC_RESOLVER_ABI, withSignerIfPossible)
}

export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean) {
return useContract<Erc20Bytes32>(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
}

export function useEIP2612Contract(tokenAddress?: string): Contract | null {
return useContract(tokenAddress, EIP_2612, false)
export function useEIP2612Contract(tokenAddress?: string) {
return useContract<Eip2612>(tokenAddress, EIP_2612, false)
}

export function useInterfaceMulticall() {
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useCurrencyBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Interface } from '@ethersproject/abi'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import ERC20ABI from 'abis/erc20.json'
import { Erc20Interface } from 'abis/types/Erc20'
import { Erc20, Erc20Interface } from 'abis/types/Erc20'
import { nativeOnChain } from 'constants/tokens'
import { useMultipleContractSingleData, useSingleContractMultipleData } from 'hooks/multicall'
import { useInterfaceMulticall } from 'hooks/useContract'
Expand Down Expand Up @@ -61,7 +61,7 @@ export function useTokenBalancesWithLoadingIndicator(
)
const validatedTokenAddresses = useMemo(() => validatedTokens.map((vt) => vt.address), [validatedTokens])

const balances = useMultipleContractSingleData(
const balances = useMultipleContractSingleData<Erc20, 'balanceOf'>(
validatedTokenAddresses,
ERC20Interface,
'balanceOf',
Expand Down
4 changes: 1 addition & 3 deletions src/hooks/usePermitAllowance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ export function usePermitAllowance(token?: Token, owner?: string, spender?: stri
// If there is no allowance yet, re-check next observed block.
// This guarantees that the permitAllowance is synced upon submission and updated upon being synced.
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
const result = useSingleCallResult(contract, 'allowance', inputs, {
blocksPerFetch,
}).result as Awaited<ReturnType<Permit2['allowance']>> | undefined
const result = useSingleCallResult(contract, 'allowance', inputs, { blocksPerFetch }).result

const rawAmount = result?.amount.toString() // convert to a string before using in a hook, to avoid spurious rerenders
const allowance = useMemo(
Expand Down
6 changes: 1 addition & 5 deletions src/hooks/useTokenAllowance.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BigNumberish } from '@ethersproject/bignumber'
import { CurrencyAmount, MaxUint256, Token } from '@uniswap/sdk-core'
import { Erc20 } from 'abis/types'
import { useSingleCallResult } from 'hooks/multicall'
import { useTokenContract } from 'hooks/useContract'
import { useCallback, useEffect, useMemo, useState } from 'react'
Expand All @@ -21,10 +20,7 @@ export function useTokenAllowance(
// If there is no allowance yet, re-check next observed block.
// This guarantees that the tokenAllowance is marked isSyncing upon approval and updated upon being synced.
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs, { blocksPerFetch }) as {
result: Awaited<ReturnType<Erc20['allowance']>> | undefined
syncing: boolean
}
const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs, { blocksPerFetch })

const rawAmount = result?.toString() // convert to a string before using in a hook, to avoid spurious rerenders
const allowance = useMemo(
Expand Down