refactor: Replace multicall implementation with library (#2768)
- Replace the local implementation of multicall with the new redux-multicall lib - Create wrappers for redux-multicall hooks to inject block number and chainId
This commit is contained in:
parent
e81e8a8f71
commit
596ea03043
@ -58,6 +58,7 @@
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
"@uniswap/redux-multicall": "^1.0.0",
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.27",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { useMemo } from 'react'
|
||||
import { Result, useSingleCallResult, useSingleContractMultipleData } from 'state/multicall/hooks'
|
||||
import { CallStateResult, useSingleCallResult, useSingleContractMultipleData } from 'state/multicall/hooks'
|
||||
import { PositionDetails } from 'types/position'
|
||||
|
||||
import { useV3NFTPositionManagerContract } from './useContract'
|
||||
@ -22,7 +22,7 @@ function useV3PositionsFromTokenIds(tokenIds: BigNumber[] | undefined): UseV3Pos
|
||||
if (!loading && !error && tokenIds) {
|
||||
return results.map((call, i) => {
|
||||
const tokenId = tokenIds[i]
|
||||
const result = call.result as Result
|
||||
const result = call.result as CallStateResult
|
||||
return {
|
||||
tokenId,
|
||||
fee: result.fee,
|
||||
@ -90,7 +90,7 @@ export function useV3Positions(account: string | null | undefined): UseV3Positio
|
||||
if (account) {
|
||||
return tokenIdResults
|
||||
.map(({ result }) => result)
|
||||
.filter((result): result is Result => !!result)
|
||||
.filter((result): result is CallStateResult => !!result)
|
||||
.map((result) => BigNumber.from(result[0]))
|
||||
}
|
||||
return []
|
||||
|
@ -11,7 +11,7 @@ import lists from './lists/reducer'
|
||||
import logs from './logs/slice'
|
||||
import mint from './mint/reducer'
|
||||
import mintV3 from './mint/v3/reducer'
|
||||
import multicall from './multicall/reducer'
|
||||
import { multicall } from './multicall/instance'
|
||||
import { routingApi } from './routing/slice'
|
||||
import swap from './swap/reducer'
|
||||
import transactions from './transactions/reducer'
|
||||
@ -29,7 +29,7 @@ const store = configureStore({
|
||||
mintV3,
|
||||
burn,
|
||||
burnV3,
|
||||
multicall,
|
||||
multicall: multicall.reducer,
|
||||
lists,
|
||||
logs,
|
||||
[dataApi.reducerPath]: dataApi.reducer,
|
||||
|
@ -1,35 +0,0 @@
|
||||
import { parseCallKey, toCallKey } from './utils'
|
||||
|
||||
describe('actions', () => {
|
||||
describe('#parseCallKey', () => {
|
||||
it('does not throw for invalid address', () => {
|
||||
expect(parseCallKey('0x-0x')).toEqual({ address: '0x', callData: '0x' })
|
||||
})
|
||||
it('does not throw for invalid calldata', () => {
|
||||
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-abc')).toEqual({
|
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
callData: 'abc',
|
||||
})
|
||||
})
|
||||
it('throws for uppercase calldata', () => {
|
||||
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toEqual({
|
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
callData: '0xabcD',
|
||||
})
|
||||
})
|
||||
it('parses pieces into address', () => {
|
||||
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd')).toEqual({
|
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
callData: '0xabcd',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#toCallKey', () => {
|
||||
it('concatenates address to data', () => {
|
||||
expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual(
|
||||
'0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,30 +0,0 @@
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
|
||||
import { Call } from './utils'
|
||||
|
||||
export interface ListenerOptions {
|
||||
// how often this data should be fetched, by default 1
|
||||
readonly blocksPerFetch: number
|
||||
}
|
||||
|
||||
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[]; options: ListenerOptions }>(
|
||||
'multicall/addMulticallListeners'
|
||||
)
|
||||
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[]; options: ListenerOptions }>(
|
||||
'multicall/removeMulticallListeners'
|
||||
)
|
||||
export const fetchingMulticallResults = createAction<{ chainId: number; calls: Call[]; fetchingBlockNumber: number }>(
|
||||
'multicall/fetchingMulticallResults'
|
||||
)
|
||||
export const errorFetchingMulticallResults = createAction<{
|
||||
chainId: number
|
||||
calls: Call[]
|
||||
fetchingBlockNumber: number
|
||||
}>('multicall/errorFetchingMulticallResults')
|
||||
export const updateMulticallResults = createAction<{
|
||||
chainId: number
|
||||
blockNumber: number
|
||||
results: {
|
||||
[callKey: string]: string | null
|
||||
}
|
||||
}>('multicall/updateMulticallResults')
|
@ -1,298 +1,43 @@
|
||||
import { FunctionFragment, Interface } from '@ethersproject/abi'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks/web3'
|
||||
import { SkipFirst } from '../../types/tuple'
|
||||
import { useBlockNumber } from '../application/hooks'
|
||||
import { addMulticallListeners, ListenerOptions, removeMulticallListeners } from './actions'
|
||||
import { Call, parseCallKey, toCallKey } from './utils'
|
||||
import { multicall } from './instance'
|
||||
export type { CallStateResult } from '@uniswap/redux-multicall' // re-export for convenience
|
||||
export { NEVER_RELOAD } from '@uniswap/redux-multicall' // re-export for convenience
|
||||
|
||||
export interface Result extends ReadonlyArray<any> {
|
||||
readonly [key: string]: any
|
||||
const {
|
||||
useMultipleContractSingleData: _useMultipleContractSingleData,
|
||||
useSingleCallResult: _useSingleCallResult,
|
||||
useSingleContractMultipleData: _useSingleContractMultipleData,
|
||||
useSingleContractWithCallData: _useSingleContractWithCallData,
|
||||
} = multicall.hooks
|
||||
|
||||
// Create wrappers for hooks so consumers don't need to get latest block themselves
|
||||
|
||||
type SkipFirstTwoParams<T extends (...args: any) => any> = SkipFirst<Parameters<T>, 2>
|
||||
|
||||
export function useMultipleContractSingleData(...args: SkipFirstTwoParams<typeof _useMultipleContractSingleData>) {
|
||||
const { chainId, latestBlock } = useCallContext()
|
||||
return _useMultipleContractSingleData(chainId, latestBlock, ...args)
|
||||
}
|
||||
|
||||
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 BigNumber.isBigNumber(x) || ['string', 'number'].indexOf(typeof x) !== -1
|
||||
export function useSingleCallResult(...args: SkipFirstTwoParams<typeof _useSingleCallResult>) {
|
||||
const { chainId, latestBlock } = useCallContext()
|
||||
return _useSingleCallResult(chainId, latestBlock, ...args)
|
||||
}
|
||||
|
||||
function isValidMethodArgs(x: unknown): x is MethodArgs | undefined {
|
||||
return (
|
||||
x === undefined ||
|
||||
(Array.isArray(x) && x.every((xi) => isMethodArg(xi) || (Array.isArray(xi) && xi.every(isMethodArg))))
|
||||
)
|
||||
export function useSingleContractMultipleData(...args: SkipFirstTwoParams<typeof _useSingleContractMultipleData>) {
|
||||
const { chainId, latestBlock } = useCallContext()
|
||||
return _useSingleContractMultipleData(chainId, latestBlock, ...args)
|
||||
}
|
||||
|
||||
interface CallResult {
|
||||
readonly valid: boolean
|
||||
readonly data: string | undefined
|
||||
readonly blockNumber: number | undefined
|
||||
export function useSingleContractWithCallData(...args: SkipFirstTwoParams<typeof _useSingleContractWithCallData>) {
|
||||
const { chainId, latestBlock } = useCallContext()
|
||||
return _useSingleContractWithCallData(chainId, latestBlock, ...args)
|
||||
}
|
||||
|
||||
const INVALID_RESULT: CallResult = { valid: false, blockNumber: undefined, data: undefined }
|
||||
|
||||
// use this options object
|
||||
export const NEVER_RELOAD: ListenerOptions = {
|
||||
blocksPerFetch: Infinity,
|
||||
}
|
||||
|
||||
// the lowest level call for subscribing to contract data
|
||||
function useCallsData(
|
||||
calls: (Call | undefined)[],
|
||||
{ blocksPerFetch }: ListenerOptions = { blocksPerFetch: 1 }
|
||||
): CallResult[] {
|
||||
function useCallContext() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const callResults = useAppSelector((state) => state.multicall.callResults)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const serializedCallKeys: string = useMemo(
|
||||
() =>
|
||||
JSON.stringify(
|
||||
calls
|
||||
?.filter((c): c is Call => Boolean(c))
|
||||
?.map(toCallKey)
|
||||
?.sort() ?? []
|
||||
),
|
||||
[calls]
|
||||
)
|
||||
|
||||
// update listeners when there is an actual change that persists for at least 100ms
|
||||
useEffect(() => {
|
||||
const callKeys: string[] = JSON.parse(serializedCallKeys)
|
||||
if (!chainId || callKeys.length === 0) return undefined
|
||||
const calls = callKeys.map((key) => parseCallKey(key))
|
||||
dispatch(
|
||||
addMulticallListeners({
|
||||
chainId,
|
||||
calls,
|
||||
options: { blocksPerFetch },
|
||||
})
|
||||
)
|
||||
|
||||
return () => {
|
||||
dispatch(
|
||||
removeMulticallListeners({
|
||||
chainId,
|
||||
calls,
|
||||
options: { blocksPerFetch },
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [chainId, dispatch, blocksPerFetch, serializedCallKeys])
|
||||
|
||||
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]
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
callResult: CallResult | undefined,
|
||||
contractInterface: Interface | undefined,
|
||||
fragment: FunctionFragment | undefined,
|
||||
latestBlockNumber: number | undefined
|
||||
): CallState {
|
||||
if (!callResult) return INVALID_CALL_STATE
|
||||
const { valid, data, blockNumber } = callResult
|
||||
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
|
||||
const syncing = (blockNumber ?? 0) < latestBlockNumber
|
||||
let result: Result | undefined = undefined
|
||||
if (success && data) {
|
||||
try {
|
||||
result = contractInterface.decodeFunctionResult(fragment, data)
|
||||
} catch (error) {
|
||||
console.debug('Result data parsing failed', fragment, data)
|
||||
return {
|
||||
valid: true,
|
||||
loading: false,
|
||||
error: true,
|
||||
syncing,
|
||||
result,
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: true,
|
||||
loading: false,
|
||||
syncing,
|
||||
result,
|
||||
error: !success,
|
||||
}
|
||||
}
|
||||
|
||||
// formats many calls to a single function on a single contract, with the function name and inputs specified
|
||||
export function useSingleContractMultipleData(
|
||||
contract: Contract | null | undefined,
|
||||
methodName: string,
|
||||
callInputs: OptionalMethodInputs[],
|
||||
options: Partial<ListenerOptions> & { gasRequired?: number } = {}
|
||||
): CallState[] {
|
||||
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
|
||||
|
||||
// encode callDatas
|
||||
const callDatas = useMemo(
|
||||
() =>
|
||||
contract && fragment
|
||||
? callInputs.map<string | undefined>((callInput) =>
|
||||
isValidMethodArgs(callInput) ? contract.interface.encodeFunctionData(fragment, callInput) : undefined
|
||||
)
|
||||
: [],
|
||||
[callInputs, contract, fragment]
|
||||
)
|
||||
|
||||
const gasRequired = options?.gasRequired
|
||||
const blocksPerFetch = options?.blocksPerFetch
|
||||
|
||||
// encode calls
|
||||
const calls = useMemo(
|
||||
() =>
|
||||
contract
|
||||
? callDatas.map<Call | undefined>((callData) =>
|
||||
callData
|
||||
? {
|
||||
address: contract.address,
|
||||
callData,
|
||||
gasRequired,
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
: [],
|
||||
[contract, callDatas, gasRequired]
|
||||
)
|
||||
|
||||
const results = useCallsData(calls, blocksPerFetch ? { blocksPerFetch } : undefined)
|
||||
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
|
||||
return useMemo(() => {
|
||||
return results.map((result) => toCallState(result, contract?.interface, fragment, latestBlockNumber))
|
||||
}, [results, contract, fragment, latestBlockNumber])
|
||||
}
|
||||
|
||||
export function useMultipleContractSingleData(
|
||||
addresses: (string | undefined)[],
|
||||
contractInterface: Interface,
|
||||
methodName: string,
|
||||
callInputs?: OptionalMethodInputs,
|
||||
options: Partial<ListenerOptions> & { gasRequired?: number } = {}
|
||||
): CallState[] {
|
||||
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
|
||||
|
||||
// encode callData
|
||||
const callData: string | undefined = useMemo(
|
||||
() => (isValidMethodArgs(callInputs) ? contractInterface.encodeFunctionData(fragment, callInputs) : undefined),
|
||||
[callInputs, contractInterface, fragment]
|
||||
)
|
||||
|
||||
const gasRequired = options?.gasRequired
|
||||
const blocksPerFetch = options?.blocksPerFetch
|
||||
|
||||
// encode calls
|
||||
const calls = useMemo(
|
||||
() =>
|
||||
callData
|
||||
? addresses.map<Call | undefined>((address) => {
|
||||
return address
|
||||
? {
|
||||
address,
|
||||
callData,
|
||||
gasRequired,
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
: [],
|
||||
[addresses, callData, gasRequired]
|
||||
)
|
||||
|
||||
const results = useCallsData(calls, blocksPerFetch ? { blocksPerFetch } : undefined)
|
||||
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
|
||||
return useMemo(() => {
|
||||
return results.map((result) => toCallState(result, contractInterface, fragment, latestBlockNumber))
|
||||
}, [fragment, results, contractInterface, latestBlockNumber])
|
||||
}
|
||||
|
||||
export function useSingleCallResult(
|
||||
contract: Contract | null | undefined,
|
||||
methodName: string,
|
||||
inputs?: OptionalMethodInputs,
|
||||
options: Partial<ListenerOptions> & { gasRequired?: number } = {}
|
||||
): CallState {
|
||||
return useSingleContractMultipleData(contract, methodName, [inputs], options)[0] ?? INVALID_CALL_STATE
|
||||
}
|
||||
|
||||
// formats many calls to any number of functions on a single contract, with only the calldata specified
|
||||
export function useSingleContractWithCallData(
|
||||
contract: Contract | null | undefined,
|
||||
callDatas: string[],
|
||||
options: Partial<ListenerOptions> & { gasRequired?: number } = {}
|
||||
): CallState[] {
|
||||
const gasRequired = options?.gasRequired
|
||||
const blocksPerFetch = options?.blocksPerFetch
|
||||
|
||||
// encode calls
|
||||
const calls = useMemo(
|
||||
() =>
|
||||
contract
|
||||
? callDatas.map<Call>((callData) => {
|
||||
return {
|
||||
address: contract.address,
|
||||
callData,
|
||||
gasRequired,
|
||||
}
|
||||
})
|
||||
: [],
|
||||
[contract, callDatas, gasRequired]
|
||||
)
|
||||
|
||||
const results = useCallsData(calls, blocksPerFetch ? { blocksPerFetch } : undefined)
|
||||
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
|
||||
return useMemo(() => {
|
||||
return results.map((result, i) =>
|
||||
toCallState(
|
||||
result,
|
||||
contract?.interface,
|
||||
contract?.interface?.getFunction(callDatas[i].substring(0, 10)),
|
||||
latestBlockNumber
|
||||
)
|
||||
)
|
||||
}, [results, contract, callDatas, latestBlockNumber])
|
||||
const latestBlock = useBlockNumber()
|
||||
return { chainId, latestBlock }
|
||||
}
|
||||
|
4
src/state/multicall/instance.ts
Normal file
4
src/state/multicall/instance.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { createMulticall } from '@uniswap/redux-multicall'
|
||||
|
||||
// Create a multicall instance with default settings
|
||||
export const multicall = createMulticall()
|
@ -1,316 +0,0 @@
|
||||
import { createStore, Store } from '@reduxjs/toolkit'
|
||||
|
||||
import {
|
||||
addMulticallListeners,
|
||||
errorFetchingMulticallResults,
|
||||
fetchingMulticallResults,
|
||||
removeMulticallListeners,
|
||||
updateMulticallResults,
|
||||
} from './actions'
|
||||
import reducer, { MulticallState } from './reducer'
|
||||
|
||||
const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f'
|
||||
|
||||
describe('multicall reducer', () => {
|
||||
let store: Store<MulticallState>
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer)
|
||||
})
|
||||
|
||||
it('has correct initial state', () => {
|
||||
expect(store.getState().callResults).toEqual({})
|
||||
expect(store.getState().callListeners).toEqual(undefined)
|
||||
})
|
||||
|
||||
describe('addMulticallListeners', () => {
|
||||
it('adds listeners', () => {
|
||||
store.dispatch(
|
||||
addMulticallListeners({
|
||||
chainId: 1,
|
||||
calls: [
|
||||
{
|
||||
address: DAI_ADDRESS,
|
||||
callData: '0x',
|
||||
},
|
||||
],
|
||||
options: { blocksPerFetch: 1 },
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callListeners: {
|
||||
1: {
|
||||
[`${DAI_ADDRESS}-0x`]: {
|
||||
1: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
callResults: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeMulticallListeners', () => {
|
||||
it('noop', () => {
|
||||
store.dispatch(
|
||||
removeMulticallListeners({
|
||||
calls: [
|
||||
{
|
||||
address: DAI_ADDRESS,
|
||||
callData: '0x',
|
||||
},
|
||||
],
|
||||
chainId: 1,
|
||||
options: { blocksPerFetch: 1 },
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({ callResults: {}, callListeners: {} })
|
||||
})
|
||||
it('removes listeners', () => {
|
||||
store.dispatch(
|
||||
addMulticallListeners({
|
||||
chainId: 1,
|
||||
calls: [
|
||||
{
|
||||
address: DAI_ADDRESS,
|
||||
callData: '0x',
|
||||
},
|
||||
],
|
||||
options: { blocksPerFetch: 1 },
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
removeMulticallListeners({
|
||||
calls: [
|
||||
{
|
||||
address: DAI_ADDRESS,
|
||||
callData: '0x',
|
||||
},
|
||||
],
|
||||
chainId: 1,
|
||||
options: { blocksPerFetch: 1 },
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {},
|
||||
callListeners: { 1: { [`${DAI_ADDRESS}-0x`]: {} } },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateMulticallResults', () => {
|
||||
it('updates data if not present', () => {
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 1,
|
||||
results: {
|
||||
abc: '0x',
|
||||
},
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {
|
||||
abc: {
|
||||
blockNumber: 1,
|
||||
data: '0x',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
it('updates old data', () => {
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 1,
|
||||
results: {
|
||||
abc: '0x',
|
||||
},
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 2,
|
||||
results: {
|
||||
abc: '0x2',
|
||||
},
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {
|
||||
abc: {
|
||||
blockNumber: 2,
|
||||
data: '0x2',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
it('ignores late updates', () => {
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 2,
|
||||
results: {
|
||||
abc: '0x2',
|
||||
},
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 1,
|
||||
results: {
|
||||
abc: '0x1',
|
||||
},
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {
|
||||
abc: {
|
||||
blockNumber: 2,
|
||||
data: '0x2',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('fetchingMulticallResults', () => {
|
||||
it('updates state to fetching', () => {
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {
|
||||
[`${DAI_ADDRESS}-0x0`]: { fetchingBlockNumber: 2 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('updates state to fetching even if already fetching older block', () => {
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 3,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {
|
||||
[`${DAI_ADDRESS}-0x0`]: { fetchingBlockNumber: 3 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('does not do update if fetching newer block', () => {
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 1,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {
|
||||
[`${DAI_ADDRESS}-0x0`]: { fetchingBlockNumber: 2 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('errorFetchingMulticallResults', () => {
|
||||
it('does nothing if not fetching', () => {
|
||||
store.dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 1,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {},
|
||||
},
|
||||
})
|
||||
})
|
||||
it('updates block number if we were fetching', () => {
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {
|
||||
[`${DAI_ADDRESS}-0x0`]: {
|
||||
blockNumber: 2,
|
||||
// null data indicates error
|
||||
data: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
it('does nothing if not errored on latest block', () => {
|
||||
store.dispatch(
|
||||
fetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 3,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
chainId: 1,
|
||||
fetchingBlockNumber: 2,
|
||||
calls: [{ address: DAI_ADDRESS, callData: '0x0' }],
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
1: {
|
||||
[`${DAI_ADDRESS}-0x0`]: { fetchingBlockNumber: 3 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -1,133 +0,0 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
|
||||
import {
|
||||
addMulticallListeners,
|
||||
errorFetchingMulticallResults,
|
||||
fetchingMulticallResults,
|
||||
removeMulticallListeners,
|
||||
updateMulticallResults,
|
||||
} from './actions'
|
||||
import { toCallKey } from './utils'
|
||||
|
||||
export interface MulticallState {
|
||||
callListeners?: {
|
||||
// on a per-chain basis
|
||||
[chainId: number]: {
|
||||
// stores for each call key the listeners' preferences
|
||||
[callKey: string]: {
|
||||
// stores how many listeners there are per each blocks per fetch preference
|
||||
[blocksPerFetch: number]: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callResults: {
|
||||
[chainId: number]: {
|
||||
[callKey: string]: {
|
||||
data?: string | null
|
||||
blockNumber?: number
|
||||
fetchingBlockNumber?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: MulticallState = {
|
||||
callResults: {},
|
||||
}
|
||||
|
||||
export default createReducer(initialState, (builder) =>
|
||||
builder
|
||||
.addCase(
|
||||
addMulticallListeners,
|
||||
(
|
||||
state,
|
||||
{
|
||||
payload: {
|
||||
calls,
|
||||
chainId,
|
||||
options: { blocksPerFetch },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const listeners: MulticallState['callListeners'] = state.callListeners
|
||||
? state.callListeners
|
||||
: (state.callListeners = {})
|
||||
listeners[chainId] = listeners[chainId] ?? {}
|
||||
calls.forEach((call) => {
|
||||
const callKey = toCallKey(call)
|
||||
listeners[chainId][callKey] = listeners[chainId][callKey] ?? {}
|
||||
listeners[chainId][callKey][blocksPerFetch] = (listeners[chainId][callKey][blocksPerFetch] ?? 0) + 1
|
||||
})
|
||||
}
|
||||
)
|
||||
.addCase(
|
||||
removeMulticallListeners,
|
||||
(
|
||||
state,
|
||||
{
|
||||
payload: {
|
||||
chainId,
|
||||
calls,
|
||||
options: { blocksPerFetch },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const listeners: MulticallState['callListeners'] = state.callListeners
|
||||
? state.callListeners
|
||||
: (state.callListeners = {})
|
||||
|
||||
if (!listeners[chainId]) return
|
||||
calls.forEach((call) => {
|
||||
const callKey = toCallKey(call)
|
||||
if (!listeners[chainId][callKey]) return
|
||||
if (!listeners[chainId][callKey][blocksPerFetch]) return
|
||||
|
||||
if (listeners[chainId][callKey][blocksPerFetch] === 1) {
|
||||
delete listeners[chainId][callKey][blocksPerFetch]
|
||||
} else {
|
||||
listeners[chainId][callKey][blocksPerFetch]--
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
.addCase(fetchingMulticallResults, (state, { payload: { chainId, fetchingBlockNumber, calls } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
calls.forEach((call) => {
|
||||
const callKey = toCallKey(call)
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (!current) {
|
||||
state.callResults[chainId][callKey] = {
|
||||
fetchingBlockNumber,
|
||||
}
|
||||
} else {
|
||||
if ((current.fetchingBlockNumber ?? 0) >= fetchingBlockNumber) return
|
||||
state.callResults[chainId][callKey].fetchingBlockNumber = fetchingBlockNumber
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(errorFetchingMulticallResults, (state, { payload: { fetchingBlockNumber, chainId, calls } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
calls.forEach((call) => {
|
||||
const callKey = toCallKey(call)
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (!current || typeof current.fetchingBlockNumber !== 'number') return // only should be dispatched if we are already fetching
|
||||
if (current.fetchingBlockNumber <= fetchingBlockNumber) {
|
||||
delete current.fetchingBlockNumber
|
||||
current.data = null
|
||||
current.blockNumber = fetchingBlockNumber
|
||||
}
|
||||
})
|
||||
})
|
||||
.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?.blockNumber ?? 0) > blockNumber) return
|
||||
state.callResults[chainId][callKey] = {
|
||||
data: results[callKey],
|
||||
blockNumber,
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
@ -1,168 +0,0 @@
|
||||
import { activeListeningKeys, outdatedListeningKeys } from './updater'
|
||||
|
||||
describe('multicall updater', () => {
|
||||
describe('#activeListeningKeys', () => {
|
||||
it('ignores 0, returns call key to block age key', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
1: {
|
||||
abc: {
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
1: 0, // 0 listeners care about 1 block old data
|
||||
},
|
||||
},
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4,
|
||||
})
|
||||
})
|
||||
it('applies min', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
1: {
|
||||
abc: {
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
3: 1, // 1 listener cares about 3 block old data
|
||||
1: 0, // 0 listeners care about 1 block old data
|
||||
},
|
||||
},
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 3,
|
||||
})
|
||||
})
|
||||
it('works for infinity', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
1: {
|
||||
abc: {
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
1: 0, // 0 listeners care about 1 block old data
|
||||
},
|
||||
def: {
|
||||
Infinity: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4,
|
||||
def: Infinity,
|
||||
})
|
||||
})
|
||||
it('multiple keys', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
1: {
|
||||
abc: {
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
1: 0, // 0 listeners care about 1 block old data
|
||||
},
|
||||
def: {
|
||||
2: 1,
|
||||
5: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4,
|
||||
def: 2,
|
||||
})
|
||||
})
|
||||
it('ignores negative numbers', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
1: {
|
||||
abc: {
|
||||
4: 2,
|
||||
1: -1,
|
||||
[-3]: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4,
|
||||
})
|
||||
})
|
||||
it('applies min to infinity', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
1: {
|
||||
abc: {
|
||||
Infinity: 2, // 2 listeners care about any data
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
1: 0, // 0 listeners care about 1 block old data
|
||||
},
|
||||
},
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#outdatedListeningKeys', () => {
|
||||
it('returns empty if missing block number or chain id', () => {
|
||||
expect(outdatedListeningKeys({}, { abc: 2 }, undefined, undefined)).toEqual([])
|
||||
expect(outdatedListeningKeys({}, { abc: 2 }, 1, undefined)).toEqual([])
|
||||
expect(outdatedListeningKeys({}, { abc: 2 }, undefined, 1)).toEqual([])
|
||||
})
|
||||
it('returns everything for no results', () => {
|
||||
expect(outdatedListeningKeys({}, { abc: 2, def: 3 }, 1, 1)).toEqual(['abc', 'def'])
|
||||
})
|
||||
it('returns only outdated keys', () => {
|
||||
expect(outdatedListeningKeys({ 1: { abc: { data: '0x', blockNumber: 2 } } }, { abc: 1, def: 1 }, 1, 2)).toEqual([
|
||||
'def',
|
||||
])
|
||||
})
|
||||
it('returns only keys not being fetched', () => {
|
||||
expect(
|
||||
outdatedListeningKeys(
|
||||
{
|
||||
1: { abc: { data: '0x', blockNumber: 2 }, def: { fetchingBlockNumber: 2 } },
|
||||
},
|
||||
{ abc: 1, def: 1 },
|
||||
1,
|
||||
2
|
||||
)
|
||||
).toEqual([])
|
||||
})
|
||||
it('returns keys being fetched for old blocks', () => {
|
||||
expect(
|
||||
outdatedListeningKeys(
|
||||
{ 1: { abc: { data: '0x', blockNumber: 2 }, def: { fetchingBlockNumber: 1 } } },
|
||||
{ abc: 1, def: 1 },
|
||||
1,
|
||||
2
|
||||
)
|
||||
).toEqual(['def'])
|
||||
})
|
||||
it('respects blocks per fetch', () => {
|
||||
expect(
|
||||
outdatedListeningKeys(
|
||||
{ 1: { abc: { data: '0x', blockNumber: 2 }, def: { data: '0x', fetchingBlockNumber: 1 } } },
|
||||
{ abc: 2, def: 2 },
|
||||
1,
|
||||
3
|
||||
)
|
||||
).toEqual(['def'])
|
||||
})
|
||||
})
|
||||
})
|
@ -1,269 +1,12 @@
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
import { UniswapInterfaceMulticall } from 'types/v3'
|
||||
|
||||
import { useMulticall2Contract } from '../../hooks/useContract'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
import { useActiveWeb3React } from '../../hooks/web3'
|
||||
import chunkArray from '../../utils/chunkArray'
|
||||
import { retry, RetryableError } from '../../utils/retry'
|
||||
import { useBlockNumber } from '../application/hooks'
|
||||
import { AppState } from '../index'
|
||||
import { errorFetchingMulticallResults, fetchingMulticallResults, updateMulticallResults } from './actions'
|
||||
import { Call, parseCallKey, toCallKey } from './utils'
|
||||
import { multicall } from './instance'
|
||||
|
||||
const DEFAULT_CALL_GAS_REQUIRED = 1_000_000
|
||||
|
||||
/**
|
||||
* Fetches a chunk of calls, enforcing a minimum block number constraint
|
||||
* @param multicall multicall contract to fetch against
|
||||
* @param chunk chunk of calls to make
|
||||
* @param blockNumber block number passed as the block tag in the eth_call
|
||||
*/
|
||||
async function fetchChunk(
|
||||
multicall: UniswapInterfaceMulticall,
|
||||
chunk: Call[],
|
||||
blockNumber: number
|
||||
): Promise<{ success: boolean; returnData: string }[]> {
|
||||
console.debug('Fetching chunk', chunk, blockNumber)
|
||||
try {
|
||||
const { returnData } = await multicall.callStatic.multicall(
|
||||
chunk.map((obj) => ({
|
||||
target: obj.address,
|
||||
callData: obj.callData,
|
||||
gasLimit: obj.gasRequired ?? DEFAULT_CALL_GAS_REQUIRED,
|
||||
})),
|
||||
{
|
||||
// we aren't passing through the block gas limit we used to create the chunk, because it causes a problem with the integ tests
|
||||
blockTag: blockNumber,
|
||||
}
|
||||
)
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
returnData.forEach(({ gasUsed, returnData, success }, i) => {
|
||||
if (
|
||||
!success &&
|
||||
returnData.length === 2 &&
|
||||
gasUsed.gte(Math.floor((chunk[i].gasRequired ?? DEFAULT_CALL_GAS_REQUIRED) * 0.95))
|
||||
) {
|
||||
console.warn(
|
||||
`A call failed due to requiring ${gasUsed.toString()} vs. allowed ${
|
||||
chunk[i].gasRequired ?? DEFAULT_CALL_GAS_REQUIRED
|
||||
}`,
|
||||
chunk[i]
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return returnData
|
||||
} catch (error) {
|
||||
if (error.code === -32000 || error.message?.indexOf('header not found') !== -1) {
|
||||
throw new RetryableError(`header not found for block number ${blockNumber}`)
|
||||
} else if (error.code === -32603 || error.message?.indexOf('execution ran out of gas') !== -1) {
|
||||
if (chunk.length > 1) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug('Splitting a chunk in 2', chunk)
|
||||
}
|
||||
const half = Math.floor(chunk.length / 2)
|
||||
const [c0, c1] = await Promise.all([
|
||||
fetchChunk(multicall, chunk.slice(0, half), blockNumber),
|
||||
fetchChunk(multicall, chunk.slice(half, chunk.length), blockNumber),
|
||||
])
|
||||
return c0.concat(c1)
|
||||
}
|
||||
}
|
||||
console.error('Failed to fetch chunk', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* From the current all listeners state, return each call key mapped to the
|
||||
* minimum number of blocks per fetch. This is how often each key must be fetched.
|
||||
* @param allListeners the all listeners state
|
||||
* @param chainId the current chain id
|
||||
*/
|
||||
export function activeListeningKeys(
|
||||
allListeners: AppState['multicall']['callListeners'],
|
||||
chainId?: number
|
||||
): { [callKey: string]: number } {
|
||||
if (!allListeners || !chainId) return {}
|
||||
const listeners = allListeners[chainId]
|
||||
if (!listeners) return {}
|
||||
|
||||
return Object.keys(listeners).reduce<{ [callKey: string]: number }>((memo, callKey) => {
|
||||
const keyListeners = listeners[callKey]
|
||||
|
||||
memo[callKey] = Object.keys(keyListeners)
|
||||
.filter((key) => {
|
||||
const blocksPerFetch = parseInt(key)
|
||||
if (blocksPerFetch <= 0) return false
|
||||
return keyListeners[blocksPerFetch] > 0
|
||||
})
|
||||
.reduce((previousMin, current) => {
|
||||
return Math.min(previousMin, parseInt(current))
|
||||
}, Infinity)
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys that need to be refetched
|
||||
* @param callResults current call result state
|
||||
* @param listeningKeys each call key mapped to how old the data can be in blocks
|
||||
* @param chainId the current chain id
|
||||
* @param latestBlockNumber the latest block number
|
||||
*/
|
||||
export function outdatedListeningKeys(
|
||||
callResults: AppState['multicall']['callResults'],
|
||||
listeningKeys: { [callKey: string]: number },
|
||||
chainId: number | undefined,
|
||||
latestBlockNumber: number | undefined
|
||||
): string[] {
|
||||
if (!chainId || !latestBlockNumber) return []
|
||||
const results = callResults[chainId]
|
||||
// no results at all, load everything
|
||||
if (!results) return Object.keys(listeningKeys)
|
||||
|
||||
return Object.keys(listeningKeys).filter((callKey) => {
|
||||
const blocksPerFetch = listeningKeys[callKey]
|
||||
|
||||
const data = callResults[chainId][callKey]
|
||||
// no data, must fetch
|
||||
if (!data) return true
|
||||
|
||||
const minDataBlockNumber = latestBlockNumber - (blocksPerFetch - 1)
|
||||
|
||||
// already fetching it for a recent enough block, don't refetch it
|
||||
if (data.fetchingBlockNumber && data.fetchingBlockNumber >= minDataBlockNumber) return false
|
||||
|
||||
// if data is older than minDataBlockNumber, fetch it
|
||||
return !data.blockNumber || data.blockNumber < minDataBlockNumber
|
||||
})
|
||||
}
|
||||
|
||||
export default function Updater(): null {
|
||||
const dispatch = useAppDispatch()
|
||||
const state = useAppSelector((state) => state.multicall)
|
||||
// wait for listeners to settle before triggering updates
|
||||
const debouncedListeners = useDebounce(state.callListeners, 100)
|
||||
// Create Updater wrappers that pull needed info from store
|
||||
export default function Updater() {
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const multicall2Contract = useMulticall2Contract()
|
||||
const cancellations = useRef<{ blockNumber: number; cancellations: (() => void)[] }>()
|
||||
|
||||
const listeningKeys: { [callKey: string]: number } = useMemo(() => {
|
||||
return activeListeningKeys(debouncedListeners, chainId)
|
||||
}, [debouncedListeners, chainId])
|
||||
|
||||
const unserializedOutdatedCallKeys = useMemo(() => {
|
||||
return outdatedListeningKeys(state.callResults, listeningKeys, chainId, latestBlockNumber)
|
||||
}, [chainId, state.callResults, listeningKeys, latestBlockNumber])
|
||||
|
||||
const serializedOutdatedCallKeys = useMemo(
|
||||
() => JSON.stringify(unserializedOutdatedCallKeys.sort()),
|
||||
[unserializedOutdatedCallKeys]
|
||||
)
|
||||
|
||||
// todo: consider getting this information from the node we are using, e.g. block.gaslimit
|
||||
const chunkGasLimit = 100_000_000
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestBlockNumber || !chainId || !multicall2Contract) return
|
||||
|
||||
const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys)
|
||||
if (outdatedCallKeys.length === 0) return
|
||||
const calls = outdatedCallKeys.map((key) => parseCallKey(key))
|
||||
|
||||
const chunkedCalls = chunkArray(calls, chunkGasLimit)
|
||||
|
||||
if (cancellations.current && cancellations.current.blockNumber !== latestBlockNumber) {
|
||||
cancellations.current.cancellations.forEach((c) => c())
|
||||
}
|
||||
|
||||
dispatch(
|
||||
fetchingMulticallResults({
|
||||
calls,
|
||||
chainId,
|
||||
fetchingBlockNumber: latestBlockNumber,
|
||||
})
|
||||
)
|
||||
|
||||
cancellations.current = {
|
||||
blockNumber: latestBlockNumber,
|
||||
cancellations: chunkedCalls.map((chunk) => {
|
||||
const { cancel, promise } = retry(() => fetchChunk(multicall2Contract, chunk, latestBlockNumber), {
|
||||
n: Infinity,
|
||||
minWait: 1000,
|
||||
maxWait: 2500,
|
||||
})
|
||||
promise
|
||||
.then((returnData) => {
|
||||
// split the returned slice into errors and results
|
||||
const { erroredCalls, results } = chunk.reduce<{
|
||||
erroredCalls: Call[]
|
||||
results: { [callKey: string]: string | null }
|
||||
}>(
|
||||
(memo, call, i) => {
|
||||
if (returnData[i].success) {
|
||||
memo.results[toCallKey(call)] = returnData[i].returnData ?? null
|
||||
} else {
|
||||
memo.erroredCalls.push(call)
|
||||
}
|
||||
return memo
|
||||
},
|
||||
{ erroredCalls: [], results: {} }
|
||||
)
|
||||
|
||||
// dispatch any new results
|
||||
if (Object.keys(results).length > 0)
|
||||
dispatch(
|
||||
updateMulticallResults({
|
||||
chainId,
|
||||
results,
|
||||
blockNumber: latestBlockNumber,
|
||||
})
|
||||
)
|
||||
|
||||
// dispatch any errored calls
|
||||
if (erroredCalls.length > 0) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
returnData.forEach((returnData, ix) => {
|
||||
if (!returnData.success) {
|
||||
console.debug('Call failed', chunk[ix], returnData)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.debug('Calls errored in fetch', erroredCalls)
|
||||
}
|
||||
dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
calls: erroredCalls,
|
||||
chainId,
|
||||
fetchingBlockNumber: latestBlockNumber,
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
if (error.isCancelledError) {
|
||||
console.debug('Cancelled fetch for blockNumber', latestBlockNumber, chunk, chainId)
|
||||
return
|
||||
}
|
||||
console.error('Failed to fetch multicall chunk', chunk, chainId, error)
|
||||
dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
calls: chunk,
|
||||
chainId,
|
||||
fetchingBlockNumber: latestBlockNumber,
|
||||
})
|
||||
)
|
||||
})
|
||||
return cancel
|
||||
}),
|
||||
}
|
||||
}, [chainId, multicall2Contract, dispatch, serializedOutdatedCallKeys, latestBlockNumber])
|
||||
|
||||
return null
|
||||
return <multicall.Updater chainId={chainId} latestBlockNumber={latestBlockNumber} contract={multicall2Contract} />
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
export interface Call {
|
||||
address: string
|
||||
callData: string
|
||||
gasRequired?: number
|
||||
}
|
||||
|
||||
export function toCallKey(call: Call): string {
|
||||
let key = `${call.address}-${call.callData}`
|
||||
if (call.gasRequired) {
|
||||
if (!Number.isSafeInteger(call.gasRequired)) {
|
||||
throw new Error(`Invalid number: ${call.gasRequired}`)
|
||||
}
|
||||
key += `-${call.gasRequired}`
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
export function parseCallKey(callKey: string): Call {
|
||||
const pcs = callKey.split('-')
|
||||
if (![2, 3].includes(pcs.length)) {
|
||||
throw new Error(`Invalid call key: ${callKey}`)
|
||||
}
|
||||
return {
|
||||
address: pcs[0],
|
||||
callData: pcs[1],
|
||||
...(pcs[2] ? { gasRequired: Number.parseInt(pcs[2]) } : {}),
|
||||
}
|
||||
}
|
12
src/types/tuple.ts
Normal file
12
src/types/tuple.ts
Normal file
@ -0,0 +1,12 @@
|
||||
// From https://stackoverflow.com/a/67605309/1345206
|
||||
// Used for slicing tuples (e.g. picking some subset of a param type)
|
||||
|
||||
export type TupleSplit<T, N extends number, O extends readonly any[] = readonly []> = O['length'] extends N
|
||||
? [O, T]
|
||||
: T extends readonly [infer F, ...infer R]
|
||||
? TupleSplit<readonly [...R], N, readonly [...O, F]>
|
||||
: [O, T]
|
||||
|
||||
export type TakeFirst<T extends readonly any[], N extends number> = TupleSplit<T, N>[0]
|
||||
|
||||
export type SkipFirst<T extends readonly any[], N extends number> = TupleSplit<T, N>[1]
|
@ -1,17 +0,0 @@
|
||||
import chunkArray, { DEFAULT_GAS_REQUIRED } from './chunkArray'
|
||||
|
||||
describe('#chunkArray', () => {
|
||||
it('size 1', () => {
|
||||
expect(chunkArray([1, 2, 3], 1)).toEqual([[1], [2], [3]])
|
||||
expect(chunkArray([1, 2, 3], DEFAULT_GAS_REQUIRED)).toEqual([[1], [2], [3]])
|
||||
})
|
||||
it('size gt items', () => {
|
||||
expect(chunkArray([1, 2, 3], DEFAULT_GAS_REQUIRED * 3 + 1)).toEqual([[1, 2, 3]])
|
||||
})
|
||||
it('size exact half', () => {
|
||||
expect(chunkArray([1, 2, 3, 4], DEFAULT_GAS_REQUIRED * 2 + 1)).toEqual([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
])
|
||||
})
|
||||
})
|
@ -1,31 +0,0 @@
|
||||
export const DEFAULT_GAS_REQUIRED = 200_000 // the default value for calls that don't specify gasRequired
|
||||
|
||||
// chunks array into chunks
|
||||
// evenly distributes items among the chunks
|
||||
export default function chunkArray<T>(items: T[], chunkGasLimit: number): T[][] {
|
||||
const chunks: T[][] = []
|
||||
let currentChunk: T[] = []
|
||||
let currentChunkCumulativeGas = 0
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
|
||||
// calculate the gas required by the current item
|
||||
const gasRequired = (item as { gasRequired?: number })?.gasRequired ?? DEFAULT_GAS_REQUIRED
|
||||
|
||||
// if the current chunk is empty, or the current item wouldn't push it over the gas limit,
|
||||
// append the current item and increment the cumulative gas
|
||||
if (currentChunk.length === 0 || currentChunkCumulativeGas + gasRequired < chunkGasLimit) {
|
||||
currentChunk.push(item)
|
||||
currentChunkCumulativeGas += gasRequired
|
||||
} else {
|
||||
// otherwise, push the current chunk and create a new chunk
|
||||
chunks.push(currentChunk)
|
||||
currentChunk = [item]
|
||||
currentChunkCumulativeGas = gasRequired
|
||||
}
|
||||
}
|
||||
if (currentChunk.length > 0) chunks.push(currentChunk)
|
||||
|
||||
return chunks
|
||||
}
|
@ -4574,6 +4574,11 @@
|
||||
resolved "https://registry.npmjs.org/@uniswap/merkle-distributor/-/merkle-distributor-1.0.1.tgz"
|
||||
integrity sha512-5gDiTI5hrXIh5UWTrxKYjw30QQDnpl8ckDSpefldNenDlYO1RKkdUYMYpvrqGi2r7YzLYTlO6+TDlNs6O7hDRw==
|
||||
|
||||
"@uniswap/redux-multicall@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/redux-multicall/-/redux-multicall-1.0.0.tgz#0cee4448909a788ea4700e5ede75ffeba05b5d75"
|
||||
integrity sha512-zR6tNC3XF6JuI6PjGlZW2Hz7tTzRzzVaPJfZ01BBWBJVt/2ixJY0SH514uffD03NHYiXZA//hlPQLfw3TkIxQg==
|
||||
|
||||
"@uniswap/sdk-core@^3.0.0-alpha.3", "@uniswap/sdk-core@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-3.0.1.tgz#d08dd68257983af64b9a5f4d6b9cf26124b4138f"
|
||||
|
Loading…
Reference in New Issue
Block a user