-
Notifications
You must be signed in to change notification settings - Fork 82
Open
Description
Hello, first of all thanks for sharing.
I have updated the code for Solidity 0.8:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
// ERC20 contract interface
interface Token {
function balanceOf(address account) external view returns (uint256);
}
contract BalanceChecker {
/* Fallback function, don't accept any ETH */
receive() external payable {
revert("BalanceChecker does not accept payments");
}
/*
Check the token balance of a wallet in a token contract
Returns the balance of the token for user. Avoids possible errors:
- return 0 on non-contract address
- returns 0 if the contract doesn't implement balanceOf
*/
function tokenBalance(address user, address token) public view returns (uint256) {
// check if token is actually a contract
uint256 tokenCode;
assembly { tokenCode := extcodesize(token) } // contract code size
// is it a contract and does it implement balanceOf
if (tokenCode > 0) {
(bool success, bytes memory data) = token.staticcall(abi.encodeWithSignature("balanceOf(address)", user));
if (success) {
return abi.decode(data, (uint256));
}
}
return 0;
}
/*
Check the token balances of a wallet for multiple tokens.
Pass address(0) as a "token" address to get ETH balance.
Possible error throws:
- extremely large arrays for user and/or tokens (gas cost too high)
Returns a one-dimensional array that's user.length * tokens.length long. The
array is ordered by all of the 0th users token balances, then the 1st
user, and so on.
*/
function balances(address[] calldata users, address[] calldata tokens) external view returns (uint256[] memory) {
uint256[] memory addrBalances = new uint256[](tokens.length * users.length);
for (uint256 i = 0; i < users.length; i++) {
for (uint256 j = 0; j < tokens.length; j++) {
uint256 addrIdx = j + tokens.length * i;
if (tokens[j] != address(0)) {
addrBalances[addrIdx] = tokenBalance(users[i], tokens[j]);
} else {
addrBalances[addrIdx] = users[i].balance; // ETH balance
}
}
}
return addrBalances;
}
}
Here is also a javascript/typescript function that can be used to optimize the amount of call to the contract so the page never execed 100
import { BalanceChecker } from 'balance-checker' // this is typechain generated by hardhat with the previous contract
type Address = string
type Token = string
interface Result {
address: Address
token: Token
balance: bigint
}
export async function balanceCheckerProcessInBatches(
addresses: Address[],
tokens: Token[],
balanceChecker: BalanceChecker,
): Promise<Result[]> {
const results: Result[] = []
const maxBatchSize = 100
for (let i = 0; i < addresses.length; i++) {
for (let j = 0; j < tokens.length; j += maxBatchSize) {
// Create subsets of addresses and tokens while respecting the limit of 100
const batchTokens = tokens.slice(
j,
Math.min(j + maxBatchSize, tokens.length),
)
const currentBatchSize = batchTokens.length
const remainingCapacity = maxBatchSize - currentBatchSize
let batchAddresses: Address[] = [addresses[i]]
if (remainingCapacity > 0 && i + 1 < addresses.length) {
const extraAddresses = Math.min(
Math.floor(remainingCapacity / tokens.length),
addresses.length - i - 1,
)
batchAddresses = batchAddresses.concat(
addresses.slice(i + 1, i + 1 + extraAddresses),
)
i += extraAddresses // Move forward the address index
}
const balances = await balanceChecker.balances(
batchAddresses,
batchTokens,
)
if (balances.length !== batchAddresses.length * batchTokens.length) {
throw new Error(
`Batch mismatch: Expected ${batchAddresses.length * batchTokens.length} balances, but got ${balances.length}`,
)
}
for (let a = 0; a < batchAddresses.length; a++) {
for (let t = 0; t < batchTokens.length; t++) {
results.push({
address: batchAddresses[a],
token: batchTokens[t],
balance: balances[a * batchTokens.length + t],
})
}
}
}
}
return results
}
/**
* Test code below
*/
// const addresses = Array.from({ length: 50 }, (_, i) => `0xAddress${i + 1}`)
// const tokens = Array.from({ length: 2 }, (_, i) => `Token${i + 1}`)
//
// const balanceChecker = {
// balances: async (
// addresses: Address[],
// tokens: Token[],
// ): Promise<bigint[]> => {
// console.log(
// `Page size: ${addresses.length * tokens.length} (address ${addresses.length}) (tokens ${tokens.length})`,
// )
//
// const arrayOfBigInt = [];
// for (let i = 0; i < addresses.length; i++) {
// for (let j = 0; j < tokens.length; j++) {
// arrayOfBigInt.push(BigInt(addresses[i].length + tokens[j].length));
// }
// }
//
// console.log(`Confirmed page size: ${arrayOfBigInt.length}`);
// return arrayOfBigInt;
// },
// }
//
// ;(async () => {
// const results = await balanceCheckerProcessInBatches(addresses, tokens, balanceChecker as BalanceChecker)
//
// results.forEach((result) => {
// console.log(`Address: ${result.address}, Token: ${result.token}, Balance: ${result.balance}`);
// })
//
// console.log(`Total number of results: ${results.length}`)
// })()The commented code is for testing yourself, in case you don't trust.
This is awesome however, I am struggling trying to pass 0x0 to get ETH balance, with your deployed version on mainnet, or with my hardhat fork.
I keep having such kind of errors: : RangeError: cannot slice beyond data bounds (buffer=0x, length=0, offset=4, code=BUFFER_OVERRUN, version=6.13.1)
When I deploy my own BalanceChecker.sol, I don´t get much more information:
eth_call
Contract call: <UnrecognizedContract>
From: 0xbaa4dbc880eaf033fd6a01128116a060ca2ac661
To: 0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e
Error: Transaction reverted without a reasonAny clue how to get ETH balance ?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels