-
Notifications
You must be signed in to change notification settings - Fork 50
feat(utils): batch balance queries via multicall #1557
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
base: master
Are you sure you want to change the base?
Conversation
Signed-off-by: Gerhard Steenkamp <[email protected]>
Signed-off-by: Gerhard Steenkamp <[email protected]>
Signed-off-by: Gerhard Steenkamp <[email protected]>
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
export const getBatchBalanceViaMulticall3 = async ( | ||
chainId: string | number, | ||
addresses: string[], | ||
tokenAddresses: string[], | ||
blockTag: providers.BlockTag = "latest" | ||
): Promise<MultiCallResult> => { | ||
const chainIdAsInt = Number(chainId); | ||
const provider = getProvider(chainIdAsInt); | ||
|
||
const multicall3 = getMulticall3(chainIdAsInt, provider); | ||
|
||
if (!multicall3) { | ||
throw new Error("No Multicall3 deployed on this chain"); | ||
} | ||
|
||
let calls: Parameters<typeof callViaMulticall3>[1] = []; | ||
|
||
for (const tokenAddress of tokenAddresses) { | ||
if (tokenAddress === ZERO_ADDRESS) { | ||
// For native currency | ||
calls.push( | ||
...addresses.map((address) => ({ | ||
contract: multicall3, | ||
functionName: "getEthBalance", | ||
args: [address], | ||
})) | ||
); | ||
} else { | ||
// For ERC20 tokens | ||
const erc20Contract = ERC20__factory.connect(tokenAddress, provider); | ||
calls.push( | ||
...addresses.map((address) => ({ | ||
contract: erc20Contract, | ||
functionName: "balanceOf", | ||
args: [address], | ||
})) | ||
); | ||
} | ||
} | ||
|
||
const inputs = calls.map(({ contract, functionName, args }) => ({ | ||
target: contract.address, | ||
callData: contract.interface.encodeFunctionData(functionName, args), | ||
})); | ||
|
||
const [blockNumber, results] = await multicall3.callStatic.aggregate(inputs, { | ||
blockTag, | ||
}); | ||
|
||
const decodedResults = results.map((result, i) => | ||
calls[i].contract.interface.decodeFunctionResult( | ||
calls[i].functionName, | ||
result | ||
) | ||
); | ||
|
||
let balances: Record<string, Record<string, string>> = {}; | ||
|
||
let resultIndex = 0; | ||
for (const tokenAddress of tokenAddresses) { | ||
addresses.forEach((address) => { | ||
if (!balances[address]) { | ||
balances[address] = {}; | ||
} | ||
balances[address][tokenAddress] = decodedResults[resultIndex].toString(); | ||
resultIndex++; | ||
}); | ||
} | ||
|
||
return { | ||
blockNumber: blockNumber.toNumber(), | ||
balances, | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copied from here (since we cannot share code between api and frontend easily)
Line 1393 in 9c29867
export const getBatchBalanceViaMulticall3 = async ( |
* @param blockTag Optional blockTag (defaults to "latest"). | ||
* @returns An object containing the balance (a string), loading state, and any error. | ||
*/ | ||
export function useBalance({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
might need to add ability to resolve tokenSymbol => address
.
let calls: Parameters<typeof callViaMulticall3>[1] = []; | ||
|
||
for (const tokenAddress of tokenAddresses) { | ||
if (tokenAddress === ZERO_ADDRESS) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to add logic somewhere to handle ETH since we use WETH address internally not the zero address
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wdyt of using the symbols instead of addresses in the context of our FE? Based on the chain id we can infer which symbols are native
timer?: NodeJS.Timeout; | ||
} | ||
|
||
const DEFAULT_BATCH_INTERVAL = 100; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this interval adds an implicit delay. is this too aggressive? is 10ms better?
Signed-off-by: Gerhard Steenkamp <[email protected]>
*/ | ||
export const getBatchBalanceViaMulticall3 = async ( | ||
chainId: string | number, | ||
addresses: string[], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gsteenkamp89 Do you think we'll be supplying > 1 address in practice? I'd thought it would typically only be address of the connected wallet, though if we need to make more queries than that's even better.
(i.e. are we maybe also querying relayer balances this way?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah that's exactly right, we're repurposing the logic originally written for relayer balances.
// For ERC20 tokens | ||
const erc20Contract = ERC20__factory.connect(tokenAddress, provider); | ||
calls.push( | ||
...addresses.map((address) => ({ | ||
contract: erc20Contract, | ||
functionName: "balanceOf", | ||
args: [address], | ||
})) | ||
); | ||
} | ||
} | ||
|
||
const inputs = calls.map(({ contract, functionName, args }) => ({ | ||
target: contract.address, | ||
callData: contract.interface.encodeFunctionData(functionName, args), | ||
})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gsteenkamp89 I think this could be reordered to squash it down. It should be possible to instantiate a single generic ERC20 contract (not connected to any specific address or provider) and then generate all the balanceOf() calldata. Then that same calldata can be reused across all of the ERC20s we want to query.
So I think it could be something like:
const erc20 = ERC20__factory.connect(ZERO_ADDRESS, ...);
const inputs: { target: string, callData: string }[] = [];
for (const address of addresses) {
const callData = erc20.interface.encodeFunctionData("balanceOf", address);
for (const tokenAddress of tokenAddresses) {
inputs.push({ target: tokenAddress, callData });
}
}
If it's done that way then that should save instantiating a bunch of ephemeral ERC20 contract instances. It does require some special handling for the snowflake "getEthBalance()" call though.
export const getBatchBalanceViaMulticall3 = async ( | ||
chainId: string | number, | ||
addresses: string[], | ||
tokenAddresses: string[], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might make sense to use symbols instead of addresses here?
let calls: Parameters<typeof callViaMulticall3>[1] = []; | ||
|
||
for (const tokenAddress of tokenAddresses) { | ||
if (tokenAddress === ZERO_ADDRESS) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wdyt of using the symbols instead of addresses in the context of our FE? Based on the chain id we can infer which symbols are native
blockTag ?? "latest", | ||
] as const, | ||
queryFn: () => | ||
balanceBatcher.queueBalanceCall( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks interesting. I am just wondering if we could reach the same goal by using optimized tanstack/query options 🤔
motivation
Each new added token/chain increases the number of balance queries exponentially.
This PR adds a class we can use to batch any balance queries (made within a specified interval) via multicall, reducing the number of requests to public RPCs