@steerprotocol/liquidity-meter 2.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/README.md +9 -0
- package/bin/liquidity-depth.js +135 -227
- package/package.json +21 -2
- package/src/api.js +89 -11
- package/src/viem-onchain.js +335 -62
package/README.md
CHANGED
|
@@ -22,6 +22,15 @@ Install
|
|
|
22
22
|
- As a package dependency:
|
|
23
23
|
- `npm install @steerprotocol/liquidity-meter`
|
|
24
24
|
|
|
25
|
+
Release Workflow
|
|
26
|
+
|
|
27
|
+
- Releases are automated with `semantic-release` on pushes to `master` or `main`.
|
|
28
|
+
- Commit messages are validated with `commitlint` using the Conventional Commits spec.
|
|
29
|
+
- CI runs unit tests on Node.js 20 and 22, then enforces coverage thresholds on Node.js 22.
|
|
30
|
+
- Required repository secrets:
|
|
31
|
+
- `NPM_TOKEN` for npm publishing.
|
|
32
|
+
- GitHub releases use the default `GITHUB_TOKEN` provided by Actions.
|
|
33
|
+
|
|
25
34
|
Library Usage
|
|
26
35
|
|
|
27
36
|
```js
|
package/bin/liquidity-depth.js
CHANGED
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
|
-
const execFile = promisify(_execFile);
|
|
10
|
-
import { parseAbi, getAddress } from 'viem';
|
|
7
|
+
import { createPublicClient, parseAbi, getAddress, http as viemHttp } from 'viem';
|
|
11
8
|
import {
|
|
12
9
|
analyzePool,
|
|
10
|
+
findBlockNumbersForTimestamps,
|
|
13
11
|
formatTimestampUTC,
|
|
14
12
|
parseDurationArg,
|
|
15
13
|
parsePercentBuckets,
|
|
@@ -263,9 +261,7 @@ async function runBatch(args) {
|
|
|
263
261
|
if (!looksLikeAddress(t0 || '') || !looksLikeAddress(t1 || '') || !feeNum) return undefined;
|
|
264
262
|
if (!rpcUrl) return undefined;
|
|
265
263
|
|
|
266
|
-
const
|
|
267
|
-
const client = createMemoryClient({ fork: { transport: http(rpcUrl), blockTag: 'latest' }, loggingLevel: 'error' });
|
|
268
|
-
if (client.tevmReady) await client.tevmReady();
|
|
264
|
+
const client = createPublicClient({ transport: viemHttp(rpcUrl) });
|
|
269
265
|
try {
|
|
270
266
|
const pool = await client.readContract({ address: UNISWAP_V3_FACTORY, abi: FACTORY_ABI, functionName: 'getPool', args: [getAddress(t0), getAddress(t1), feeNum] });
|
|
271
267
|
if (pool && pool !== '0x0000000000000000000000000000000000000000') return pool;
|
|
@@ -275,27 +271,6 @@ async function runBatch(args) {
|
|
|
275
271
|
return undefined;
|
|
276
272
|
}
|
|
277
273
|
|
|
278
|
-
// base args to pass through from CLI
|
|
279
|
-
const base = [];
|
|
280
|
-
if (args.percent) base.push('--percent', String(args.percent));
|
|
281
|
-
if (args.source) base.push('--source', String(args.source));
|
|
282
|
-
if (args.rpc) base.push('--rpc', String(args.rpc));
|
|
283
|
-
if (args.subgraph) base.push('--subgraph', String(args.subgraph));
|
|
284
|
-
if (Number.isFinite(args.token0USD)) base.push('--token0-usd', String(args.token0USD));
|
|
285
|
-
if (Number.isFinite(args.token1USD)) base.push('--token1-usd', String(args.token1USD));
|
|
286
|
-
if (Number.isFinite(args.assumeStable)) base.push('--assume-stable', String(args.assumeStable));
|
|
287
|
-
if (args.usdSizes) base.push('--usd-sizes', String(args.usdSizes));
|
|
288
|
-
if (args.prices) base.push('--prices', String(args.prices));
|
|
289
|
-
if (args.reserveLimit) base.push('--reserve-limit', String(args.reserveLimit));
|
|
290
|
-
if (args.json) base.push('--json');
|
|
291
|
-
if (args.debug) base.push('--debug');
|
|
292
|
-
if (args.block) base.push('--block', String(args.block));
|
|
293
|
-
if (args.timestamp) base.push('--timestamp', String(args.timestamp));
|
|
294
|
-
if (args.owner) base.push('--owner', String(args.owner));
|
|
295
|
-
if (Number.isFinite(args.withdrawPct)) base.push('--withdraw-pct', String(args.withdrawPct));
|
|
296
|
-
if (args.withdrawShares) base.push('--withdraw-shares', String(args.withdrawShares));
|
|
297
|
-
|
|
298
|
-
const scriptPath = process.argv[1];
|
|
299
274
|
for (let idx = 0; idx < rows.length; idx++) {
|
|
300
275
|
const row = rows[idx];
|
|
301
276
|
let pool = pick(row, ['pool', 'address', 'pool_address', 'id', 'Pool', 'Address', 'contract_address', 'contract address', 'pair_address', 'pair address']);
|
|
@@ -316,36 +291,29 @@ async function runBatch(args) {
|
|
|
316
291
|
continue;
|
|
317
292
|
}
|
|
318
293
|
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
if (vault) per.push('--vault', vault);
|
|
343
|
-
const owner = pick(row, ['owner', 'Owner', 'owner_address', 'owner address', 'withdraw_owner', 'withdraw owner', 'account', 'user', 'recipient', 'to']);
|
|
344
|
-
if (owner) per.push('--owner', owner);
|
|
345
|
-
const wpct = pick(row, ['withdraw_pct', 'withdraw-pct', 'withdraw percent', 'withdraw_percent', 'withdrawPercentage', 'withdrawPercent', 'pct_withdraw', 'percent_withdraw']);
|
|
346
|
-
if (wpct) per.push('--withdraw-pct', String(wpct));
|
|
347
|
-
const wshares = pick(row, ['withdraw_shares', 'withdraw-shares', 'withdraw shares', 'withdrawShares', 'shares_to_withdraw', 'withdraw_share_amount']);
|
|
348
|
-
if (wshares) per.push('--withdraw-shares', String(wshares));
|
|
294
|
+
const rowArgs = {
|
|
295
|
+
...args,
|
|
296
|
+
pool,
|
|
297
|
+
rpc: pick(row, ['rpc', 'RPC', 'rpc_url', 'RPC_URL']) || args.rpc,
|
|
298
|
+
source: pick(row, ['source', 'Source']) || args.source,
|
|
299
|
+
percent: pick(row, ['percent', 'Percents', 'buckets']) || args.percent,
|
|
300
|
+
block: pick(row, ['block', 'Block', 'block_number', 'block number']) || args.block,
|
|
301
|
+
timestamp: pick(row, ['timestamp', 'Timestamp', 'time', 'Time']) || args.timestamp,
|
|
302
|
+
token0USD: pick(row, ['token0_usd', 'token0-usd', 'token0USD']) != null ? Number(pick(row, ['token0_usd', 'token0-usd', 'token0USD'])) : args.token0USD,
|
|
303
|
+
token1USD: pick(row, ['token1_usd', 'token1-usd', 'token1USD']) != null ? Number(pick(row, ['token1_usd', 'token1-usd', 'token1USD'])) : args.token1USD,
|
|
304
|
+
usdSizes: pick(row, ['usd_sizes', 'usd-sizes', 'usdSizes']) || args.usdSizes,
|
|
305
|
+
assumeStable: pick(row, ['assume_stable', 'assume-stable', 'assumeStable']) != null ? Number(pick(row, ['assume_stable', 'assume-stable', 'assumeStable'])) : args.assumeStable,
|
|
306
|
+
prices: pick(row, ['prices', 'Prices']) || args.prices,
|
|
307
|
+
vault: pick(row, ['vault', 'Vault', 'vault_address', 'VaultAddress', 'vault address', 'strategy']) || args.vault,
|
|
308
|
+
owner: pick(row, ['owner', 'Owner', 'owner_address', 'owner address', 'withdraw_owner', 'withdraw owner', 'account', 'user', 'recipient', 'to']) || args.owner,
|
|
309
|
+
withdrawPct: pick(row, ['withdraw_pct', 'withdraw-pct', 'withdraw percent', 'withdraw_percent', 'withdrawPercentage', 'withdrawPercent', 'pct_withdraw', 'percent_withdraw']) != null ? Number(pick(row, ['withdraw_pct', 'withdraw-pct', 'withdraw percent', 'withdraw_percent', 'withdrawPercentage', 'withdrawPercent', 'pct_withdraw', 'percent_withdraw'])) : args.withdrawPct,
|
|
310
|
+
withdrawShares: pick(row, ['withdraw_shares', 'withdraw-shares', 'withdraw shares', 'withdrawShares', 'shares_to_withdraw', 'withdraw_share_amount']) || args.withdrawShares,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const wpct = rowArgs.withdrawPct;
|
|
314
|
+
const wshares = rowArgs.withdrawShares;
|
|
315
|
+
const vault = rowArgs.vault;
|
|
316
|
+
const owner = rowArgs.owner;
|
|
349
317
|
|
|
350
318
|
if (args.debug) {
|
|
351
319
|
const dbg = [];
|
|
@@ -356,20 +324,14 @@ async function runBatch(args) {
|
|
|
356
324
|
if (dbg.length) console.error(`[batch] row ${idx + 1} overrides: ${dbg.join(' ')}`);
|
|
357
325
|
}
|
|
358
326
|
|
|
359
|
-
const argsArr = [scriptPath, '--pool', pool, ...per];
|
|
360
327
|
try {
|
|
361
|
-
const { stdout, stderr } = await execFile(process.execPath, argsArr, { env: process.env, maxBuffer: 5 * 1024 * 1024 });
|
|
362
328
|
const baseName = `${pool.toLowerCase()}`;
|
|
363
329
|
const file = path.join(outdir, `${baseName}.${args.json ? 'json' : 'txt'}`);
|
|
364
|
-
const
|
|
365
|
-
await fs.writeFile(file,
|
|
366
|
-
if (args.debug && stderr) {
|
|
367
|
-
// Forward child's debug logs to parent stderr
|
|
368
|
-
process.stderr.write(stderr);
|
|
369
|
-
}
|
|
330
|
+
const output = await runSingleInvocation(rowArgs);
|
|
331
|
+
await fs.writeFile(file, output, 'utf8');
|
|
370
332
|
console.log(`Wrote: ${file}`);
|
|
371
333
|
} catch (err) {
|
|
372
|
-
const errMsg = String(err?.
|
|
334
|
+
const errMsg = String(err?.stack || err?.message || err);
|
|
373
335
|
const errFile = path.join(outdir, `error-${pool.toLowerCase()}.txt`);
|
|
374
336
|
await fs.writeFile(errFile, errMsg, 'utf8');
|
|
375
337
|
console.error(`Failed row ${idx + 1} (${pool}): ${err?.message || err}`);
|
|
@@ -378,41 +340,6 @@ async function runBatch(args) {
|
|
|
378
340
|
}
|
|
379
341
|
}
|
|
380
342
|
|
|
381
|
-
function parseChildJsonOutput(raw) {
|
|
382
|
-
// First, strip known noisy lines from stdout (e.g., tevm logs, hex chainId)
|
|
383
|
-
const cleaned = cleanReportOutput(String(raw ?? ''));
|
|
384
|
-
const trimmed = cleaned.trim();
|
|
385
|
-
if (!trimmed) return null;
|
|
386
|
-
try {
|
|
387
|
-
return JSON.parse(trimmed);
|
|
388
|
-
} catch (err) {
|
|
389
|
-
// Try to recover by locating a likely JSON block. Prefer the last complete
|
|
390
|
-
// object/array, since noise often appears before the payload.
|
|
391
|
-
const lastObj = trimmed.lastIndexOf('{');
|
|
392
|
-
const lastArr = trimmed.lastIndexOf('[');
|
|
393
|
-
const start = Math.max(lastObj, lastArr);
|
|
394
|
-
if (start >= 0) {
|
|
395
|
-
const endChar = trimmed[start] === '[' ? ']' : '}';
|
|
396
|
-
const end = trimmed.lastIndexOf(endChar);
|
|
397
|
-
if (end > start) {
|
|
398
|
-
const slice = trimmed.slice(start, end + 1).trim();
|
|
399
|
-
if (slice) {
|
|
400
|
-
try {
|
|
401
|
-
return JSON.parse(slice);
|
|
402
|
-
} catch (_) {
|
|
403
|
-
// fall through to error reporting below
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
const snippet = trimmed.length > 240 ? `${trimmed.slice(0, 240)}…` : trimmed;
|
|
409
|
-
const message = `${err?.message || err}. Child output: ${snippet}`;
|
|
410
|
-
const outErr = err instanceof Error ? err : new Error(String(message));
|
|
411
|
-
if (outErr instanceof Error) outErr.message = message;
|
|
412
|
-
throw outErr;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
343
|
async function runTimeSeries(args) {
|
|
417
344
|
if (!args.pool) {
|
|
418
345
|
console.error('Time-series mode requires --pool');
|
|
@@ -511,6 +438,11 @@ async function runTimeSeries(args) {
|
|
|
511
438
|
console.error('No timestamps resolved for time-series mode');
|
|
512
439
|
process.exit(1);
|
|
513
440
|
}
|
|
441
|
+
const resolvedBlocks = await findBlockNumbersForTimestamps({
|
|
442
|
+
rpcUrl: args.rpc,
|
|
443
|
+
timestamps,
|
|
444
|
+
debug: !!args.debug,
|
|
445
|
+
});
|
|
514
446
|
|
|
515
447
|
if (!args.json) {
|
|
516
448
|
const startIso = formatTimestampUTC(BigInt(tsStrings[0]));
|
|
@@ -519,60 +451,33 @@ async function runTimeSeries(args) {
|
|
|
519
451
|
console.log(`Time-series: ${tsStrings.length} snapshots from ${startIso || tsStrings[0]} to ${endIso || tsStrings[tsStrings.length - 1]} every ~${intervalHuman}`);
|
|
520
452
|
}
|
|
521
453
|
|
|
522
|
-
const base = [];
|
|
523
|
-
if (args.percent) base.push('--percent', String(args.percent));
|
|
524
|
-
if (args.source) base.push('--source', String(args.source));
|
|
525
|
-
if (args.rpc) base.push('--rpc', String(args.rpc));
|
|
526
|
-
if (args.subgraph) base.push('--subgraph', String(args.subgraph));
|
|
527
|
-
if (Number.isFinite(args.token0USD)) base.push('--token0-usd', String(args.token0USD));
|
|
528
|
-
if (Number.isFinite(args.token1USD)) base.push('--token1-usd', String(args.token1USD));
|
|
529
|
-
if (Number.isFinite(args.assumeStable)) base.push('--assume-stable', String(args.assumeStable));
|
|
530
|
-
if (args.usdSizes) base.push('--usd-sizes', String(args.usdSizes));
|
|
531
|
-
if (args.prices) base.push('--prices', String(args.prices));
|
|
532
|
-
if (args.reserveLimit) base.push('--reserve-limit', String(args.reserveLimit));
|
|
533
|
-
if (args.json) base.push('--json');
|
|
534
|
-
if (args.debug) base.push('--debug');
|
|
535
|
-
if (args.owner) base.push('--owner', String(args.owner));
|
|
536
|
-
if (Number.isFinite(args.withdrawPct)) base.push('--withdraw-pct', String(args.withdrawPct));
|
|
537
|
-
if (args.withdrawShares) base.push('--withdraw-shares', String(args.withdrawShares));
|
|
538
|
-
if (args.vault) base.push('--vault', String(args.vault));
|
|
539
|
-
if (args.outdir) base.push('--outdir', String(args.outdir));
|
|
540
|
-
|
|
541
|
-
const scriptPath = process.argv[1];
|
|
542
454
|
const jsonResults = [];
|
|
543
455
|
for (let i = 0; i < tsStrings.length; i++) {
|
|
544
456
|
const tsStr = tsStrings[i];
|
|
545
|
-
const
|
|
457
|
+
const pointArgs = {
|
|
458
|
+
...args,
|
|
459
|
+
timestamp: undefined,
|
|
460
|
+
block: resolvedBlocks[i].toString(),
|
|
461
|
+
};
|
|
546
462
|
const label = formatTimestampUTC(BigInt(tsStr)) || tsStr;
|
|
547
463
|
if (!args.json) {
|
|
548
464
|
console.log(`\n=== ${label} (unix ${tsStr}) ===`);
|
|
549
465
|
}
|
|
550
466
|
try {
|
|
551
|
-
const
|
|
467
|
+
const output = await runSingleInvocation(pointArgs);
|
|
552
468
|
if (args.json) {
|
|
553
|
-
const text =
|
|
469
|
+
const text = output.trim();
|
|
554
470
|
if (text) {
|
|
555
|
-
|
|
556
|
-
jsonResults.push(parseChildJsonOutput(text));
|
|
557
|
-
} catch (err) {
|
|
558
|
-
if (args.debug && stderr) process.stderr.write(stderr);
|
|
559
|
-
console.error(`Failed to parse JSON for timestamp ${tsStr}: ${err?.message || err}`);
|
|
560
|
-
console.error('Raw child stdout:');
|
|
561
|
-
console.error(text);
|
|
562
|
-
process.exit(1);
|
|
563
|
-
}
|
|
471
|
+
jsonResults.push(JSON.parse(text));
|
|
564
472
|
} else {
|
|
565
473
|
jsonResults.push(null);
|
|
566
474
|
}
|
|
567
475
|
} else {
|
|
568
|
-
process.stdout.write(
|
|
569
|
-
if (!
|
|
476
|
+
process.stdout.write(output);
|
|
477
|
+
if (!output.endsWith('\n')) console.log('');
|
|
570
478
|
}
|
|
571
|
-
if (args.debug && stderr) process.stderr.write(stderr);
|
|
572
479
|
} catch (err) {
|
|
573
480
|
console.error(`Time-series run failed for timestamp ${tsStr}: ${err?.message || err}`);
|
|
574
|
-
const errText = err?.stderr || err?.stdout;
|
|
575
|
-
if (errText) console.error(String(errText).trim());
|
|
576
481
|
process.exit(1);
|
|
577
482
|
}
|
|
578
483
|
}
|
|
@@ -582,66 +487,29 @@ async function runTimeSeries(args) {
|
|
|
582
487
|
}
|
|
583
488
|
}
|
|
584
489
|
|
|
585
|
-
|
|
586
|
-
|
|
490
|
+
function captureConsoleOutput(run) {
|
|
491
|
+
const lines = [];
|
|
492
|
+
const original = console.log;
|
|
493
|
+
console.log = (...args) => {
|
|
494
|
+
lines.push(args.join(' '));
|
|
495
|
+
};
|
|
587
496
|
try {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (!line) { out.push(line); continue; }
|
|
592
|
-
// Filter known noisy patterns
|
|
593
|
-
if (line.includes('"name":"@tevm/')) continue; // TEVM structured logs
|
|
594
|
-
if (/\bEIP-7702\b/i.test(line)) continue; // EIP-7702 warning block
|
|
595
|
-
if (/^\s*\{\s*chainId\s*:/i.test(line)) continue; // stray chainId object
|
|
596
|
-
if (/^\s*0x[0-9a-fA-F]+\s*$/.test(line)) continue; // bare hex line
|
|
597
|
-
out.push(line);
|
|
598
|
-
}
|
|
599
|
-
// Trim leading empty lines left by filtering
|
|
600
|
-
while (out.length && out[0].trim() === '') out.shift();
|
|
601
|
-
return out.join('\n');
|
|
602
|
-
} catch (_) {
|
|
603
|
-
return s;
|
|
497
|
+
run();
|
|
498
|
+
} finally {
|
|
499
|
+
console.log = original;
|
|
604
500
|
}
|
|
501
|
+
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
605
502
|
}
|
|
606
503
|
|
|
607
|
-
async function
|
|
608
|
-
const args = parseArgs(process.argv);
|
|
609
|
-
if (args.version) {
|
|
610
|
-
console.log('liquidity-depth 0.1.0');
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
if (args.help || (!args.pool && !args.csv)) {
|
|
614
|
-
printHelp();
|
|
615
|
-
if (!args.pool && !args.csv) process.exitCode = 1;
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (args.csv) {
|
|
620
|
-
await runBatch(args);
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const wantsTimeSeries =
|
|
625
|
-
args.timeStart != null ||
|
|
626
|
-
args.timeEnd != null ||
|
|
627
|
-
args.timeInterval != null ||
|
|
628
|
-
args.timeWindow != null ||
|
|
629
|
-
args.timeCount != null;
|
|
630
|
-
if (wantsTimeSeries) {
|
|
631
|
-
await runTimeSeries(args);
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
|
|
504
|
+
async function executeSingle(args) {
|
|
635
505
|
let percentBuckets;
|
|
636
506
|
try {
|
|
637
507
|
percentBuckets = parsePercentBuckets(args.percent);
|
|
638
508
|
} catch (e) {
|
|
639
|
-
|
|
640
|
-
process.exit(1);
|
|
509
|
+
throw new Error(`Invalid --percent value: ${e?.message || e}`);
|
|
641
510
|
}
|
|
642
511
|
if (!percentBuckets.length) {
|
|
643
|
-
|
|
644
|
-
process.exit(1);
|
|
512
|
+
throw new Error('No valid percent buckets parsed.');
|
|
645
513
|
}
|
|
646
514
|
|
|
647
515
|
const wantsWithdraw =
|
|
@@ -650,8 +518,7 @@ async function main() {
|
|
|
650
518
|
let sharedBlockTag;
|
|
651
519
|
if (wantsWithdraw) {
|
|
652
520
|
if (!args.rpc) {
|
|
653
|
-
|
|
654
|
-
process.exit(1);
|
|
521
|
+
throw new Error('Missing --rpc (or RPC_URL env) for tevm source.');
|
|
655
522
|
}
|
|
656
523
|
const blockContext = await resolveAnalysisBlockContext({
|
|
657
524
|
block: args.block,
|
|
@@ -667,36 +534,24 @@ async function main() {
|
|
|
667
534
|
}
|
|
668
535
|
|
|
669
536
|
let before;
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
});
|
|
689
|
-
} catch (e) {
|
|
690
|
-
if (
|
|
691
|
-
args.timestamp != null &&
|
|
692
|
-
args.rpc == null &&
|
|
693
|
-
String(e?.message || '').includes('--timestamp requires --rpc')
|
|
694
|
-
) {
|
|
695
|
-
console.error('--timestamp requires --rpc (or RPC_URL env) to resolve block number');
|
|
696
|
-
process.exit(1);
|
|
697
|
-
}
|
|
698
|
-
throw e;
|
|
699
|
-
}
|
|
537
|
+
before = await analyzePool({
|
|
538
|
+
poolAddress: args.pool,
|
|
539
|
+
percentBuckets,
|
|
540
|
+
source: args.source,
|
|
541
|
+
rpcUrl: args.rpc,
|
|
542
|
+
subgraphUrl: args.subgraph,
|
|
543
|
+
token0USD: args.token0USD,
|
|
544
|
+
token1USD: args.token1USD,
|
|
545
|
+
assumeStable: args.assumeStable,
|
|
546
|
+
usdSizes: args.usdSizes,
|
|
547
|
+
prices: args.prices,
|
|
548
|
+
reserveLimit: args.reserveLimit,
|
|
549
|
+
block: args.block,
|
|
550
|
+
timestamp: args.timestamp,
|
|
551
|
+
debug: args.debug,
|
|
552
|
+
client: sharedClient,
|
|
553
|
+
blockTag: sharedBlockTag,
|
|
554
|
+
});
|
|
700
555
|
|
|
701
556
|
let after = null;
|
|
702
557
|
if (before.usedSource === 'tevm' && wantsWithdraw) {
|
|
@@ -734,6 +589,10 @@ async function main() {
|
|
|
734
589
|
}
|
|
735
590
|
}
|
|
736
591
|
|
|
592
|
+
return { before, after };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function renderSingle(args, before, after) {
|
|
737
596
|
if (args.json) {
|
|
738
597
|
const output = after
|
|
739
598
|
? {
|
|
@@ -741,15 +600,64 @@ async function main() {
|
|
|
741
600
|
after: serializeAnalysis(args, after),
|
|
742
601
|
}
|
|
743
602
|
: serializeAnalysis(args, before);
|
|
744
|
-
|
|
603
|
+
return `${JSON.stringify(output, null, 2)}\n`;
|
|
604
|
+
}
|
|
605
|
+
return captureConsoleOutput(() => {
|
|
606
|
+
printAnalysisSummary(args, before);
|
|
607
|
+
if (after) {
|
|
608
|
+
console.log('');
|
|
609
|
+
console.log('--- After Withdraw ---');
|
|
610
|
+
printAnalysisSummary(args, after);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function runSingleInvocation(args) {
|
|
616
|
+
const { before, after } = await executeSingle(args);
|
|
617
|
+
return renderSingle(args, before, after);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function main() {
|
|
621
|
+
const args = parseArgs(process.argv);
|
|
622
|
+
if (args.version) {
|
|
623
|
+
console.log('liquidity-depth 0.1.0');
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (args.help || (!args.pool && !args.csv)) {
|
|
627
|
+
printHelp();
|
|
628
|
+
if (!args.pool && !args.csv) process.exitCode = 1;
|
|
745
629
|
return;
|
|
746
630
|
}
|
|
747
631
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
632
|
+
if (args.csv) {
|
|
633
|
+
await runBatch(args);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const wantsTimeSeries =
|
|
638
|
+
args.timeStart != null ||
|
|
639
|
+
args.timeEnd != null ||
|
|
640
|
+
args.timeInterval != null ||
|
|
641
|
+
args.timeWindow != null ||
|
|
642
|
+
args.timeCount != null;
|
|
643
|
+
if (wantsTimeSeries) {
|
|
644
|
+
await runTimeSeries(args);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
const output = await runSingleInvocation(args);
|
|
650
|
+
process.stdout.write(output);
|
|
651
|
+
} catch (e) {
|
|
652
|
+
if (
|
|
653
|
+
args.timestamp != null &&
|
|
654
|
+
args.rpc == null &&
|
|
655
|
+
String(e?.message || '').includes('--timestamp requires --rpc')
|
|
656
|
+
) {
|
|
657
|
+
console.error('--timestamp requires --rpc (or RPC_URL env) to resolve block number');
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
throw e;
|
|
753
661
|
}
|
|
754
662
|
}
|
|
755
663
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerprotocol/liquidity-meter",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
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",
|
|
7
11
|
"main": "./src/index.js",
|
|
8
12
|
"exports": {
|
|
@@ -17,6 +21,9 @@
|
|
|
17
21
|
"README.md",
|
|
18
22
|
"templates"
|
|
19
23
|
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
20
27
|
"bin": {
|
|
21
28
|
"liquidity-depth": "bin/liquidity-depth.js"
|
|
22
29
|
},
|
|
@@ -25,7 +32,19 @@
|
|
|
25
32
|
},
|
|
26
33
|
"scripts": {
|
|
27
34
|
"start": "node bin/liquidity-depth.js --help",
|
|
28
|
-
"test": "node --test"
|
|
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"
|
|
29
48
|
},
|
|
30
49
|
"dependencies": {
|
|
31
50
|
"axios": "1.7.7",
|
package/src/api.js
CHANGED
|
@@ -5,6 +5,19 @@ import {
|
|
|
5
5
|
} from './depth.js';
|
|
6
6
|
import { fetchPoolSnapshot as fetchFromSubgraph } from './uniswapv3-subgraph.js';
|
|
7
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
|
+
|
|
8
21
|
export function parsePercentBuckets(input = '1,2,5,10') {
|
|
9
22
|
if (Array.isArray(input)) {
|
|
10
23
|
return input
|
|
@@ -101,28 +114,44 @@ export async function findBlockNumberForTimestamp({
|
|
|
101
114
|
rpcUrl,
|
|
102
115
|
targetTimestampSec,
|
|
103
116
|
debug = false,
|
|
117
|
+
client,
|
|
118
|
+
bounds,
|
|
119
|
+
blockCache,
|
|
104
120
|
}) {
|
|
105
121
|
if (!rpcUrl) throw new Error('RPC URL required to resolve timestamp');
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
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);
|
|
109
133
|
if (!latestBlock || latestBlock.timestamp == null) {
|
|
110
134
|
throw new Error('Latest block did not include a timestamp');
|
|
111
135
|
}
|
|
112
136
|
const latestTs = latestBlock.timestamp;
|
|
113
|
-
const genesis = await
|
|
137
|
+
const genesis = bounds?.genesisBlock ?? await readBlock(0n);
|
|
114
138
|
if (!genesis || genesis.timestamp == null) {
|
|
115
139
|
throw new Error('Genesis block did not include a timestamp');
|
|
116
140
|
}
|
|
117
141
|
if (targetTimestampSec <= genesis.timestamp) return 0n;
|
|
118
142
|
if (targetTimestampSec >= latestTs) return latestBlock.number;
|
|
119
143
|
|
|
120
|
-
let lo = 0n;
|
|
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
|
+
}
|
|
121
150
|
let hi = latestBlock.number;
|
|
122
|
-
let best = genesis.number ?? 0n;
|
|
151
|
+
let best = loTimestamp <= targetTimestampSec ? lo : (genesis.number ?? 0n);
|
|
123
152
|
for (let iter = 0; iter < 80 && lo <= hi; iter++) {
|
|
124
153
|
const mid = (lo + hi) >> 1n;
|
|
125
|
-
const block = await
|
|
154
|
+
const block = await readBlock(mid);
|
|
126
155
|
if (!block || block.timestamp == null) {
|
|
127
156
|
hi = mid - 1n;
|
|
128
157
|
continue;
|
|
@@ -144,12 +173,54 @@ export async function findBlockNumberForTimestamp({
|
|
|
144
173
|
return best;
|
|
145
174
|
}
|
|
146
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
|
+
|
|
147
217
|
export async function resolveAnalysisBlockContext({
|
|
148
218
|
block,
|
|
149
219
|
timestamp,
|
|
150
220
|
rpcUrl,
|
|
151
221
|
debug = false,
|
|
152
222
|
blockTag,
|
|
223
|
+
client,
|
|
153
224
|
}) {
|
|
154
225
|
let blockNumber;
|
|
155
226
|
let blockSource;
|
|
@@ -174,6 +245,7 @@ export async function resolveAnalysisBlockContext({
|
|
|
174
245
|
rpcUrl,
|
|
175
246
|
targetTimestampSec: timestampSec,
|
|
176
247
|
debug,
|
|
248
|
+
client,
|
|
177
249
|
});
|
|
178
250
|
blockSource = '--timestamp';
|
|
179
251
|
}
|
|
@@ -221,13 +293,18 @@ function usdToRaw(usd, usdPrice, decimals) {
|
|
|
221
293
|
async function resolveChainId({ client, rpcUrl, blockTag }) {
|
|
222
294
|
try {
|
|
223
295
|
if (client && typeof client.getChainId === 'function') {
|
|
224
|
-
|
|
296
|
+
const chainId = await client.getChainId();
|
|
297
|
+
if (rpcUrl) chainIdByRpcUrl.set(rpcUrl, chainId);
|
|
298
|
+
return chainId;
|
|
225
299
|
}
|
|
226
300
|
} catch (_) {}
|
|
227
301
|
try {
|
|
228
302
|
if (!rpcUrl) return undefined;
|
|
229
|
-
|
|
230
|
-
|
|
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;
|
|
231
308
|
} catch (_) {
|
|
232
309
|
return undefined;
|
|
233
310
|
}
|
|
@@ -425,6 +502,7 @@ export async function analyzePool(input = {}) {
|
|
|
425
502
|
rpcUrl: options.rpcUrl,
|
|
426
503
|
debug: options.debug,
|
|
427
504
|
blockTag: options.blockTag,
|
|
505
|
+
client: options.client,
|
|
428
506
|
});
|
|
429
507
|
|
|
430
508
|
let client = options.client;
|
|
@@ -443,7 +521,7 @@ export async function analyzePool(input = {}) {
|
|
|
443
521
|
}
|
|
444
522
|
const { fetchPoolSnapshotViem } = await import('./viem-onchain.js');
|
|
445
523
|
if (!client) {
|
|
446
|
-
client =
|
|
524
|
+
client = getCachedPublicClient(options.rpcUrl);
|
|
447
525
|
}
|
|
448
526
|
usedSource = 'tevm';
|
|
449
527
|
snap = await fetchPoolSnapshotViem({
|
package/src/viem-onchain.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
// On-chain fetcher for Uniswap v3 pools using a viem public client
|
|
2
|
-
// -
|
|
3
|
-
// - Enumerates initialized ticks via tickBitmap within a bounded range
|
|
1
|
+
// On-chain fetcher for Uniswap v3 pools using a viem public client.
|
|
2
|
+
// It prefers JSON-RPC batching when an RPC URL is available to reduce HTTP round trips.
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createPublicClient,
|
|
6
|
+
decodeFunctionResult,
|
|
7
|
+
encodeFunctionData,
|
|
8
|
+
getAddress,
|
|
9
|
+
http,
|
|
10
|
+
parseAbi,
|
|
11
|
+
} from 'viem';
|
|
6
12
|
|
|
7
13
|
const UNISWAP_POOL_ABI = parseAbi([
|
|
8
14
|
'function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)',
|
|
@@ -32,6 +38,124 @@ const ERC20_ABI = parseAbi([
|
|
|
32
38
|
const GLOBAL_STATE_SELECTOR = '0xe76c01e4';
|
|
33
39
|
const TWO_256 = 1n << 256n;
|
|
34
40
|
const TWO_255 = 1n << 255n;
|
|
41
|
+
const poolStaticCache = new Map();
|
|
42
|
+
const tokenMetaCache = new Map();
|
|
43
|
+
|
|
44
|
+
function toRpcBlockTag(blockTag) {
|
|
45
|
+
if (typeof blockTag === 'bigint') return `0x${blockTag.toString(16)}`;
|
|
46
|
+
if (typeof blockTag === 'number') return `0x${blockTag.toString(16)}`;
|
|
47
|
+
return blockTag ?? 'latest';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function batchRpcRequest({ rpcUrl, requests }) {
|
|
51
|
+
if (!rpcUrl) throw new Error('rpcUrl is required for batched RPC requests');
|
|
52
|
+
const res = await fetch(rpcUrl, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'content-type': 'application/json' },
|
|
55
|
+
body: JSON.stringify(requests),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`Batched RPC request failed with HTTP ${res.status}`);
|
|
59
|
+
}
|
|
60
|
+
const body = await res.json();
|
|
61
|
+
const responses = Array.isArray(body) ? body : [body];
|
|
62
|
+
const byId = new Map(responses.map((entry) => [entry.id, entry]));
|
|
63
|
+
return requests.map((request) => {
|
|
64
|
+
const response = byId.get(request.id);
|
|
65
|
+
if (!response) {
|
|
66
|
+
throw new Error(`Missing batched RPC response for id ${request.id}`);
|
|
67
|
+
}
|
|
68
|
+
if (response.error) {
|
|
69
|
+
throw new Error(response.error.message || JSON.stringify(response.error));
|
|
70
|
+
}
|
|
71
|
+
return response.result;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readContractsBatch({ rpcUrl, blockTag, address, abi, calls }) {
|
|
76
|
+
const normalizedAddress = getAddress(address);
|
|
77
|
+
return readContractCallsBatch({
|
|
78
|
+
rpcUrl,
|
|
79
|
+
blockTag,
|
|
80
|
+
calls: calls.map((call) => ({ ...call, address: normalizedAddress, abi })),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readContractCallsBatch({ rpcUrl, blockTag, calls }) {
|
|
85
|
+
const rpcBlockTag = toRpcBlockTag(blockTag);
|
|
86
|
+
const requests = calls.map((call, index) => ({
|
|
87
|
+
jsonrpc: '2.0',
|
|
88
|
+
id: index + 1,
|
|
89
|
+
method: 'eth_call',
|
|
90
|
+
params: [
|
|
91
|
+
{
|
|
92
|
+
to: getAddress(call.address),
|
|
93
|
+
data: encodeFunctionData({
|
|
94
|
+
abi: call.abi,
|
|
95
|
+
functionName: call.functionName,
|
|
96
|
+
args: call.args,
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
rpcBlockTag,
|
|
100
|
+
],
|
|
101
|
+
}));
|
|
102
|
+
const results = await batchRpcRequest({ rpcUrl, requests });
|
|
103
|
+
return results.map((result, index) =>
|
|
104
|
+
decodeFunctionResult({
|
|
105
|
+
abi: calls[index].abi,
|
|
106
|
+
functionName: calls[index].functionName,
|
|
107
|
+
data: result,
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function readContracts({ client, rpcUrl, blockTag, address, abi, calls, readBlockParams }) {
|
|
113
|
+
if (rpcUrl) {
|
|
114
|
+
try {
|
|
115
|
+
return await readContractsBatch({ rpcUrl, blockTag, address, abi, calls });
|
|
116
|
+
} catch (_) {}
|
|
117
|
+
}
|
|
118
|
+
return Promise.all(
|
|
119
|
+
calls.map((call) =>
|
|
120
|
+
client.readContract({
|
|
121
|
+
address,
|
|
122
|
+
abi,
|
|
123
|
+
functionName: call.functionName,
|
|
124
|
+
args: call.args,
|
|
125
|
+
...readBlockParams,
|
|
126
|
+
})
|
|
127
|
+
)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function readRawCall({ client, rpcUrl, to, data, blockTag }) {
|
|
132
|
+
if (rpcUrl) {
|
|
133
|
+
try {
|
|
134
|
+
const [result] = await batchRpcRequest({
|
|
135
|
+
rpcUrl,
|
|
136
|
+
requests: [
|
|
137
|
+
{
|
|
138
|
+
jsonrpc: '2.0',
|
|
139
|
+
id: 1,
|
|
140
|
+
method: 'eth_call',
|
|
141
|
+
params: [{ to, data }, toRpcBlockTag(blockTag)],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
return result;
|
|
146
|
+
} catch (_) {}
|
|
147
|
+
}
|
|
148
|
+
return client.transport.request({
|
|
149
|
+
method: 'eth_call',
|
|
150
|
+
params: [{ to, data }, toRpcBlockTag(blockTag)],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function chunk(array, size) {
|
|
155
|
+
const out = [];
|
|
156
|
+
for (let i = 0; i < array.length; i += size) out.push(array.slice(i, i + size));
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
35
159
|
|
|
36
160
|
function toSigned256(x) {
|
|
37
161
|
return x >= TWO_255 ? x - TWO_256 : x;
|
|
@@ -115,57 +239,126 @@ export async function fetchPoolSnapshotViem({ poolAddress, rpcUrl, percentBucket
|
|
|
115
239
|
let token0Addr;
|
|
116
240
|
let token1Addr;
|
|
117
241
|
let poolType = 'uniswap';
|
|
242
|
+
const poolCacheKey = pool.toLowerCase();
|
|
243
|
+
const cachedPoolStatic = poolStaticCache.get(poolCacheKey);
|
|
118
244
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
245
|
+
if (cachedPoolStatic?.poolType === 'uniswap') {
|
|
246
|
+
poolType = 'uniswap';
|
|
247
|
+
const [slot0, L] = await readContracts({
|
|
248
|
+
client: c,
|
|
249
|
+
rpcUrl,
|
|
250
|
+
blockTag,
|
|
251
|
+
address: pool,
|
|
252
|
+
abi: UNISWAP_POOL_ABI,
|
|
253
|
+
calls: [{ functionName: 'slot0' }, { functionName: 'liquidity' }],
|
|
254
|
+
readBlockParams,
|
|
255
|
+
});
|
|
128
256
|
sqrtPriceX96 = slot0[0];
|
|
129
257
|
tick = Number(slot0[1]);
|
|
130
258
|
liquidity = L;
|
|
131
|
-
feePips =
|
|
132
|
-
tickSpacing =
|
|
133
|
-
token0Addr =
|
|
134
|
-
token1Addr =
|
|
135
|
-
}
|
|
259
|
+
feePips = cachedPoolStatic.feePips;
|
|
260
|
+
tickSpacing = cachedPoolStatic.tickSpacing;
|
|
261
|
+
token0Addr = cachedPoolStatic.token0Addr;
|
|
262
|
+
token1Addr = cachedPoolStatic.token1Addr;
|
|
263
|
+
} else if (cachedPoolStatic?.poolType === 'algebra') {
|
|
136
264
|
poolType = 'algebra';
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
],
|
|
149
|
-
});
|
|
150
|
-
globalState = decodeAlgebraGlobalState(callResult);
|
|
151
|
-
} catch (algebraStateErr) {
|
|
152
|
-
const combined = new Error(`Failed to read pool state as Uniswap (slot0): ${uniswapErr?.message || uniswapErr}. Algebra globalState call also failed: ${algebraStateErr?.message || algebraStateErr}`);
|
|
153
|
-
combined.cause = { uniswap: uniswapErr, algebra: algebraStateErr };
|
|
154
|
-
throw combined;
|
|
155
|
-
}
|
|
156
|
-
const [L, spacing, tok0, tok1] = await Promise.all([
|
|
157
|
-
c.readContract({ address: pool, abi: ALGEBRA_POOL_ABI, functionName: 'liquidity', ...readBlockParams }),
|
|
158
|
-
c.readContract({ address: pool, abi: ALGEBRA_POOL_ABI, functionName: 'tickSpacing', ...readBlockParams }),
|
|
159
|
-
c.readContract({ address: pool, abi: ALGEBRA_POOL_ABI, functionName: 'token0', ...readBlockParams }),
|
|
160
|
-
c.readContract({ address: pool, abi: ALGEBRA_POOL_ABI, functionName: 'token1', ...readBlockParams }),
|
|
265
|
+
const [globalStateHex, L] = await Promise.all([
|
|
266
|
+
readRawCall({ client: c, rpcUrl, to: pool, data: GLOBAL_STATE_SELECTOR, blockTag }),
|
|
267
|
+
readContracts({
|
|
268
|
+
client: c,
|
|
269
|
+
rpcUrl,
|
|
270
|
+
blockTag,
|
|
271
|
+
address: pool,
|
|
272
|
+
abi: ALGEBRA_POOL_ABI,
|
|
273
|
+
calls: [{ functionName: 'liquidity' }],
|
|
274
|
+
readBlockParams,
|
|
275
|
+
}).then((results) => results[0]),
|
|
161
276
|
]);
|
|
277
|
+
const globalState = decodeAlgebraGlobalState(globalStateHex);
|
|
162
278
|
sqrtPriceX96 = globalState.sqrtPriceX96;
|
|
163
279
|
tick = globalState.tick;
|
|
164
280
|
liquidity = L;
|
|
165
281
|
feePips = globalState.feePips;
|
|
166
|
-
tickSpacing =
|
|
167
|
-
token0Addr =
|
|
168
|
-
token1Addr =
|
|
282
|
+
tickSpacing = cachedPoolStatic.tickSpacing;
|
|
283
|
+
token0Addr = cachedPoolStatic.token0Addr;
|
|
284
|
+
token1Addr = cachedPoolStatic.token1Addr;
|
|
285
|
+
} else {
|
|
286
|
+
try {
|
|
287
|
+
const [slot0, L, fee, spacing, tok0, tok1] = await readContracts({
|
|
288
|
+
client: c,
|
|
289
|
+
rpcUrl,
|
|
290
|
+
blockTag,
|
|
291
|
+
address: pool,
|
|
292
|
+
abi: UNISWAP_POOL_ABI,
|
|
293
|
+
calls: [
|
|
294
|
+
{ functionName: 'slot0' },
|
|
295
|
+
{ functionName: 'liquidity' },
|
|
296
|
+
{ functionName: 'fee' },
|
|
297
|
+
{ functionName: 'tickSpacing' },
|
|
298
|
+
{ functionName: 'token0' },
|
|
299
|
+
{ functionName: 'token1' },
|
|
300
|
+
],
|
|
301
|
+
readBlockParams,
|
|
302
|
+
});
|
|
303
|
+
sqrtPriceX96 = slot0[0];
|
|
304
|
+
tick = Number(slot0[1]);
|
|
305
|
+
liquidity = L;
|
|
306
|
+
feePips = Number(fee);
|
|
307
|
+
tickSpacing = Number(spacing);
|
|
308
|
+
token0Addr = tok0;
|
|
309
|
+
token1Addr = tok1;
|
|
310
|
+
poolStaticCache.set(poolCacheKey, {
|
|
311
|
+
poolType: 'uniswap',
|
|
312
|
+
feePips,
|
|
313
|
+
tickSpacing,
|
|
314
|
+
token0Addr,
|
|
315
|
+
token1Addr,
|
|
316
|
+
});
|
|
317
|
+
} catch (uniswapErr) {
|
|
318
|
+
poolType = 'algebra';
|
|
319
|
+
let globalState;
|
|
320
|
+
try {
|
|
321
|
+
const callResult = await readRawCall({
|
|
322
|
+
client: c,
|
|
323
|
+
rpcUrl,
|
|
324
|
+
to: pool,
|
|
325
|
+
data: GLOBAL_STATE_SELECTOR,
|
|
326
|
+
blockTag,
|
|
327
|
+
});
|
|
328
|
+
globalState = decodeAlgebraGlobalState(callResult);
|
|
329
|
+
} catch (algebraStateErr) {
|
|
330
|
+
const combined = new Error(`Failed to read pool state as Uniswap (slot0): ${uniswapErr?.message || uniswapErr}. Algebra globalState call also failed: ${algebraStateErr?.message || algebraStateErr}`);
|
|
331
|
+
combined.cause = { uniswap: uniswapErr, algebra: algebraStateErr };
|
|
332
|
+
throw combined;
|
|
333
|
+
}
|
|
334
|
+
const [L, spacing, tok0, tok1] = await readContracts({
|
|
335
|
+
client: c,
|
|
336
|
+
rpcUrl,
|
|
337
|
+
blockTag,
|
|
338
|
+
address: pool,
|
|
339
|
+
abi: ALGEBRA_POOL_ABI,
|
|
340
|
+
calls: [
|
|
341
|
+
{ functionName: 'liquidity' },
|
|
342
|
+
{ functionName: 'tickSpacing' },
|
|
343
|
+
{ functionName: 'token0' },
|
|
344
|
+
{ functionName: 'token1' },
|
|
345
|
+
],
|
|
346
|
+
readBlockParams,
|
|
347
|
+
});
|
|
348
|
+
sqrtPriceX96 = globalState.sqrtPriceX96;
|
|
349
|
+
tick = globalState.tick;
|
|
350
|
+
liquidity = L;
|
|
351
|
+
feePips = globalState.feePips;
|
|
352
|
+
tickSpacing = Number(spacing);
|
|
353
|
+
token0Addr = tok0;
|
|
354
|
+
token1Addr = tok1;
|
|
355
|
+
poolStaticCache.set(poolCacheKey, {
|
|
356
|
+
poolType: 'algebra',
|
|
357
|
+
tickSpacing,
|
|
358
|
+
token0Addr,
|
|
359
|
+
token1Addr,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
169
362
|
}
|
|
170
363
|
|
|
171
364
|
tickSpacing = Math.abs(Number(tickSpacing));
|
|
@@ -173,12 +366,66 @@ export async function fetchPoolSnapshotViem({ poolAddress, rpcUrl, percentBucket
|
|
|
173
366
|
throw new Error('tickSpacing resolved to zero');
|
|
174
367
|
}
|
|
175
368
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
369
|
+
const token0Key = token0Addr.toLowerCase();
|
|
370
|
+
const token1Key = token1Addr.toLowerCase();
|
|
371
|
+
let token0Meta = tokenMetaCache.get(token0Key);
|
|
372
|
+
let token1Meta = tokenMetaCache.get(token1Key);
|
|
373
|
+
|
|
374
|
+
if (!token0Meta || !token1Meta) {
|
|
375
|
+
if (rpcUrl) {
|
|
376
|
+
const pendingCalls = [];
|
|
377
|
+
if (!token0Meta) {
|
|
378
|
+
pendingCalls.push(
|
|
379
|
+
{ address: token0Addr, abi: ERC20_ABI, functionName: 'decimals' },
|
|
380
|
+
{ address: token0Addr, abi: ERC20_ABI, functionName: 'symbol' }
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (!token1Meta) {
|
|
384
|
+
pendingCalls.push(
|
|
385
|
+
{ address: token1Addr, abi: ERC20_ABI, functionName: 'decimals' },
|
|
386
|
+
{ address: token1Addr, abi: ERC20_ABI, functionName: 'symbol' }
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const results = await readContractCallsBatch({ rpcUrl, blockTag, calls: pendingCalls });
|
|
390
|
+
let offset = 0;
|
|
391
|
+
if (!token0Meta) {
|
|
392
|
+
token0Meta = { decimals: Number(results[offset]), symbol: results[offset + 1] };
|
|
393
|
+
tokenMetaCache.set(token0Key, token0Meta);
|
|
394
|
+
offset += 2;
|
|
395
|
+
}
|
|
396
|
+
if (!token1Meta) {
|
|
397
|
+
token1Meta = { decimals: Number(results[offset]), symbol: results[offset + 1] };
|
|
398
|
+
tokenMetaCache.set(token1Key, token1Meta);
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
if (!token0Meta) {
|
|
402
|
+
const [decimals, symbol] = await readContracts({
|
|
403
|
+
client: c,
|
|
404
|
+
rpcUrl,
|
|
405
|
+
blockTag,
|
|
406
|
+
address: token0Addr,
|
|
407
|
+
abi: ERC20_ABI,
|
|
408
|
+
calls: [{ functionName: 'decimals' }, { functionName: 'symbol' }],
|
|
409
|
+
readBlockParams,
|
|
410
|
+
});
|
|
411
|
+
token0Meta = { decimals: Number(decimals), symbol };
|
|
412
|
+
tokenMetaCache.set(token0Key, token0Meta);
|
|
413
|
+
}
|
|
414
|
+
if (!token1Meta) {
|
|
415
|
+
const [decimals, symbol] = await readContracts({
|
|
416
|
+
client: c,
|
|
417
|
+
rpcUrl,
|
|
418
|
+
blockTag,
|
|
419
|
+
address: token1Addr,
|
|
420
|
+
abi: ERC20_ABI,
|
|
421
|
+
calls: [{ functionName: 'decimals' }, { functionName: 'symbol' }],
|
|
422
|
+
readBlockParams,
|
|
423
|
+
});
|
|
424
|
+
token1Meta = { decimals: Number(decimals), symbol };
|
|
425
|
+
tokenMetaCache.set(token1Key, token1Meta);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
182
429
|
|
|
183
430
|
const far = Math.max(...percentBuckets);
|
|
184
431
|
const delta = percentToTickDelta(far);
|
|
@@ -193,19 +440,45 @@ export async function fetchPoolSnapshotViem({ poolAddress, rpcUrl, percentBucket
|
|
|
193
440
|
const ticks = [];
|
|
194
441
|
const poolAbi = poolType === 'algebra' ? ALGEBRA_POOL_ABI : UNISWAP_POOL_ABI;
|
|
195
442
|
const bitmapFn = poolType === 'algebra' ? 'tickTable' : 'tickBitmap';
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
443
|
+
const bitmapWords = [];
|
|
444
|
+
for (let w = wordStart; w <= wordEnd; w++) bitmapWords.push(w);
|
|
445
|
+
const bitmapResults = bitmapWords.length
|
|
446
|
+
? await readContracts({
|
|
447
|
+
client: c,
|
|
448
|
+
rpcUrl,
|
|
449
|
+
blockTag,
|
|
450
|
+
address: pool,
|
|
451
|
+
abi: poolAbi,
|
|
452
|
+
calls: bitmapWords.map((w) => ({ functionName: bitmapFn, args: [BigInt(w)] })),
|
|
453
|
+
readBlockParams,
|
|
454
|
+
})
|
|
455
|
+
: [];
|
|
456
|
+
const tickIndexes = [];
|
|
457
|
+
for (let i = 0; i < bitmapWords.length; i++) {
|
|
458
|
+
const w = bitmapWords[i];
|
|
459
|
+
const bitmap = bitmapResults[i];
|
|
199
460
|
if (bitmap === 0n) continue;
|
|
200
461
|
for (let bit = 0; bit < 256; bit++) {
|
|
201
462
|
if (((bitmap >> BigInt(bit)) & 1n) === 0n) continue;
|
|
202
463
|
const compressed = w * 256 + bit;
|
|
203
464
|
const t = compressed * tickSpacing;
|
|
204
465
|
if (t < start || t > end) continue;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
466
|
+
tickIndexes.push(t);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
for (const tickBatch of chunk(tickIndexes, 128)) {
|
|
471
|
+
const tickResults = await readContracts({
|
|
472
|
+
client: c,
|
|
473
|
+
rpcUrl,
|
|
474
|
+
blockTag,
|
|
475
|
+
address: pool,
|
|
476
|
+
abi: poolAbi,
|
|
477
|
+
calls: tickBatch.map((tickIndex) => ({ functionName: 'ticks', args: [tickIndex] })),
|
|
478
|
+
readBlockParams,
|
|
479
|
+
});
|
|
480
|
+
for (let i = 0; i < tickBatch.length; i++) {
|
|
481
|
+
ticks.push({ index: tickBatch[i], liquidityNet: tickResults[i][1] });
|
|
209
482
|
}
|
|
210
483
|
}
|
|
211
484
|
|
|
@@ -215,11 +488,11 @@ export async function fetchPoolSnapshotViem({ poolAddress, rpcUrl, percentBucket
|
|
|
215
488
|
liquidity,
|
|
216
489
|
feePips,
|
|
217
490
|
ticks: ticks.sort((a,b) => a.index - b.index),
|
|
218
|
-
token0: { decimals:
|
|
219
|
-
token1: { decimals:
|
|
491
|
+
token0: { decimals: token0Meta.decimals, usdPrice: 0, symbol: token0Meta.symbol, id: token0Addr },
|
|
492
|
+
token1: { decimals: token1Meta.decimals, usdPrice: 0, symbol: token1Meta.symbol, id: token1Addr },
|
|
220
493
|
meta: {
|
|
221
|
-
token0: { decimals:
|
|
222
|
-
token1: { decimals:
|
|
494
|
+
token0: { decimals: token0Meta.decimals, symbol: token0Meta.symbol, id: token0Addr },
|
|
495
|
+
token1: { decimals: token1Meta.decimals, symbol: token1Meta.symbol, id: token1Addr },
|
|
223
496
|
poolId: pool,
|
|
224
497
|
range: { start, end },
|
|
225
498
|
farPercent: far,
|