@defillama/sdk 5.0.242 → 5.0.244

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@ const providers_json_1 = __importDefault(require("../providers.json"));
14
14
  const env_1 = require("./env");
15
15
  const debugLog_1 = require("./debugLog");
16
16
  const axios_1 = __importDefault(require("axios"));
17
+ const callStats_1 = require("./callStats");
17
18
  let buildProviders = undefined;
18
19
  if (process.env['TEST_MODE']) {
19
20
  buildProviders = require('../../build/providers.json');
@@ -137,6 +138,7 @@ class LlamaProvider extends ethers_1.FallbackProvider {
137
138
  // }
138
139
  for (const runner of runners) {
139
140
  try {
141
+ (0, callStats_1.recordMethod)(method, this.chainName); // per attempt: call stats count requests, not successes
140
142
  const result = await httpRPC[method](runner.url, params);
141
143
  return result;
142
144
  }
@@ -179,6 +181,7 @@ function createProvider(name, rpcString, chainId = 400069, archivalRPCList = [])
179
181
  // return getProviderObject(rpcList[0], name)
180
182
  rpcList = [rpcList[0]];
181
183
  }
184
+ (0, callStats_1.registerEndpoints)(name, [...rpcList, ...archivalRPCList]); // so http level call stats are attributed to this chain
182
185
  try {
183
186
  const fallbackConfig = { cacheTimeout: 5 * 1000, quorum: 1, eventQuorum: 1, };
184
187
  const getProviderWithURL = (url) => ({ url, provider: getProviderObject(url, name) });
@@ -237,39 +240,42 @@ function toHex(n) {
237
240
  return n;
238
241
  return '0x' + n.toString(16);
239
242
  }
243
+ // A public RPC behind a dead or re-routed domain can answer HTTP 200 with an HTML page, an empty body or a JSON object without `result`
244
+ // (seen on mainnet.telos.net/evm). Reading `data.result` off that gave `undefined`, which the caller treated as a successful
245
+ // call and handed to the ABI decoder, so the error surfaced as "invalid BytesLike value null" / a multicall failure instead of the
246
+ // provider moving on to the next RPC. Anything that is not a JSON-RPC envelope with a `result` is therefore thrown here.
247
+ async function rpcPost(rpc, method, params, timeoutEnvKey, defaultTimeout) {
248
+ const { data } = await axios_1.default.post(rpc, { jsonrpc: '2.0', id: 1, params, method }, {
249
+ timeout: +(0, env_1.getEnvValue)(timeoutEnvKey, defaultTimeout)
250
+ });
251
+ if (!data || typeof data !== 'object' || Array.isArray(data))
252
+ throw new Error(`${method} on ${rpc}: not a JSON-RPC response: ${String(data).slice(0, 120)}`);
253
+ if (data.error)
254
+ throw data.error;
255
+ if (!Object.prototype.hasOwnProperty.call(data, 'result'))
256
+ throw new Error(`${method} on ${rpc}: response has no result field: ${JSON.stringify(data).slice(0, 120)}`);
257
+ return data.result;
258
+ }
240
259
  const httpRPC = {
241
260
  getBlockNumber: async (rpc) => {
242
261
  const getData = async () => {
243
- const data = (await axios_1.default.post(rpc, {
244
- jsonrpc: '2.0', id: 1, params: [],
245
- method: 'eth_blockNumber',
246
- }, {
247
- timeout: +(0, env_1.getEnvValue)('LLAMA_PROVIDER_RPC_GET_BLOCKNUMBER_TIMEOUT', '30000')
248
- })).data;
249
- if (data.error)
250
- throw data.error;
251
- return data.result;
262
+ const result = await rpcPost(rpc, 'eth_blockNumber', [], 'LLAMA_PROVIDER_RPC_GET_BLOCKNUMBER_TIMEOUT', '30000');
263
+ const block = parseInt(result);
264
+ if (isNaN(block))
265
+ throw new Error(`eth_blockNumber on ${rpc}: bad result ${JSON.stringify(result)}`);
266
+ return block;
252
267
  };
253
- let data;
254
268
  try {
255
- data = await getData();
269
+ return await getData();
256
270
  }
257
271
  catch (e) {
258
272
  // try again
259
- data = await getData();
273
+ return await getData();
260
274
  }
261
- return parseInt(data);
262
275
  },
263
276
  getBlock: async (rpc, params) => {
264
277
  params[0] = toHex(params[0]);
265
- const { data: { result, error } } = await axios_1.default.post(rpc, {
266
- jsonrpc: '2.0', id: 1, params,
267
- method: 'eth_getBlockByNumber',
268
- }, {
269
- timeout: +(0, env_1.getEnvValue)('LLAMA_PROVIDER_RPC_GET_BLOCK_TIMEOUT', '60000')
270
- });
271
- if (error)
272
- throw error;
278
+ const result = await rpcPost(rpc, 'eth_getBlockByNumber', params, 'LLAMA_PROVIDER_RPC_GET_BLOCK_TIMEOUT', '60000');
273
279
  if (!result)
274
280
  throw new Error(`block ${params[0]} not found`); // pruned or not yet synced, let callers recognise it
275
281
  result.number = parseInt(result.number);
@@ -278,39 +284,23 @@ const httpRPC = {
278
284
  },
279
285
  getBalance: async (rpc, params) => {
280
286
  params[1] = toHex(params[1]);
281
- const { data: { result, error } } = await axios_1.default.post(rpc, {
282
- jsonrpc: '2.0', id: 1, params,
283
- method: 'eth_getBalance',
284
- }, {
285
- timeout: +(0, env_1.getEnvValue)('LLAMA_PROVIDER_RPC_GET_BALANCE_TIMEOUT', '30000')
286
- });
287
- if (error)
288
- throw error;
287
+ const result = await rpcPost(rpc, 'eth_getBalance', params, 'LLAMA_PROVIDER_RPC_GET_BALANCE_TIMEOUT', '30000');
289
288
  return BigInt(result).toString();
290
289
  },
291
290
  call: async (rpc, params) => {
292
291
  params[1] = toHex(params[1]);
293
- const { data } = await axios_1.default.post(rpc, {
294
- jsonrpc: '2.0', id: 1, params,
295
- method: 'eth_call',
296
- }, {
297
- timeout: +(0, env_1.getEnvValue)('LLAMA_PROVIDER_RPC_CALL_TIMEOUT', '30000')
298
- });
299
- if (data.error)
300
- throw data.error;
301
- return data.result;
292
+ const result = await rpcPost(rpc, 'eth_call', params, 'LLAMA_PROVIDER_RPC_CALL_TIMEOUT', '30000');
293
+ // eth_call always returns hex data (`0x` when the target has no code); a null here is a broken node, not an empty return
294
+ if (typeof result !== 'string')
295
+ throw new Error(`eth_call on ${rpc}: bad result ${JSON.stringify(result)}`);
296
+ return result;
302
297
  },
303
298
  getLogs: async (rpc, params) => {
304
299
  params[0].fromBlock = toHex(params[0].fromBlock);
305
300
  params[0].toBlock = toHex(params[0].toBlock);
306
- const { data: { result, error } } = await axios_1.default.post(rpc, {
307
- jsonrpc: '2.0', id: 1, params,
308
- method: 'eth_getLogs',
309
- }, {
310
- timeout: +(0, env_1.getEnvValue)('LLAMA_PROVIDER_RPC_GET_LOGS_TIMEOUT', '30000')
311
- });
312
- if (error)
313
- throw error;
301
+ const result = await rpcPost(rpc, 'eth_getLogs', params, 'LLAMA_PROVIDER_RPC_GET_LOGS_TIMEOUT', '30000');
302
+ if (!Array.isArray(result))
303
+ throw new Error(`eth_getLogs on ${rpc}: bad result ${JSON.stringify(result)}`);
314
304
  result.forEach((i) => {
315
305
  i.blockNumber = parseInt(i.blockNumber);
316
306
  i.transactionIndex = parseInt(i.transactionIndex);
@@ -320,14 +310,7 @@ const httpRPC = {
320
310
  return result;
321
311
  },
322
312
  getTransaction: async (rpc, params) => {
323
- const { data: { result, error } } = await axios_1.default.post(rpc, {
324
- jsonrpc: '2.0', id: 1, params,
325
- method: 'eth_getTransactionByHash',
326
- }, {
327
- timeout: +(0, env_1.getEnvValue)('LLAMA_PROVIDER_RPC_GET_TRANSACTION_TIMEOUT', '180000')
328
- });
329
- if (error)
330
- throw error;
313
+ const result = await rpcPost(rpc, 'eth_getTransactionByHash', params, 'LLAMA_PROVIDER_RPC_GET_TRANSACTION_TIMEOUT', '180000');
331
314
  if (!result)
332
315
  return null;
333
316
  result.blockNumber = parseInt(result.blockNumber);
@@ -336,14 +319,7 @@ const httpRPC = {
336
319
  return result;
337
320
  },
338
321
  getTransactionReceipt: async (rpc, params) => {
339
- const { data: { result, error } } = await axios_1.default.post(rpc, {
340
- jsonrpc: '2.0', id: 1, params,
341
- method: 'eth_getTransactionReceipt',
342
- }, {
343
- timeout: +(0, env_1.getEnvValue)('LLAMA_PROVIDER_RPC_GET_TXN_RECEIPT_TIMEOUT', '180000')
344
- });
345
- if (error)
346
- throw error;
322
+ const result = await rpcPost(rpc, 'eth_getTransactionReceipt', params, 'LLAMA_PROVIDER_RPC_GET_TXN_RECEIPT_TIMEOUT', '180000');
347
323
  if (!result)
348
324
  return null;
349
325
  result.blockNumber = parseInt(result.blockNumber);
@@ -0,0 +1,74 @@
1
+ export declare const ES_INDEX = "adapter-call-stats";
2
+ export declare const DEFAULT_MIN_CALLS = 50;
3
+ /** a chain bucket is also kept when it has at least this many getLogs requests, however few calls it made */
4
+ export declare const DEFAULT_MIN_GET_LOGS = 10;
5
+ export type CallStatsMeta = {
6
+ /** 'tvl' | 'dimensions' | ... */
7
+ application: string;
8
+ /** protocol / module name */
9
+ name: string;
10
+ [key: string]: any;
11
+ };
12
+ export type CallStats = {
13
+ meta: CallStatsMeta;
14
+ startTime: number;
15
+ total: number;
16
+ /** chain -> requests to a registered rpc endpoint of that chain */
17
+ byChain: Record<string, number>;
18
+ /** host -> requests, every request */
19
+ byHost: Record<string, number>;
20
+ /** requests that matched no registered endpoint */
21
+ unattributed: number;
22
+ /**
23
+ * logical rpc method -> chain -> requests, recorded by the SDK at the call site (`recordMethod`):
24
+ * every LlamaProvider attempt (`call`, `getLogs`, `getBlock`, ...), starknet `getLogs`, and `indexerGetLogs`
25
+ */
26
+ byMethod: Record<string, Record<string, number>>;
27
+ };
28
+ export type CallStatsSnapshot = Pick<CallStats, 'total' | 'byChain' | 'byHost' | 'unattributed' | 'byMethod'>;
29
+ /**
30
+ * Remember that `endpoints` serve `chain`, so requests to them are counted against that chain.
31
+ * Called by the SDK wherever it resolves rpc urls; adapters can call it for endpoints they manage themselves.
32
+ */
33
+ export declare function registerEndpoints(chain: string | undefined, endpoints: string | string[] | undefined): void;
34
+ /** Chain a url is counted against, or undefined when it matches no registered endpoint. */
35
+ export declare function resolveChain(url: string): string | undefined;
36
+ /** Requests to these hosts are not counted (eg. the elasticsearch cluster the stats are written to). */
37
+ export declare function ignoreHost(url: string): void;
38
+ /**
39
+ * Count a logical rpc request (one attempt, so retries count) against `method` and `chain` in the current run.
40
+ * The http layer already counts the request itself; this adds the method breakdown (getLogs being the one that matters).
41
+ */
42
+ export declare function recordMethod(method: string, chain?: string): void;
43
+ /** Patch the node http layer. Idempotent, shared across sdk copies. Called by `run`. */
44
+ export declare function install(): void;
45
+ export type RunOptions = {
46
+ /** chain / host buckets below this are dropped, and nothing is written when the total is below it (default 50, env LLAMA_SDK_CALL_STATS_MIN_CALLS) */
47
+ minCalls?: number;
48
+ /** print the summary table to the console (always, not only in debug mode) */
49
+ print?: boolean;
50
+ /** skip the elasticsearch write */
51
+ skipElastic?: boolean;
52
+ };
53
+ /** Stats of the current run context, or the process-wide bucket when called outside `run`. */
54
+ export declare function getCurrent(): CallStats;
55
+ export declare function snapshot(stats?: CallStats): CallStatsSnapshot;
56
+ export declare function mergeSnapshots(snapshots: CallStatsSnapshot[]): CallStatsSnapshot;
57
+ /**
58
+ * Run `fn` with its own counters and report them when it settles (success or failure).
59
+ * Nested calls get their own context; calls made by `fn` through any http client are attributed to it.
60
+ */
61
+ export declare function run<T>(meta: CallStatsMeta, fn: () => Promise<T>, options?: RunOptions): Promise<T>;
62
+ export declare function getMinCalls(minCalls?: number): number;
63
+ /** Elasticsearch documents for a run: one `run` summary, one `chain` doc per chain and one `host` doc per unattributed host at or above `minCalls`. */
64
+ export declare function buildDocs(stats: CallStatsSnapshot, meta: CallStatsMeta, { success, runtime, minCalls }?: {
65
+ success?: boolean;
66
+ runtime?: number;
67
+ minCalls?: number;
68
+ }): any[];
69
+ /** Write the docs for `stats` to elasticsearch (no-op when ELASTICSEARCH_CONFIG is unset) and print the summary in debug mode. */
70
+ export declare function report(stats: CallStats | CallStatsSnapshot, { success, runtime, minCalls, print, skipElastic, meta }?: RunOptions & {
71
+ success?: boolean;
72
+ runtime?: number;
73
+ meta?: CallStatsMeta;
74
+ }): Promise<void>;
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ var _a;
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.DEFAULT_MIN_GET_LOGS = exports.DEFAULT_MIN_CALLS = exports.ES_INDEX = void 0;
8
+ exports.registerEndpoints = registerEndpoints;
9
+ exports.resolveChain = resolveChain;
10
+ exports.ignoreHost = ignoreHost;
11
+ exports.recordMethod = recordMethod;
12
+ exports.install = install;
13
+ exports.getCurrent = getCurrent;
14
+ exports.snapshot = snapshot;
15
+ exports.mergeSnapshots = mergeSnapshots;
16
+ exports.run = run;
17
+ exports.getMinCalls = getMinCalls;
18
+ exports.buildDocs = buildDocs;
19
+ exports.report = report;
20
+ /**
21
+ * Per-run counters for every outbound HTTP request a process makes, attributed to a chain
22
+ * and to the adapter run that made them, and flushed to Elasticsearch when the run ends.
23
+ *
24
+ * How it works
25
+ * - `install()` wraps `http.request` / `http.get` / `https.request` / `https.get` and subscribes to
26
+ * the `undici:request:create` diagnostics channel (global `fetch`). That catches axios, ethers,
27
+ * @solana/web3.js, graphql-request, raw fetch... without touching any call site.
28
+ * - The SDK registers the RPC urls it resolves for a chain (`registerEndpoints`, called from
29
+ * LlamaProvider and the `chains/*` endpoint resolvers). A request whose url starts with a
30
+ * registered endpoint is counted against that chain, everything else against its host only.
31
+ * - `run(meta, fn)` executes `fn` inside an AsyncLocalStorage context, so concurrent adapter runs
32
+ * in one process (the server's pooled children) each get their own counters.
33
+ *
34
+ * All state lives on `globalThis` under a well-known symbol: when two copies of the SDK are loaded
35
+ * (server's node_modules + the adapters repo's node_modules) they share one hook, one endpoint
36
+ * registry and one context, and nothing is counted twice.
37
+ */
38
+ const async_hooks_1 = require("async_hooks");
39
+ const http_1 = __importDefault(require("http"));
40
+ const https_1 = __importDefault(require("https"));
41
+ const diagnostics_channel_1 = __importDefault(require("diagnostics_channel"));
42
+ const elastic_1 = require("./elastic");
43
+ const debugLog_1 = require("./debugLog");
44
+ const env_1 = require("./env");
45
+ exports.ES_INDEX = 'adapter-call-stats';
46
+ exports.DEFAULT_MIN_CALLS = 50;
47
+ /** a chain bucket is also kept when it has at least this many getLogs requests, however few calls it made */
48
+ exports.DEFAULT_MIN_GET_LOGS = 10;
49
+ const STATE_KEY = Symbol.for('@defillama/sdk/callStats');
50
+ function newStats(meta) {
51
+ return { meta, startTime: Date.now(), total: 0, byChain: {}, byHost: {}, unattributed: 0, byMethod: {} };
52
+ }
53
+ const shared = (_a = globalThis[STATE_KEY]) !== null && _a !== void 0 ? _a : (globalThis[STATE_KEY] = {
54
+ als: new async_hooks_1.AsyncLocalStorage(),
55
+ installed: false,
56
+ endpoints: new Map(),
57
+ global: newStats({ application: 'global', name: '-' }),
58
+ ignoredHosts: new Set(),
59
+ registered: new Set(),
60
+ });
61
+ // ---------------------------------------------------------------------------
62
+ // endpoint registry
63
+ // ---------------------------------------------------------------------------
64
+ function parseUrl(url) {
65
+ try {
66
+ const u = new URL(url);
67
+ if (u.protocol !== 'http:' && u.protocol !== 'https:')
68
+ return;
69
+ return { host: u.host.toLowerCase(), path: u.pathname.replace(/\/+$/, '') };
70
+ }
71
+ catch {
72
+ return;
73
+ }
74
+ }
75
+ /**
76
+ * Remember that `endpoints` serve `chain`, so requests to them are counted against that chain.
77
+ * Called by the SDK wherever it resolves rpc urls; adapters can call it for endpoints they manage themselves.
78
+ */
79
+ function registerEndpoints(chain, endpoints) {
80
+ if (!chain || !endpoints)
81
+ return;
82
+ chain = chain.toLowerCase();
83
+ // callers pass the same list on every request: skip the url parsing after the first time
84
+ const key = chain + '|' + (Array.isArray(endpoints) ? endpoints.join(',') : endpoints);
85
+ if (shared.registered.has(key))
86
+ return;
87
+ shared.registered.add(key);
88
+ const list = Array.isArray(endpoints) ? endpoints : endpoints.split(',');
89
+ for (const raw of list) {
90
+ const parsed = parseUrl(String(raw).trim());
91
+ if (!parsed)
92
+ continue;
93
+ let entries = shared.endpoints.get(parsed.host);
94
+ if (!entries) {
95
+ entries = [];
96
+ shared.endpoints.set(parsed.host, entries);
97
+ }
98
+ if (entries.some(e => e.path === parsed.path && e.chain === chain))
99
+ continue;
100
+ entries.push({ path: parsed.path, chain });
101
+ entries.sort((a, b) => b.path.length - a.path.length);
102
+ }
103
+ }
104
+ /** Chain a url is counted against, or undefined when it matches no registered endpoint. */
105
+ function resolveChain(url) {
106
+ const parsed = parseUrl(url);
107
+ if (!parsed)
108
+ return;
109
+ const entries = shared.endpoints.get(parsed.host);
110
+ if (!entries)
111
+ return;
112
+ for (const entry of entries) {
113
+ if (!entry.path || parsed.path === entry.path || parsed.path.startsWith(entry.path + '/'))
114
+ return entry.chain;
115
+ }
116
+ }
117
+ /** Requests to these hosts are not counted (eg. the elasticsearch cluster the stats are written to). */
118
+ function ignoreHost(url) {
119
+ const parsed = parseUrl(url);
120
+ if (parsed)
121
+ shared.ignoredHosts.add(parsed.host);
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // recording
125
+ // ---------------------------------------------------------------------------
126
+ function record(url) {
127
+ var _a, _b, _c;
128
+ const parsed = parseUrl(url);
129
+ if (!parsed || shared.ignoredHosts.has(parsed.host))
130
+ return;
131
+ const stats = (_a = shared.als.getStore()) !== null && _a !== void 0 ? _a : shared.global;
132
+ stats.total++;
133
+ stats.byHost[parsed.host] = ((_b = stats.byHost[parsed.host]) !== null && _b !== void 0 ? _b : 0) + 1;
134
+ const chain = resolveChain(url);
135
+ if (chain)
136
+ stats.byChain[chain] = ((_c = stats.byChain[chain]) !== null && _c !== void 0 ? _c : 0) + 1;
137
+ else
138
+ stats.unattributed++;
139
+ }
140
+ /**
141
+ * Count a logical rpc request (one attempt, so retries count) against `method` and `chain` in the current run.
142
+ * The http layer already counts the request itself; this adds the method breakdown (getLogs being the one that matters).
143
+ */
144
+ function recordMethod(method, chain = 'unknown') {
145
+ var _a, _b, _c;
146
+ const stats = (_a = shared.als.getStore()) !== null && _a !== void 0 ? _a : shared.global;
147
+ const byChain = (_b = stats.byMethod[method]) !== null && _b !== void 0 ? _b : (stats.byMethod[method] = {});
148
+ chain = chain.toLowerCase();
149
+ byChain[chain] = ((_c = byChain[chain]) !== null && _c !== void 0 ? _c : 0) + 1;
150
+ }
151
+ function getLogsCount(stats, chain) {
152
+ var _a, _b, _c;
153
+ const byChain = (_b = (_a = stats.byMethod) === null || _a === void 0 ? void 0 : _a.getLogs) !== null && _b !== void 0 ? _b : {};
154
+ if (chain)
155
+ return (_c = byChain[chain]) !== null && _c !== void 0 ? _c : 0;
156
+ return Object.values(byChain).reduce((a, b) => a + b, 0);
157
+ }
158
+ /** method -> requests across chains */
159
+ function methodTotals(stats) {
160
+ var _a;
161
+ const out = {};
162
+ for (const [method, byChain] of Object.entries((_a = stats.byMethod) !== null && _a !== void 0 ? _a : {}))
163
+ out[method] = Object.values(byChain).reduce((a, b) => a + b, 0);
164
+ return out;
165
+ }
166
+ /** method -> requests for one chain (only methods the chain used) */
167
+ function methodsForChain(stats, chain) {
168
+ var _a;
169
+ const out = {};
170
+ for (const [method, byChain] of Object.entries((_a = stats.byMethod) !== null && _a !== void 0 ? _a : {}))
171
+ if (byChain[chain])
172
+ out[method] = byChain[chain];
173
+ return out;
174
+ }
175
+ function urlFromRequestArgs(args, defaultProtocol) {
176
+ let url;
177
+ let options;
178
+ if (typeof args[0] === 'string') {
179
+ url = args[0];
180
+ if (args[1] && typeof args[1] === 'object')
181
+ options = args[1];
182
+ }
183
+ else if (args[0] instanceof URL) {
184
+ url = args[0].href;
185
+ if (args[1] && typeof args[1] === 'object')
186
+ options = args[1];
187
+ }
188
+ else if (args[0] && typeof args[0] === 'object') {
189
+ options = args[0];
190
+ }
191
+ if (url && (!options || (!options.hostname && !options.host && !options.path)))
192
+ return url;
193
+ if (!options)
194
+ return url;
195
+ // options override the url (same precedence as node)
196
+ const base = url ? new URL(url) : undefined;
197
+ const protocol = options.protocol || (base === null || base === void 0 ? void 0 : base.protocol) || defaultProtocol;
198
+ const host = options.hostname || options.host || (base === null || base === void 0 ? void 0 : base.hostname) || 'localhost';
199
+ const port = options.port ? `:${options.port}` : (options.hostname || !options.host) && (base === null || base === void 0 ? void 0 : base.port) ? `:${base.port}` : '';
200
+ const path = options.path || (base ? base.pathname + base.search : '/');
201
+ return `${protocol}//${host}${host.includes(':') ? '' : port}${path}`;
202
+ }
203
+ function wrap(mod, name, defaultProtocol) {
204
+ const original = mod[name];
205
+ if (typeof original !== 'function')
206
+ return;
207
+ const patched = function (...args) {
208
+ try {
209
+ const url = urlFromRequestArgs(args, defaultProtocol);
210
+ if (url)
211
+ record(url);
212
+ }
213
+ catch { /* never break the request */ }
214
+ return original.apply(this, args);
215
+ };
216
+ Object.defineProperty(patched, 'name', { value: original.name });
217
+ mod[name] = patched;
218
+ }
219
+ /** Patch the node http layer. Idempotent, shared across sdk copies. Called by `run`. */
220
+ function install() {
221
+ var _a, _b;
222
+ if (shared.installed)
223
+ return;
224
+ shared.installed = true;
225
+ wrap(http_1.default, 'request', 'http:');
226
+ wrap(http_1.default, 'get', 'http:');
227
+ wrap(https_1.default, 'request', 'https:');
228
+ wrap(https_1.default, 'get', 'https:');
229
+ try {
230
+ // global fetch (undici); node < 18.7 has no subscribe, fetch is then simply not counted
231
+ ;
232
+ (_b = (_a = diagnostics_channel_1.default).subscribe) === null || _b === void 0 ? void 0 : _b.call(_a, 'undici:request:create', (message) => {
233
+ var _a;
234
+ try {
235
+ const request = message === null || message === void 0 ? void 0 : message.request;
236
+ if (request === null || request === void 0 ? void 0 : request.origin)
237
+ record(String(request.origin) + String((_a = request.path) !== null && _a !== void 0 ? _a : ''));
238
+ }
239
+ catch { /* ignore */ }
240
+ });
241
+ }
242
+ catch { /* ignore */ }
243
+ try {
244
+ const esConfig = (0, env_1.getEnvValue)('ELASTICSEARCH_CONFIG');
245
+ if (esConfig)
246
+ ignoreHost(JSON.parse(esConfig.replace(/\\"/g, '"')).host);
247
+ }
248
+ catch { /* ignore */ }
249
+ }
250
+ /** Stats of the current run context, or the process-wide bucket when called outside `run`. */
251
+ function getCurrent() {
252
+ var _a;
253
+ return (_a = shared.als.getStore()) !== null && _a !== void 0 ? _a : shared.global;
254
+ }
255
+ function snapshot(stats = getCurrent()) {
256
+ const byMethod = {};
257
+ for (const [method, byChain] of Object.entries(stats.byMethod))
258
+ byMethod[method] = { ...byChain };
259
+ return { total: stats.total, byChain: { ...stats.byChain }, byHost: { ...stats.byHost }, unattributed: stats.unattributed, byMethod };
260
+ }
261
+ function mergeSnapshots(snapshots) {
262
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
263
+ const out = { total: 0, byChain: {}, byHost: {}, unattributed: 0, byMethod: {} };
264
+ for (const s of snapshots) {
265
+ if (!s)
266
+ continue;
267
+ out.total += (_a = s.total) !== null && _a !== void 0 ? _a : 0;
268
+ out.unattributed += (_b = s.unattributed) !== null && _b !== void 0 ? _b : 0;
269
+ for (const [k, v] of Object.entries((_c = s.byChain) !== null && _c !== void 0 ? _c : {}))
270
+ out.byChain[k] = ((_d = out.byChain[k]) !== null && _d !== void 0 ? _d : 0) + v;
271
+ for (const [k, v] of Object.entries((_e = s.byHost) !== null && _e !== void 0 ? _e : {}))
272
+ out.byHost[k] = ((_f = out.byHost[k]) !== null && _f !== void 0 ? _f : 0) + v;
273
+ for (const [method, byChain] of Object.entries((_g = s.byMethod) !== null && _g !== void 0 ? _g : {})) {
274
+ const target = (_h = out.byMethod[method]) !== null && _h !== void 0 ? _h : (out.byMethod[method] = {});
275
+ for (const [k, v] of Object.entries(byChain))
276
+ target[k] = ((_j = target[k]) !== null && _j !== void 0 ? _j : 0) + v;
277
+ }
278
+ }
279
+ return out;
280
+ }
281
+ /**
282
+ * Run `fn` with its own counters and report them when it settles (success or failure).
283
+ * Nested calls get their own context; calls made by `fn` through any http client are attributed to it.
284
+ */
285
+ async function run(meta, fn, options = {}) {
286
+ install();
287
+ const stats = newStats(meta);
288
+ return shared.als.run(stats, async () => {
289
+ let success = false;
290
+ try {
291
+ const result = await fn();
292
+ success = true;
293
+ return result;
294
+ }
295
+ finally {
296
+ try {
297
+ await report(stats, { success, runtime: (Date.now() - stats.startTime) / 1000, ...options });
298
+ }
299
+ catch (e) {
300
+ (0, debugLog_1.debugLog)('[callStats] failed to report', e === null || e === void 0 ? void 0 : e.message);
301
+ }
302
+ }
303
+ });
304
+ }
305
+ // ---------------------------------------------------------------------------
306
+ // reporting
307
+ // ---------------------------------------------------------------------------
308
+ function sortedEntries(map, minCalls, limit) {
309
+ const entries = Object.entries(map).filter(([, calls]) => calls >= minCalls).map(([key, calls]) => ({ key, calls }));
310
+ entries.sort((a, b) => b.calls - a.calls);
311
+ return limit ? entries.slice(0, limit) : entries;
312
+ }
313
+ function getMinCalls(minCalls) {
314
+ if (minCalls !== undefined)
315
+ return minCalls;
316
+ return +(0, env_1.getEnvValue)('CALL_STATS_MIN_CALLS', String(exports.DEFAULT_MIN_CALLS));
317
+ }
318
+ function getMinGetLogs() {
319
+ return +(0, env_1.getEnvValue)('CALL_STATS_MIN_GET_LOGS', String(exports.DEFAULT_MIN_GET_LOGS));
320
+ }
321
+ /**
322
+ * Chains worth a row: at least `minCalls` http requests, or at least `minGetLogs` getLogs requests
323
+ * (a chain that makes 20 getLogs calls is heavier than one making 50 eth_calls). Most calls first.
324
+ */
325
+ function chainEntries(stats, minCalls) {
326
+ var _a, _b;
327
+ const minGetLogs = getMinGetLogs();
328
+ const chains = new Set([...Object.keys(stats.byChain), ...Object.keys((_b = (_a = stats.byMethod) === null || _a === void 0 ? void 0 : _a.getLogs) !== null && _b !== void 0 ? _b : {})]);
329
+ const entries = [...chains]
330
+ .map(key => { var _a; return ({ key, calls: (_a = stats.byChain[key]) !== null && _a !== void 0 ? _a : 0, getLogs: getLogsCount(stats, key) }); })
331
+ .filter(i => i.calls >= minCalls || i.getLogs >= minGetLogs);
332
+ entries.sort((a, b) => b.calls - a.calls || b.getLogs - a.getLogs);
333
+ return entries;
334
+ }
335
+ /** Elasticsearch documents for a run: one `run` summary, one `chain` doc per chain and one `host` doc per unattributed host at or above `minCalls`. */
336
+ function buildDocs(stats, meta, { success, runtime, minCalls } = {}) {
337
+ minCalls = getMinCalls(minCalls);
338
+ const timestamp = Date.now();
339
+ const chains = chainEntries(stats, minCalls);
340
+ const hosts = sortedEntries(stats.byHost, minCalls, 25);
341
+ const rpcCalls = Object.values(stats.byChain).reduce((a, b) => a + b, 0);
342
+ const getLogsCalls = getLogsCount(stats);
343
+ const unattributedHosts = sortedEntries(Object.fromEntries(Object.entries(stats.byHost).filter(([host]) => !shared.endpoints.has(host))), minCalls);
344
+ const docs = [];
345
+ if (stats.total < minCalls && getLogsCalls < getMinGetLogs())
346
+ return docs;
347
+ docs.push({
348
+ timestamp, type: 'run', metadata: meta, success, runtime,
349
+ calls: stats.total, rpcCalls, otherCalls: stats.unattributed, getLogsCalls,
350
+ methods: methodTotals(stats),
351
+ chains: chains.map(i => ({ chain: i.key, calls: i.calls, getLogs: i.getLogs })),
352
+ hosts: hosts.map(i => ({ host: i.key, calls: i.calls })),
353
+ });
354
+ for (const i of chains)
355
+ docs.push({ timestamp, type: 'chain', metadata: meta, chain: i.key, calls: i.calls, getLogs: i.getLogs, methods: methodsForChain(stats, i.key) });
356
+ for (const i of unattributedHosts)
357
+ docs.push({ timestamp, type: 'host', metadata: meta, host: i.key, calls: i.calls });
358
+ return docs;
359
+ }
360
+ /** Write the docs for `stats` to elasticsearch (no-op when ELASTICSEARCH_CONFIG is unset) and print the summary in debug mode. */
361
+ async function report(stats, { success, runtime, minCalls, print, skipElastic, meta } = {}) {
362
+ var _a;
363
+ meta = (_a = meta !== null && meta !== void 0 ? meta : stats.meta) !== null && _a !== void 0 ? _a : { application: 'unknown', name: '-' };
364
+ minCalls = getMinCalls(minCalls);
365
+ const docs = buildDocs(stats, meta, { success, runtime, minCalls });
366
+ if (print)
367
+ printStats(stats, meta, minCalls, console.log, console.table);
368
+ else
369
+ printStats(stats, meta, minCalls, debugLog_1.debugLog, debugLog_1.debugTable);
370
+ if (skipElastic || !docs.length)
371
+ return;
372
+ await Promise.all(docs.map(doc => (0, elastic_1.writeLog)(exports.ES_INDEX, doc)));
373
+ }
374
+ function printStats(stats, meta, minCalls, log, table) {
375
+ const getLogsCalls = getLogsCount(stats);
376
+ if (stats.total < minCalls && getLogsCalls < getMinGetLogs())
377
+ return;
378
+ log(`[callStats] ${meta.application}/${meta.name}: ${stats.total} http requests (${stats.unattributed} not matching a known rpc, ${getLogsCalls} getLogs)`);
379
+ const rows = [
380
+ ...chainEntries(stats, minCalls).map(i => ({ chain: i.key, calls: i.calls, getLogs: i.getLogs })),
381
+ ...sortedEntries(Object.fromEntries(Object.entries(stats.byHost).filter(([host]) => !shared.endpoints.has(host))), minCalls).map(i => ({ chain: `host: ${i.key}`, calls: i.calls, getLogs: 0 })),
382
+ ];
383
+ if (rows.length)
384
+ table(rows);
385
+ }