• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hicommonwealth / commonwealth / 13497108514

24 Feb 2025 11:36AM UTC coverage: 46.449% (+0.08%) from 46.365%
13497108514

Pull #11078

github

web-flow
Merge 56f84aad3 into beadf67b7
Pull Request #11078: Improvements to Community Homepages

1317 of 3113 branches covered (42.31%)

Branch coverage included in aggregate %.

2490 of 5083 relevant lines covered (48.99%)

38.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

1.23
/libs/model/src/services/tokenBalanceCache/util.ts
1
import { HttpBatchClient, Tendermint34Client } from '@cosmjs/tendermint-rpc';
2
import { logger } from '@hicommonwealth/core';
3
import {
4
  decodeParameters,
5
  encodeParameters,
6
} from '@hicommonwealth/evm-protocols';
7
import { ZERO_ADDRESS } from '@hicommonwealth/shared';
8
import { ChainNodeAttributes } from '../../models/chain_node';
9
import { Balances, GetTendermintClientOptions } from './types';
10

11
const log = logger(import.meta);
29✔
12

13
/**
14
 * This function batches hundreds of RPC requests (1 per address) into a few batched RPC requests.
15
 * This function cannot be used for on-chain batching since several hundred addresses are batched
16
 * for a single RPC request (not 1 RPC request per address).
17
 */
18
export async function evmOffChainRpcBatching(
19
  source: {
20
    evmChainId: number;
21
    url: string;
22
    contractAddress?: string;
23
  },
24
  rpc: {
25
    method: 'eth_call' | 'eth_getBalance';
26
    getParams: (
27
      address: string,
28
      contractAddress?: string,
29
    ) => string | Record<string, string>;
30
    batchSize?: number;
31
  },
32
  addresses: string[],
33
): Promise<{ balances: Balances; failedAddresses: string[] }> {
34
  if (!rpc.batchSize) rpc.batchSize = 500;
×
35

36
  const batchRequestPromises = [];
×
37
  // maps an RPC request id to an address
38
  const idAddressMap: Balances = {};
×
39

40
  // iterate through addresses in batches of size rpcBatchSize creating a single request for each batch
41
  let id = 1;
×
42
  for (
×
43
    let startIndex = 0;
×
44
    startIndex < addresses.length;
45
    startIndex += rpc.batchSize
46
  ) {
47
    const endIndex = Math.min(startIndex + rpc.batchSize, addresses.length);
×
48
    const batchAddresses = addresses.slice(startIndex, endIndex);
×
49
    const rpcRequests = [];
×
50
    for (const address of batchAddresses) {
×
51
      rpcRequests.push({
×
52
        method: rpc.method,
53
        params: [rpc.getParams(address, source.contractAddress), 'latest'],
54
        id,
55
        jsonrpc: '2.0',
56
      });
57
      idAddressMap[id] = address;
×
58
      ++id;
×
59
    }
60

61
    batchRequestPromises.push(
×
62
      fetch(source.url, {
63
        method: 'POST',
64
        body: JSON.stringify(rpcRequests),
65
        headers: { 'Content-Type': 'application/json' },
66
      }),
67
    );
68
  }
69

70
  let failedAddresses: string[] = [];
×
71
  const jsonPromises: Promise<any>[] = [];
×
72
  const responses = await Promise.allSettled(batchRequestPromises);
×
73
  const chainNodeErrorMsg =
74
    `${failingChainNodeError} RPC batch request failed for method '${rpc.method}' ` +
×
75
    `with batch size ${rpc.batchSize} on evm chain id ${source.evmChainId}${
76
      source.contractAddress ? ` for token ${source.contractAddress}` : ''
×
77
    }.`;
78
  responses.forEach((res, index) => {
×
79
    // handle a failed batch request
80
    if (res.status === 'rejected') {
×
81
      const startIndex = rpc.batchSize! * index;
×
82
      const relevantAddresses = addresses.slice(
×
83
        startIndex,
84
        Math.min(startIndex + rpc.batchSize!, addresses.length),
85
      );
86
      failedAddresses = [...failedAddresses, ...relevantAddresses];
×
87
      log.fatal(chainNodeErrorMsg, res.reason);
×
88
    } else {
89
      jsonPromises.push(res.value.json());
×
90
    }
91
  });
92

93
  let datas;
94
  try {
×
95
    datas = (await Promise.all(jsonPromises)).flat();
×
96
  } catch (e) {
97
    log.fatal(chainNodeErrorMsg, e instanceof Error ? e : undefined);
×
98
    return {
×
99
      balances: {},
100
      failedAddresses: addresses,
101
    };
102
  }
103

104
  const balances: Balances = {};
×
105
  for (const data of datas) {
×
106
    if (data.error) {
×
107
      failedAddresses.push(idAddressMap[data.id]);
×
108
      const msg = `RPC request failed on EVM chain id ${source.evmChainId}${
×
109
        source.contractAddress ? `for token ${source.contractAddress}` : ''
×
110
      }.`;
111
      log.error(msg, data.error);
×
112
      continue;
×
113
    }
114

115
    const address = idAddressMap[data.id];
×
116
    if (source.contractAddress) {
×
117
      const { 0: balance } = decodeParameters({
×
118
        abiInput: ['uint256'],
119
        data: data.result,
120
      });
121
      balances[address] = String(balance);
×
122
    } else {
123
      balances[address] = BigInt(data.result).toString();
×
124
    }
125
  }
126

127
  return { balances, failedAddresses };
×
128
}
129

130
/**
131
 * This function uses the on-chain Balance Fetcher contract to batch fetch balances
132
 * for many different addresses using a single RPC request. This is extremely scalable
133
 * because we can batch multiple requests together e.g. each on-chain call can batch 1k
134
 * paired with 100 batched RPC requests that means we can fetch 100k address balances
135
 * from a single HTTP request. ONLY WORKS FOR ERC20 and ETH!
136
 */
137
export async function evmBalanceFetcherBatching(
138
  source: {
139
    evmChainId: number;
140
    url: string;
141
    contractAddress?: string;
142
  },
143
  rpc: {
144
    batchSize?: number;
145
  },
146
  addresses: string[],
147
): Promise<{ balances: Balances; failedAddresses: string[] }> {
148
  if (!rpc.batchSize) rpc.batchSize = 500;
×
149
  // 0x0 tells the on-chain contract to only return ETH balances
150
  if (!source.contractAddress) source.contractAddress = ZERO_ADDRESS;
×
151

152
  const rpcRequests = [];
×
153

154
  for (
×
155
    let startIndex = 0;
×
156
    startIndex < addresses.length;
157
    startIndex += rpc.batchSize
158
  ) {
159
    const endIndex = Math.min(startIndex + rpc.batchSize, addresses.length);
×
160
    const batchAddresses = addresses.slice(startIndex, endIndex);
×
161

162
    const calldata =
163
      '0xf0002ea9' +
×
164
      encodeParameters({
165
        abiInput: ['address[]', 'address[]'],
166
        data: [batchAddresses, [source.contractAddress]],
167
      }).substring(2);
168

169
    rpcRequests.push({
×
170
      method: 'eth_call',
171
      params: [
172
        {
173
          to: mapNodeToBalanceFetcherContract(source.evmChainId),
174
          data: calldata,
175
        },
176
        'latest',
177
      ],
178
      id: startIndex,
179
      jsonrpc: '2.0',
180
    });
181
  }
182

183
  const errorMsg =
184
    `On-chain batch request failed ` +
×
185
    `with batch size ${rpc.batchSize} on evm chain id ${source.evmChainId}${
186
      source.contractAddress ? `for token ${source.contractAddress}` : ''
×
187
    }.`;
188

189
  const datas = await evmRpcRequest(source.url, rpcRequests, errorMsg);
×
190
  if (!datas)
×
191
    return {
×
192
      balances: {},
193
      failedAddresses: addresses,
194
    };
195

196
  const addressBalanceMap: Balances = {};
×
197
  let failedAddresses: string[] = [];
×
198

199
  if (datas.error) {
×
200
    log.error(errorMsg, datas.error);
×
201
    return { balances: {}, failedAddresses: addresses };
×
202
  } else {
203
    for (const data of datas) {
×
204
      // this replicates the batches used when creating the requests
205
      // note -> data.id is the startIndex defined in the loop above
206
      const endIndex = Math.min(data.id + rpc.batchSize, addresses.length);
×
207
      const relevantAddresses = addresses.slice(data.id, endIndex);
×
208

209
      if (data.error) {
×
210
        failedAddresses = [...failedAddresses, ...relevantAddresses];
×
211
        const msg =
212
          'Balance Fetcher Contract request failed on EVM ' +
×
213
          `chain id: ${source.evmChainId}${
214
            source.contractAddress ? `for token ${source.contractAddress}` : ''
×
215
          }.`;
216
        log.error(msg, data.error);
×
217
        continue;
×
218
      }
219

220
      const { 0: balances } = decodeParameters({
×
221
        abiInput: ['uint256[]'],
222
        data: data.result,
223
      });
224
      console.log('>>>>>>>>>>>>>>>>>>', balances);
×
225
      relevantAddresses.forEach(
×
226
        (key, i) => (addressBalanceMap[key] = String((<number[]>balances)[i])),
×
227
      );
228
    }
229
  }
230

231
  return { balances: addressBalanceMap, failedAddresses };
×
232
}
233

234
/**
235
 * Maps an EVM chain id to the contract address of that chains Balance Checker contract.
236
 * All supported contract addresses can be found here: https://github.com/wbobeirne/eth-balance-checker
237
 * Some contract addresses are available in the open PRs or issues.
238
 */
239
export function mapNodeToBalanceFetcherContract(
240
  ethChainId: ChainNodeAttributes['eth_chain_id'],
241
) {
242
  switch (ethChainId) {
×
243
    case 1: // Ethereum Mainnet
244
    case 1337: // Local Ganache - assuming fork of mainnet
245
      return '0xb1f8e55c7f64d203c1400b9d8555d050f94adf39';
×
246
    case 5: // Goerli
247
      return '0x9788C4E93f9002a7ad8e72633b11E8d1ecd51f9b';
×
248
    case 56: // BSC
249
    case 97: // BSC Testnet
250
      return '0x2352c63A83f9Fd126af8676146721Fa00924d7e4';
×
251
    case 137: // Polygon
252
    case 80001: // Polygon Mumbai
253
      return '0x2352c63A83f9Fd126af8676146721Fa00924d7e4';
×
254
    case 10: // Optimism Mainnet
255
      return '0xB1c568e9C3E6bdaf755A60c7418C269eb11524FC';
×
256
    case 42161: // Arbitrum One
257
      return '0x151E24A486D7258dd7C33Fb67E4bB01919B7B32c';
×
258
    case 43114: // Avalanche
259
      return '0xD023D153a0DFa485130ECFdE2FAA7e612EF94818';
×
260
    case 43113: // Avalanche Testnet
261
      return '0x100665685d533F65bdD0BD1d65ca6387FC4F4FDB';
×
262
    case 250: // Fantom
263
      return '0x07f697424ABe762bB808c109860c04eA488ff92B';
×
264
    case 4002: // Fantom Testnet
265
      return '0x8B14C79f24986B127EC7208cE4e93E8e45125F8f';
×
266
    case 1284: // Moonbeam -- unverified
267
      return '0xf614056a46e293DD701B9eCeBa5df56B354b75f9';
×
268
    case 1285: // Moonriver -- unverified
269
      return '0xDEAa846cca7FEc9e76C8e4D56A55A75bb0973888';
×
270
    case 1313161554: // Aurora Mainnet -- unverified
271
      return '0x100665685d533F65bdD0BD1d65ca6387FC4F4FDB';
×
272
    case 1313161555: // Aurora Testnet -- unverified
273
      return '0x60d2714e1a9Fd5e9580A66f6aF6b259C77A87b09';
×
274
    case 25: // Cronos Mainnet -- unverified
275
      return '0x8b14c79f24986b127ec7208ce4e93e8e45125f8f';
×
276
    case 338: // Cronos Testnet -- unverified
277
      return '0x8b14c79f24986b127ec7208ce4e93e8e45125f8f';
×
278
    case 66: // OKXChain Mainnet:
279
      return '0x42CD9068d471c861796D56A37f8BFEae19DAC12F';
×
280
    case 9001: // Evmos -- unverified
281
      return '0x42CD9068d471c861796D56A37f8BFEae19DAC12F';
×
282
    case 59144: // Linea Mainnet -- unverified
283
      return '0xF62e6a41561b3650a69Bb03199C735e3E3328c0D';
×
284
    case 59140: // Linea Testnet -- unverified
285
      return '0x10dAd7Ca3921471f616db788D9300DC97Db01783';
×
286
    case 11155111: // Sepolia
287
      return '0xBfbCed302deD369855fc5f7668356e123ca4B329';
×
288
  }
289
}
290

291
export const failingChainNodeError = 'FAILING OR RATE LIMITED CHAIN NODE:';
29✔
292

293
export async function evmRpcRequest(
294
  rpcEndpoint: string,
295
  rawRequestBody: Record<string, unknown> | Array<Record<string, unknown>>,
296
  errorMsg: string,
297
) {
298
  let data;
299
  try {
×
300
    const response = await fetch(rpcEndpoint, {
×
301
      method: 'POST',
302
      body: JSON.stringify(rawRequestBody),
303
      headers: { 'Content-Type': 'application/json' },
304
    });
305
    data = await response.json();
×
306
  } catch (e) {
307
    const augmentedMsg = `${failingChainNodeError} ${errorMsg}`;
×
308
    log.fatal(augmentedMsg, e instanceof Error ? e : undefined);
×
309
  }
310

311
  return data;
×
312
}
313

314
export async function getTendermintClient(
315
  options: GetTendermintClientOptions,
316
): Promise<Tendermint34Client> {
317
  const batchClient = new HttpBatchClient(
×
318
    options.chainNode?.private_url || options.chainNode.url,
×
319
    {
320
      batchSizeLimit: options.batchSize || 100,
×
321
      dispatchInterval: 10,
322
    },
323
  );
324
  return await Tendermint34Client.create(batchClient);
×
325
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc