chunk multicall by gas cost

add useAllV3Ticks
This commit is contained in:
Noah Zinsmeister 2021-04-02 19:56:59 -04:00
parent edf4c47451
commit a037595e6e
No known key found for this signature in database
GPG Key ID: 83022DD49188C9F2
11 changed files with 168 additions and 50 deletions

@ -49,7 +49,7 @@
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v2-sdk": "^1.0.6",
"@uniswap/v3-core": "^1.0.0-rc.0",
"@uniswap/v3-periphery": "^1.0.0-beta.11",
"@uniswap/v3-periphery": "^1.0.0-beta.12",
"@web3-react/core": "^6.0.9",
"@web3-react/fortmatic-connector": "^6.0.9",
"@web3-react/injected-connector": "^6.0.7",

@ -1,12 +1,13 @@
import { ChainId } from '@uniswap/sdk-core'
import MULTICALL_ABI from './abi.json'
const MULTICALL_NETWORKS: { [chainId in ChainId]: string } = {
const MULTICALL_NETWORKS: { [chainId in ChainId | 1337]: string } = {
[ChainId.MAINNET]: '0xeefBa1e63905eF1D7ACbA5a8513c70307C1cE441',
[ChainId.ROPSTEN]: '0x53C43764255c17BD724F74c4eF150724AC50a3ed',
[ChainId.KOVAN]: '0x2cc8688C5f75E365aaEEb4ea8D6a480405A48D2A',
[ChainId.RINKEBY]: '0x42Ad527de7d4e9d9d011aC45B31D8551f8Fe9821',
[ChainId.GÖRLI]: '0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e',
[1337]: '0xFeabCc62240297F1e4b238937D68e7516f0918D7',
}
export { MULTICALL_ABI, MULTICALL_NETWORKS }

@ -1,12 +1,30 @@
import { ChainId } from '@uniswap/sdk-core'
export const FACTORY_ADDRESSES: { [chainId in ChainId | 1337]: string } = {
[ChainId.MAINNET]: '',
[ChainId.ROPSTEN]: '',
[ChainId.RINKEBY]: '',
[ChainId.GÖRLI]: '',
[ChainId.KOVAN]: '',
[1337]: '0x49A6d0854B0fF95065f0dA247b8a2d440D92D2c7',
}
export const TICK_LENS_ADDRESSES: { [chainId in ChainId | 1337]: string } = {
[ChainId.MAINNET]: '',
[ChainId.ROPSTEN]: '',
[ChainId.RINKEBY]: '',
[ChainId.GÖRLI]: '',
[ChainId.KOVAN]: '',
[1337]: '0xe0507a63E40Ce227CbF2ed7273a01066bAFE667B',
}
export const NONFUNGIBLE_POSITION_MANAGER_ADDRESSES: { [chainId in ChainId | 1337]: string } = {
[ChainId.MAINNET]: '',
[ChainId.ROPSTEN]: '',
[ChainId.RINKEBY]: '',
[ChainId.GÖRLI]: '',
[ChainId.KOVAN]: '',
[1337]: '0xee9e30637f84Bbf929042A9118c6E20023dab833',
[1337]: '0x273Edaa13C845F605b5886Dd66C89AB497A6B17b',
}
export const NONFUNGIBLE_TOKEN_POSITION_DESCRIPTOR_ADDRESSES: { [chainId in ChainId | 1337]: string } = {
@ -15,7 +33,7 @@ export const NONFUNGIBLE_TOKEN_POSITION_DESCRIPTOR_ADDRESSES: { [chainId in Chai
[ChainId.RINKEBY]: '',
[ChainId.GÖRLI]: '',
[ChainId.KOVAN]: '',
[1337]: '0x3431b9Ed12e3204bC6f7039e1c576417B70fdD67',
[1337]: '0x3d137e860008BaF6d1c063158e5ec0baBbcFefF8',
}
export const SWAP_ROUTER_ADDRESSES: { [chainId in ChainId | 1337]: string } = {
@ -24,5 +42,5 @@ export const SWAP_ROUTER_ADDRESSES: { [chainId in ChainId | 1337]: string } = {
[ChainId.RINKEBY]: '',
[ChainId.GÖRLI]: '',
[ChainId.KOVAN]: '',
[1337]: '0xa0588c89Fe967e66533aB1A0504C30989f90156f',
[1337]: '0x80AacDBEe92DC1c2Fbaa261Fb369696AF1AD9f98',
}

@ -0,0 +1,48 @@
import { Result, useSingleContractMultipleData } from 'state/multicall/hooks'
import { useTickLens } from './useContract'
// the following should probably all be from the sdk, just mocking it for now
function MIN_TICK(tickSpacing: number) {
return Math.ceil(-887272 / tickSpacing) * tickSpacing
}
function MAX_TICK(tickSpacing: number) {
return Math.floor(887272 / tickSpacing) * tickSpacing
}
function bitmapIndex(tick: number, tickSpacing: number) {
return (tick / tickSpacing) >> 8
}
// todo this hook needs some tlc around return values
export function useAllV3Ticks(poolAddress: string, tickSpacing: number): Result[] | null {
const tickLens = useTickLens()
const min = MIN_TICK(tickSpacing)
const max = MAX_TICK(tickSpacing)
const minIndex = bitmapIndex(min, tickSpacing)
const maxIndex = bitmapIndex(max, tickSpacing)
const tickLensArgs = new Array(maxIndex - minIndex + 1)
.fill(0)
.map((_, i) => i + minIndex)
.map((wordIndex) => [poolAddress, wordIndex])
const callStates = useSingleContractMultipleData(
tickLens,
'getPopulatedTicksInWord',
tickLensArgs,
undefined,
2_000_000
)
const canReturn = callStates.every(
(callState) => !callState.error && !callState.loading && !callState.syncing && callState.valid && callState.result
)
return canReturn
? callStates
.map(({ result }) => (result as Result).populatedTicks)
.reduce((accumulator, current) => [...accumulator, ...current], [])
: null
}

@ -6,6 +6,9 @@ import { abi as MERKLE_DISTRIBUTOR_ABI } from '@uniswap/merkle-distributor/build
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { abi as V3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json'
import { abi as TickLensABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/TickLens.sol/TickLens.json'
import ARGENT_WALLET_DETECTOR_ABI from 'abis/argent-wallet-detector.json'
import ENS_PUBLIC_RESOLVER_ABI from 'abis/ens-public-resolver.json'
import ENS_ABI from 'abis/ens-registrar.json'
@ -24,8 +27,9 @@ import {
} from 'constants/index'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from 'constants/multicall'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from 'constants/v1'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES } from 'constants/v3'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, FACTORY_ADDRESSES, TICK_LENS_ADDRESSES } from 'constants/v3'
import { useMemo } from 'react'
import { TickLens, UniswapV3Factory } from 'types/v3'
import { NonfungiblePositionManager } from 'types/v3/NonfungiblePositionManager'
import { getContract } from 'utils'
import { useActiveWeb3React } from './index'
@ -141,3 +145,15 @@ export function useV3NFTPositionManagerContract(): NonfungiblePositionManager |
const address = chainId ? NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[chainId] : undefined
return useContract(address, NFTPositionManagerABI) as NonfungiblePositionManager | null
}
export function useV3Factory(): UniswapV3Factory | null {
const { chainId } = useActiveWeb3React()
const address = chainId ? FACTORY_ADDRESSES[chainId] : undefined
return useContract(address, V3FactoryABI) as UniswapV3Factory | null
}
export function useTickLens(): TickLens | null {
const { chainId } = useActiveWeb3React()
const address = chainId ? TICK_LENS_ADDRESSES[chainId] : undefined
return useContract(address, TickLensABI) as TickLens | null
}

@ -3,6 +3,7 @@ import { createAction } from '@reduxjs/toolkit'
export interface Call {
address: string
callData: string
gasRequired?: number
}
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
@ -14,17 +15,25 @@ export function toCallKey(call: Call): string {
if (!LOWER_HEX_REGEX.test(call.callData)) {
throw new Error(`Invalid hex: ${call.callData}`)
}
return `${call.address}-${call.callData}`
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 (pcs.length !== 2) {
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]) } : {}),
}
}

@ -164,7 +164,8 @@ export function useSingleContractMultipleData(
contract: Contract | null | undefined,
methodName: string,
callInputs: OptionalMethodInputs[],
options?: ListenerOptions
options?: ListenerOptions,
gasRequired?: number
): CallState[] {
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
@ -175,10 +176,11 @@ export function useSingleContractMultipleData(
return {
address: contract.address,
callData: contract.interface.encodeFunctionData(fragment, inputs),
...(gasRequired ? { gasRequired } : {}),
}
})
: [],
[callInputs, contract, fragment]
[callInputs, contract, fragment, gasRequired]
)
const results = useCallsData(calls, options)
@ -195,7 +197,8 @@ export function useMultipleContractSingleData(
contractInterface: Interface,
methodName: string,
callInputs?: OptionalMethodInputs,
options?: ListenerOptions
options?: ListenerOptions,
gasRequired?: number
): CallState[] {
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
const callData: string | undefined = useMemo(
@ -214,11 +217,12 @@ export function useMultipleContractSingleData(
? {
address,
callData,
...(gasRequired ? { gasRequired } : {}),
}
: undefined
})
: [],
[addresses, callData, fragment]
[addresses, callData, fragment, gasRequired]
)
const results = useCallsData(calls, options)
@ -234,7 +238,8 @@ export function useSingleCallResult(
contract: Contract | null | undefined,
methodName: string,
inputs?: OptionalMethodInputs,
options?: ListenerOptions
options?: ListenerOptions,
gasRequired?: number
): CallState {
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
@ -244,10 +249,11 @@ export function useSingleCallResult(
{
address: contract.address,
callData: contract.interface.encodeFunctionData(fragment, inputs),
...(gasRequired ? { gasRequired } : {}),
},
]
: []
}, [contract, fragment, inputs])
}, [contract, fragment, inputs, gasRequired])
const result = useCallsData(calls, options)[0]
const latestBlockNumber = useBlockNumber()

@ -16,9 +16,6 @@ import {
updateMulticallResults,
} from './actions'
// chunk calls so we do not exceed the gas limit
const CALL_CHUNK_SIZE = 500
/**
* Fetches a chunk of calls, enforcing a minimum block number constraint
* @param multicallContract multicall contract to fetch against
@ -141,7 +138,7 @@ export default function Updater(): null {
if (outdatedCallKeys.length === 0) return
const calls = outdatedCallKeys.map((key) => parseCallKey(key))
const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE)
const chunkedCalls = chunkArray(calls)
if (cancellations.current?.blockNumber !== latestBlockNumber) {
cancellations.current?.cancellations?.forEach((c) => c())

@ -1,32 +1,17 @@
import chunkArray from './chunkArray'
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 0 throws', () => {
expect(() => chunkArray([1, 2, 3], 0)).toThrow('maxChunkSize must be gte 1')
})
it('size gte items', () => {
expect(chunkArray([1, 2, 3], 3)).toEqual([[1, 2, 3]])
expect(chunkArray([1, 2, 3], 4)).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], 2)).toEqual([
expect(chunkArray([1, 2, 3, 4], DEFAULT_GAS_REQUIRED * 2 + 1)).toEqual([
[1, 2],
[3, 4],
])
})
it('evenly distributes', () => {
const chunked = chunkArray([...Array(100).keys()], 40)
expect(chunked).toEqual([
[...Array(34).keys()],
[...Array(34).keys()].map((i) => i + 34),
[...Array(32).keys()].map((i) => i + 68),
])
expect(chunked[0][0]).toEqual(0)
expect(chunked[2][31]).toEqual(99)
})
})

@ -1,11 +1,32 @@
// chunks array into chunks of maximum size
const CONSERVATIVE_BLOCK_GAS_LIMIT = 10_000_000 // conservative, hard-coded estimate of the current block gas limit
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[], maxChunkSize: number): T[][] {
if (maxChunkSize < 1) throw new Error('maxChunkSize must be gte 1')
if (items.length <= maxChunkSize) return [items]
export default function chunkArray<T>(items: T[], gasLimit = CONSERVATIVE_BLOCK_GAS_LIMIT * 10): T[][] {
const chunks: T[][] = []
let currentChunk: T[] = []
let currentChunkCumulativeGas = 0
const numChunks: number = Math.ceil(items.length / maxChunkSize)
const chunkSize = Math.ceil(items.length / numChunks)
for (let i = 0; i < items.length; i++) {
const item = items[i]
return [...Array(numChunks).keys()].map((ix) => items.slice(ix * chunkSize, ix * chunkSize + chunkSize))
// 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 < gasLimit) {
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
}

@ -4085,6 +4085,11 @@
resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-1.1.1.tgz#0afd29601846c16e5d082866cbb24a9e0758e6bc"
integrity sha512-2yK7sLpKIT91TiS5sewHtOa7YuM8IuBXVl4GZv2jZFys4D2sY7K5vZh6MqD25TPA95Od+0YzCVq6cTF2IKrOmg==
"@uniswap/lib@^4.0.1-alpha":
version "4.0.1-alpha"
resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02"
integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA==
"@uniswap/liquidity-staker@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@uniswap/liquidity-staker/-/liquidity-staker-1.0.2.tgz#afed49d51c5f97411432e891a59a24817728dad6"
@ -4120,6 +4125,11 @@
resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.0.tgz#e0fab91a7d53e8cafb5326ae4ca18351116b0844"
integrity sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA==
"@uniswap/v2-core@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425"
integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==
"@uniswap/v2-periphery@^1.1.0-beta.0":
version "1.1.0-beta.0"
resolved "https://registry.yarnpkg.com/@uniswap/v2-periphery/-/v2-periphery-1.1.0-beta.0.tgz#20a4ccfca22f1a45402303aedb5717b6918ebe6d"
@ -4139,18 +4149,25 @@
tiny-invariant "^1.1.0"
tiny-warning "^1.0.3"
"@uniswap/v3-core@1.0.0-rc.1":
version "1.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0-rc.1.tgz#f2bbc483451364a951fba06eb2d978c6e8bdd58f"
integrity sha512-4ET2H0a8p7nVBGFWfio9SHc+RA6UIXEvlTRIJNsDwjQvfs8Jq9lfJ+eSOUTGmiB8Vp8V5dWarLDBU/rDE159pQ==
"@uniswap/v3-core@^1.0.0-rc.0":
version "1.0.0-rc.0"
resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0-rc.0.tgz#eee325b11ab423b5e77af1ae6e9aec89fa51d7c5"
integrity sha512-nMmAtXtU1B3pOsP1DMMBO4/CUwPlatVhPKoos1l8KvVKCO9wP+trMR/heYmcwILJVF4Em5EqUBvx6Jzd8oagAg==
"@uniswap/v3-periphery@^1.0.0-beta.11":
version "1.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.0.0-beta.11.tgz#5068eed1cafa591ebb69ee4e990b4ca38de602be"
integrity sha512-/jUNxj+/oH9TDbXv4acJc4G4xLydYJnsFc5W+fYvPTMR89PT3RLXWYK8o8FaEr/2uDxnycjXfovHb1tgB/kgtg==
"@uniswap/v3-periphery@^1.0.0-beta.12":
version "1.0.0-beta.12"
resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.0.0-beta.12.tgz#f8c8010ec92582950127a068414e7d6682b247bc"
integrity sha512-c6qUcr5V+EQ/2SqlyGax1N0drRkULfOwrnJcM+98MBzVbJe2q4PfRk9dnsfijqoN9hb4GMqnb/8Ahhs0u48fEA==
dependencies:
"@openzeppelin/contracts" "3.4.1-solc-0.7-2"
"@uniswap/v3-core" "^1.0.0-rc.0"
"@uniswap/lib" "^4.0.1-alpha"
"@uniswap/v2-core" "1.0.1"
"@uniswap/v3-core" "1.0.0-rc.1"
"@walletconnect/client@^1.1.1-alpha.0":
version "1.3.6"