@steerprotocol/liquidity-meter 1.0.0 → 2.0.4

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.
package/package.json CHANGED
@@ -1,47 +1,57 @@
1
1
  {
2
2
  "name": "@steerprotocol/liquidity-meter",
3
- "version": "1.0.0",
4
- "public": true,
3
+ "version": "2.0.4",
4
+ "private": false,
5
5
  "description": "CLI to compute Uniswap v3 market depth bands using real on-chain/subgraph data",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/SteerProtocol/liquidity-meter.git"
9
+ },
6
10
  "type": "module",
11
+ "main": "./src/index.js",
12
+ "exports": {
13
+ ".": "./src/index.js",
14
+ "./api": "./src/api.js",
15
+ "./withdraw": "./src/withdraw.js",
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src",
21
+ "README.md",
22
+ "templates"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
7
27
  "bin": {
8
- "liquidity-depth": "dist/cli/liquidity-depth.js"
28
+ "liquidity-depth": "bin/liquidity-depth.js"
9
29
  },
10
30
  "engines": {
11
31
  "node": ">=18"
12
32
  },
13
33
  "scripts": {
14
- "build": "tsc -p tsconfig.json",
15
- "generate": "wagmi generate",
16
- "start": "node dist/cli/liquidity-depth.js --help"
34
+ "start": "node bin/liquidity-depth.js --help",
35
+ "test": "node --test",
36
+ "test:coverage": "node ./scripts/check-coverage.mjs",
37
+ "lint:commits": "commitlint --from HEAD~1 --to HEAD",
38
+ "release": "semantic-release"
39
+ },
40
+ "devDependencies": {
41
+ "@commitlint/cli": "20.5.0",
42
+ "@commitlint/config-conventional": "20.5.0",
43
+ "@semantic-release/commit-analyzer": "13.0.1",
44
+ "@semantic-release/github": "11.0.6",
45
+ "@semantic-release/npm": "13.1.3",
46
+ "@semantic-release/release-notes-generator": "14.1.0",
47
+ "semantic-release": "25.0.3"
17
48
  },
18
49
  "dependencies": {
19
- "@wagmi/core": "^2.20.3",
20
- "axios": "^1.7.7",
21
- "tevm": "^1.0.0-next.147",
22
- "viem": "^2.21.13"
50
+ "axios": "1.7.7",
51
+ "tevm": "1.0.0-next.149",
52
+ "viem": "2.37.9"
23
53
  },
24
- "devDependencies": {
25
- "@types/node": "^20.19.11",
26
- "@wagmi/cli": "^2.5.1",
27
- "typescript": "^5.9.2"
28
- }
29
- ,
30
- "types": "dist/index.d.ts",
31
- "files": [
32
- "dist"
33
- ],
34
- "exports": {
35
- ".": {
36
- "types": "./dist/index.d.ts",
37
- "import": "./dist/index.js"
38
- },
39
- "./cli": {
40
- "import": "./dist/cli/liquidity-depth.js"
41
- },
42
- "./prices": {
43
- "types": "./dist/prices.d.ts",
44
- "import": "./dist/prices.js"
45
- }
54
+ "overrides": {
55
+ "viem": "2.37.9"
46
56
  }
47
57
  }
package/src/api.js ADDED
@@ -0,0 +1,568 @@
1
+ import { createPublicClient, http as viemHttp } from 'viem';
2
+ import {
3
+ computeMarketDepthBands,
4
+ computePriceImpactsBySizes,
5
+ } from './depth.js';
6
+ import { fetchPoolSnapshot as fetchFromSubgraph } from './uniswapv3-subgraph.js';
7
+
8
+ const chainIdByRpcUrl = new Map();
9
+ const publicClientByRpcUrl = new Map();
10
+
11
+ function getCachedPublicClient(rpcUrl) {
12
+ if (!rpcUrl) return null;
13
+ let client = publicClientByRpcUrl.get(rpcUrl);
14
+ if (!client) {
15
+ client = createPublicClient({ transport: viemHttp(rpcUrl) });
16
+ publicClientByRpcUrl.set(rpcUrl, client);
17
+ }
18
+ return client;
19
+ }
20
+
21
+ export function parsePercentBuckets(input = '1,2,5,10') {
22
+ if (Array.isArray(input)) {
23
+ return input
24
+ .map((value) => Number(value))
25
+ .filter((value) => Number.isFinite(value) && value > 0)
26
+ .sort((a, b) => a - b);
27
+ }
28
+ return String(input)
29
+ .split(',')
30
+ .map((value) => Number(value.trim()))
31
+ .filter((value) => Number.isFinite(value) && value > 0)
32
+ .sort((a, b) => a - b);
33
+ }
34
+
35
+ export function parseBlockIdentifier(input) {
36
+ if (input == null) return undefined;
37
+ const raw = String(input).trim();
38
+ if (!raw) throw new Error('Empty block identifier');
39
+ const lowered = raw.toLowerCase();
40
+ if (
41
+ lowered === 'latest' ||
42
+ lowered === 'pending' ||
43
+ lowered === 'safe' ||
44
+ lowered === 'finalized'
45
+ ) {
46
+ return undefined;
47
+ }
48
+ let value;
49
+ if (/^0x[0-9a-f]+$/i.test(raw)) value = BigInt(raw);
50
+ else if (/^-?\d+$/.test(raw)) value = BigInt(raw);
51
+ else throw new Error(`Invalid block identifier: ${raw}`);
52
+ if (value < 0n) throw new Error('Block number must be non-negative');
53
+ return value;
54
+ }
55
+
56
+ export function parseTimestampArg(input) {
57
+ if (input == null) return undefined;
58
+ const raw = String(input).trim();
59
+ if (!raw) throw new Error('Empty timestamp');
60
+ if (/^-?\d+$/.test(raw)) {
61
+ let ts = BigInt(raw);
62
+ if (ts < 0n) throw new Error('Timestamp must be non-negative');
63
+ if (ts > 1_000_000_000_000n) ts /= 1000n;
64
+ return ts;
65
+ }
66
+ const ms = Date.parse(raw);
67
+ if (!Number.isFinite(ms)) throw new Error(`Invalid timestamp: ${raw}`);
68
+ if (ms < 0) throw new Error('Timestamp must be >= 1970-01-01');
69
+ return BigInt(Math.floor(ms / 1000));
70
+ }
71
+
72
+ export function parseDurationArg(input) {
73
+ if (input == null) throw new Error('Missing duration');
74
+ const raw = String(input).trim();
75
+ if (!raw) throw new Error('Empty duration');
76
+ const match = raw.match(/^([0-9]+(?:\.[0-9]+)?)([a-zA-Z]*)$/);
77
+ if (!match) throw new Error(`Invalid duration: ${raw}`);
78
+ const value = Number(match[1]);
79
+ if (!Number.isFinite(value) || value <= 0) {
80
+ throw new Error('Duration must be positive');
81
+ }
82
+ const unit = match[2].toLowerCase();
83
+ const multipliers = {
84
+ '': 1,
85
+ s: 1,
86
+ m: 60,
87
+ h: 3600,
88
+ d: 86400,
89
+ w: 604800,
90
+ };
91
+ const multiplier = multipliers[unit];
92
+ if (!multiplier) {
93
+ throw new Error(`Unsupported duration unit in ${raw} (use s, m, h, d, w)`);
94
+ }
95
+ const rounded = Math.round(value * multiplier);
96
+ if (!Number.isFinite(rounded) || rounded <= 0) {
97
+ throw new Error(`Duration out of range: ${raw}`);
98
+ }
99
+ return BigInt(rounded);
100
+ }
101
+
102
+ export function formatTimestampUTC(seconds) {
103
+ if (seconds == null) return undefined;
104
+ try {
105
+ const numeric = Number(seconds);
106
+ if (!Number.isFinite(numeric)) return undefined;
107
+ return new Date(numeric * 1000).toISOString();
108
+ } catch (_) {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ export async function findBlockNumberForTimestamp({
114
+ rpcUrl,
115
+ targetTimestampSec,
116
+ debug = false,
117
+ client,
118
+ bounds,
119
+ blockCache,
120
+ }) {
121
+ if (!rpcUrl) throw new Error('RPC URL required to resolve timestamp');
122
+ const resolvedClient = client || getCachedPublicClient(rpcUrl);
123
+ const cache = blockCache || new Map();
124
+ const readBlock = async (blockNumber) => {
125
+ const key = blockNumber.toString();
126
+ if (cache.has(key)) return cache.get(key);
127
+ const block = await resolvedClient.getBlock({ blockNumber });
128
+ cache.set(key, block);
129
+ return block;
130
+ };
131
+ const latestNumber = bounds?.latestNumber ?? await resolvedClient.getBlockNumber();
132
+ const latestBlock = bounds?.latestBlock ?? await readBlock(latestNumber);
133
+ if (!latestBlock || latestBlock.timestamp == null) {
134
+ throw new Error('Latest block did not include a timestamp');
135
+ }
136
+ const latestTs = latestBlock.timestamp;
137
+ const genesis = bounds?.genesisBlock ?? await readBlock(0n);
138
+ if (!genesis || genesis.timestamp == null) {
139
+ throw new Error('Genesis block did not include a timestamp');
140
+ }
141
+ if (targetTimestampSec <= genesis.timestamp) return 0n;
142
+ if (targetTimestampSec >= latestTs) return latestBlock.number;
143
+
144
+ let lo = bounds?.loBlock?.number ?? 0n;
145
+ let loTimestamp = bounds?.loBlock?.timestamp ?? genesis.timestamp;
146
+ if (loTimestamp > targetTimestampSec) {
147
+ lo = 0n;
148
+ loTimestamp = genesis.timestamp;
149
+ }
150
+ let hi = latestBlock.number;
151
+ let best = loTimestamp <= targetTimestampSec ? lo : (genesis.number ?? 0n);
152
+ for (let iter = 0; iter < 80 && lo <= hi; iter++) {
153
+ const mid = (lo + hi) >> 1n;
154
+ const block = await readBlock(mid);
155
+ if (!block || block.timestamp == null) {
156
+ hi = mid - 1n;
157
+ continue;
158
+ }
159
+ if (block.timestamp === targetTimestampSec) {
160
+ return block.number;
161
+ }
162
+ if (block.timestamp < targetTimestampSec) {
163
+ best = block.number;
164
+ lo = mid + 1n;
165
+ } else {
166
+ if (mid === 0n) break;
167
+ hi = mid - 1n;
168
+ }
169
+ }
170
+ if (debug) {
171
+ console.error(`[api] resolved timestamp ${targetTimestampSec} -> block ${best}`);
172
+ }
173
+ return best;
174
+ }
175
+
176
+ export async function findBlockNumbersForTimestamps({
177
+ rpcUrl,
178
+ timestamps,
179
+ debug = false,
180
+ client,
181
+ }) {
182
+ if (!timestamps?.length) return [];
183
+ const resolvedClient = client || getCachedPublicClient(rpcUrl);
184
+ const blockCache = new Map();
185
+ const latestNumber = await resolvedClient.getBlockNumber();
186
+ const latestBlock = await resolvedClient.getBlock({ blockNumber: latestNumber });
187
+ const genesisBlock = await resolvedClient.getBlock({ blockNumber: 0n });
188
+ blockCache.set(latestNumber.toString(), latestBlock);
189
+ blockCache.set('0', genesisBlock);
190
+
191
+ let previousBlock = genesisBlock;
192
+ const out = [];
193
+ for (const timestamp of timestamps) {
194
+ const blockNumber = await findBlockNumberForTimestamp({
195
+ rpcUrl,
196
+ targetTimestampSec: timestamp,
197
+ debug,
198
+ client: resolvedClient,
199
+ bounds: {
200
+ latestNumber,
201
+ latestBlock,
202
+ genesisBlock,
203
+ loBlock: previousBlock,
204
+ },
205
+ blockCache,
206
+ });
207
+ const resolvedBlock =
208
+ blockCache.get(blockNumber.toString()) ||
209
+ await resolvedClient.getBlock({ blockNumber });
210
+ blockCache.set(blockNumber.toString(), resolvedBlock);
211
+ previousBlock = resolvedBlock;
212
+ out.push(blockNumber);
213
+ }
214
+ return out;
215
+ }
216
+
217
+ export async function resolveAnalysisBlockContext({
218
+ block,
219
+ timestamp,
220
+ rpcUrl,
221
+ debug = false,
222
+ blockTag,
223
+ client,
224
+ }) {
225
+ let blockNumber;
226
+ let blockSource;
227
+ if (block != null) {
228
+ const parsed = parseBlockIdentifier(block);
229
+ if (parsed !== undefined) {
230
+ blockNumber = parsed;
231
+ blockSource = '--block';
232
+ }
233
+ }
234
+
235
+ let timestampSec;
236
+ if (blockNumber === undefined && timestamp != null) {
237
+ timestampSec = parseTimestampArg(timestamp);
238
+ }
239
+
240
+ if (blockNumber === undefined && timestampSec !== undefined) {
241
+ if (!rpcUrl) {
242
+ throw new Error('--timestamp requires --rpc (or RPC_URL env) to resolve block number');
243
+ }
244
+ blockNumber = await findBlockNumberForTimestamp({
245
+ rpcUrl,
246
+ targetTimestampSec: timestampSec,
247
+ debug,
248
+ client,
249
+ });
250
+ blockSource = '--timestamp';
251
+ }
252
+
253
+ const resolvedBlockTag =
254
+ blockTag ?? (blockNumber !== undefined ? blockNumber : 'latest');
255
+ if (debug && blockNumber !== undefined) {
256
+ const tsInfo = timestampSec !== undefined ? ` timestamp=${timestampSec}` : '';
257
+ console.error(
258
+ `[api] using blockTag=${resolvedBlockTag} (${blockSource || 'implicit'})${tsInfo}`
259
+ );
260
+ }
261
+
262
+ return {
263
+ blockNumber,
264
+ blockSource,
265
+ timestampSec,
266
+ blockTag: resolvedBlockTag,
267
+ };
268
+ }
269
+
270
+ function normalizeUsdSizes(input) {
271
+ if (Array.isArray(input)) {
272
+ return input
273
+ .map((value) => Number(value))
274
+ .filter((value) => Number.isFinite(value) && value > 0)
275
+ .sort((a, b) => a - b);
276
+ }
277
+ return String(input ?? '')
278
+ .split(',')
279
+ .map((value) => Number(value.trim()))
280
+ .filter((value) => Number.isFinite(value) && value > 0)
281
+ .sort((a, b) => a - b);
282
+ }
283
+
284
+ function usdToRaw(usd, usdPrice, decimals) {
285
+ if (!usdPrice || usdPrice <= 0) return 0n;
286
+ const micros = Math.round(usd * 1e6);
287
+ const usdMicrosPrice = Math.round(usdPrice * 1e6);
288
+ if (usdMicrosPrice <= 0) return 0n;
289
+ const scale = 10n ** BigInt(decimals);
290
+ return (BigInt(micros) * scale) / BigInt(usdMicrosPrice);
291
+ }
292
+
293
+ async function resolveChainId({ client, rpcUrl, blockTag }) {
294
+ try {
295
+ if (client && typeof client.getChainId === 'function') {
296
+ const chainId = await client.getChainId();
297
+ if (rpcUrl) chainIdByRpcUrl.set(rpcUrl, chainId);
298
+ return chainId;
299
+ }
300
+ } catch (_) {}
301
+ try {
302
+ if (!rpcUrl) return undefined;
303
+ if (chainIdByRpcUrl.has(rpcUrl)) return chainIdByRpcUrl.get(rpcUrl);
304
+ const tempClient = getCachedPublicClient(rpcUrl);
305
+ const chainId = await tempClient.getChainId();
306
+ chainIdByRpcUrl.set(rpcUrl, chainId);
307
+ return chainId;
308
+ } catch (_) {
309
+ return undefined;
310
+ }
311
+ }
312
+
313
+ async function computeSnapshotMetrics({
314
+ options,
315
+ snap,
316
+ percentBuckets,
317
+ client,
318
+ blockTag,
319
+ }) {
320
+ const usd0 = Number.isFinite(options.token0USD)
321
+ ? options.token0USD
322
+ : snap.token0.usdPrice;
323
+ const usd1 = Number.isFinite(options.token1USD)
324
+ ? options.token1USD
325
+ : snap.token1.usdPrice;
326
+
327
+ let token0USD = usd0;
328
+ let token1USD = usd1;
329
+ const dec0 = snap.token0.decimals;
330
+ const dec1 = snap.token1.decimals;
331
+ const price1Per0 =
332
+ Math.pow(1.0001, snap.tick) * Math.pow(10, dec0 - dec1);
333
+
334
+ if (!usd0 && usd1) token0USD = usd1 * price1Per0;
335
+ if (usd0 && !usd1) token1USD = usd0 / price1Per0;
336
+
337
+ let priceSource0 = usd0 ? 'override' : undefined;
338
+ let priceSource1 = usd1 ? 'override' : undefined;
339
+
340
+ if (!token0USD || !token1USD) {
341
+ try {
342
+ const { fetchTokenUSDPrices } = await import('./prices.js');
343
+ const addresses = [
344
+ snap.meta?.token0?.id || snap.token0.id,
345
+ snap.meta?.token1?.id || snap.token1.id,
346
+ ].map((address) => address.toLowerCase());
347
+ const chainId = await resolveChainId({
348
+ client,
349
+ rpcUrl: options.rpcUrl,
350
+ blockTag,
351
+ });
352
+ if (options.debug) {
353
+ console.error(
354
+ `[api] price fetch: addrs=${addresses.join(',')} source=${options.prices} rpc=${options.rpcUrl ? 'yes' : 'no'} chainId=${chainId ?? 'n/a'}`
355
+ );
356
+ }
357
+ const { byAddress } = await fetchTokenUSDPrices({
358
+ addresses,
359
+ source: options.prices,
360
+ chainId,
361
+ rpcUrl: options.rpcUrl,
362
+ debug: !!options.debug,
363
+ reserveLimit: options.reserveLimit,
364
+ });
365
+ const p0 = byAddress[addresses[0]];
366
+ const p1 = byAddress[addresses[1]];
367
+ if (!token0USD && p0) {
368
+ token0USD = p0.price;
369
+ priceSource0 = p0.source;
370
+ }
371
+ if (!token1USD && p1) {
372
+ token1USD = p1.price;
373
+ priceSource1 = p1.source;
374
+ }
375
+ if (token0USD && !token1USD) {
376
+ token1USD = token0USD / price1Per0;
377
+ if (!priceSource1) priceSource1 = 'derived';
378
+ }
379
+ if (!token0USD && token1USD) {
380
+ token0USD = token1USD * price1Per0;
381
+ if (!priceSource0) priceSource0 = 'derived';
382
+ }
383
+ } catch (_) {}
384
+ }
385
+
386
+ if (!priceSource0 && usd0) priceSource0 = 'subgraph';
387
+ if (!priceSource1 && usd1) priceSource1 = 'subgraph';
388
+
389
+ if (!token0USD || !token1USD) {
390
+ if (
391
+ options.assumeStable === 0 ||
392
+ /USD|USDC|USDT|DAI/i.test(snap.meta?.token0?.symbol || '')
393
+ ) {
394
+ if (!token0USD) {
395
+ token0USD = 1;
396
+ priceSource0 = 'inferred';
397
+ }
398
+ if (!token1USD) {
399
+ token1USD = 1 / price1Per0;
400
+ priceSource1 = 'inferred';
401
+ }
402
+ } else if (
403
+ options.assumeStable === 1 ||
404
+ /USD|USDC|USDT|DAI/i.test(snap.meta?.token1?.symbol || '')
405
+ ) {
406
+ if (!token1USD) {
407
+ token1USD = 1;
408
+ priceSource1 = 'inferred';
409
+ }
410
+ if (!token0USD) {
411
+ token0USD = price1Per0;
412
+ priceSource0 = 'inferred';
413
+ }
414
+ }
415
+ }
416
+
417
+ if (options.debug) {
418
+ console.error(
419
+ `[api] usd results: token0=${token0USD ?? 'n/a'} src0=${priceSource0 ?? 'n/a'} token1=${token1USD ?? 'n/a'} src1=${priceSource1 ?? 'n/a'}`
420
+ );
421
+ }
422
+
423
+ if (!priceSource0 && token0USD) priceSource0 = 'inferred';
424
+ if (!priceSource1 && token1USD) priceSource1 = 'inferred';
425
+
426
+ const result = computeMarketDepthBands({
427
+ sqrtPriceX96: snap.sqrtPriceX96,
428
+ tick: snap.tick,
429
+ liquidity: snap.liquidity,
430
+ feePips: snap.feePips,
431
+ ticks: snap.ticks,
432
+ token0: { decimals: snap.token0.decimals, usdPrice: token0USD || 0 },
433
+ token1: { decimals: snap.token1.decimals, usdPrice: token1USD || 0 },
434
+ percentBuckets,
435
+ });
436
+
437
+ const usdSizes = normalizeUsdSizes(options.usdSizes);
438
+ let priceImpacts = null;
439
+ if (usdSizes.length && token0USD && token1USD) {
440
+ const buySizesToken1 = usdSizes.map((usd) =>
441
+ usdToRaw(usd, token1USD, snap.token1.decimals)
442
+ );
443
+ const sellSizesToken0 = usdSizes.map((usd) =>
444
+ usdToRaw(usd, token0USD, snap.token0.decimals)
445
+ );
446
+ priceImpacts = computePriceImpactsBySizes({
447
+ sqrtPriceX96: snap.sqrtPriceX96,
448
+ tick: snap.tick,
449
+ liquidity: snap.liquidity,
450
+ feePips: snap.feePips,
451
+ ticks: snap.ticks,
452
+ buySizesToken1,
453
+ sellSizesToken0,
454
+ });
455
+ }
456
+
457
+ return {
458
+ token0USD,
459
+ token1USD,
460
+ priceSource0,
461
+ priceSource1,
462
+ result,
463
+ usdSizes,
464
+ priceImpacts,
465
+ };
466
+ }
467
+
468
+ export async function analyzePool(input = {}) {
469
+ const options = {
470
+ poolAddress: input.poolAddress ?? input.pool,
471
+ percentBuckets:
472
+ input.percentBuckets ?? parsePercentBuckets(input.percent ?? '1,2,5,10'),
473
+ source: input.source ?? 'tevm',
474
+ rpcUrl: input.rpcUrl ?? input.rpc,
475
+ subgraphUrl: input.subgraphUrl ?? input.subgraph,
476
+ token0USD: input.token0USD,
477
+ token1USD: input.token1USD,
478
+ assumeStable: input.assumeStable,
479
+ usdSizes: input.usdSizes ?? '1000,5000,10000,25000,50000,100000',
480
+ prices: input.prices ?? 'auto',
481
+ reserveLimit: input.reserveLimit ?? 100,
482
+ block: input.block,
483
+ timestamp: input.timestamp,
484
+ debug: !!input.debug,
485
+ snapshot: input.snapshot,
486
+ client: input.client ?? null,
487
+ blockTag: input.blockTag,
488
+ };
489
+
490
+ if (!options.percentBuckets.length) {
491
+ throw new Error('No valid percent buckets parsed.');
492
+ }
493
+
494
+ const {
495
+ blockNumber,
496
+ blockSource,
497
+ timestampSec,
498
+ blockTag,
499
+ } = await resolveAnalysisBlockContext({
500
+ block: options.block,
501
+ timestamp: options.timestamp,
502
+ rpcUrl: options.rpcUrl,
503
+ debug: options.debug,
504
+ blockTag: options.blockTag,
505
+ client: options.client,
506
+ });
507
+
508
+ let client = options.client;
509
+ let usedSource = options.source;
510
+ let snap = options.snapshot;
511
+
512
+ if (!snap) {
513
+ const useTevm =
514
+ options.source === 'viem' ||
515
+ options.source === 'tevm' ||
516
+ (options.source === 'auto' && options.rpcUrl);
517
+
518
+ if (useTevm) {
519
+ if (!options.rpcUrl) {
520
+ throw new Error('Missing --rpc (or RPC_URL env) for tevm source.');
521
+ }
522
+ const { fetchPoolSnapshotViem } = await import('./viem-onchain.js');
523
+ if (!client) {
524
+ client = getCachedPublicClient(options.rpcUrl);
525
+ }
526
+ usedSource = 'tevm';
527
+ snap = await fetchPoolSnapshotViem({
528
+ poolAddress: options.poolAddress,
529
+ rpcUrl: options.rpcUrl,
530
+ percentBuckets: options.percentBuckets,
531
+ client,
532
+ blockTag,
533
+ });
534
+ } else if (options.source === 'subgraph' || options.source === 'auto') {
535
+ usedSource = 'subgraph';
536
+ snap = await fetchFromSubgraph({
537
+ poolAddress: options.poolAddress,
538
+ percentBuckets: options.percentBuckets,
539
+ subgraphUrl: options.subgraphUrl,
540
+ blockNumber,
541
+ });
542
+ } else {
543
+ throw new Error(`Unknown --source: ${options.source}`);
544
+ }
545
+ } else {
546
+ usedSource = options.source === 'auto' ? 'snapshot' : options.source;
547
+ }
548
+
549
+ const metrics = await computeSnapshotMetrics({
550
+ options,
551
+ snap,
552
+ percentBuckets: options.percentBuckets,
553
+ client,
554
+ blockTag,
555
+ });
556
+
557
+ return {
558
+ snap,
559
+ client,
560
+ usedSource,
561
+ percentBuckets: options.percentBuckets,
562
+ blockTag,
563
+ blockNumber,
564
+ blockSource,
565
+ timestampSec,
566
+ ...metrics,
567
+ };
568
+ }