@steerprotocol/liquidity-meter 1.0.0 → 2.0.0
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 +18 -56
- package/bin/liquidity-depth.js +887 -0
- package/package.json +23 -32
- package/src/api.js +490 -0
- package/src/depth.js +492 -0
- package/src/index.js +6 -0
- package/src/prices.js +188 -0
- package/src/uniswapv3-subgraph.js +141 -0
- package/src/viem-onchain.js +233 -0
- package/src/withdraw.js +333 -0
- package/templates/README.md +37 -0
- package/templates/pools_minimal.csv +3 -0
- package/templates/pools_with_vault.csv +3 -0
- package/dist/adapters/onchain.d.ts +0 -51
- package/dist/adapters/onchain.js +0 -158
- package/dist/adapters/uniswapv3-subgraph.d.ts +0 -44
- package/dist/adapters/uniswapv3-subgraph.js +0 -105
- package/dist/adapters/withdraw.d.ts +0 -14
- package/dist/adapters/withdraw.js +0 -150
- package/dist/api.d.ts +0 -72
- package/dist/api.js +0 -180
- package/dist/cli/liquidity-depth.d.ts +0 -2
- package/dist/cli/liquidity-depth.js +0 -1160
- package/dist/core/depth.d.ts +0 -48
- package/dist/core/depth.js +0 -314
- package/dist/handlers/cron.d.ts +0 -27
- package/dist/handlers/cron.js +0 -68
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -7
- package/dist/prices.d.ts +0 -15
- package/dist/prices.js +0 -205
- package/dist/wagmi/config.d.ts +0 -2106
- package/dist/wagmi/config.js +0 -24
- package/dist/wagmi/generated.d.ts +0 -2019
- package/dist/wagmi/generated.js +0 -346
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// CLI for computing Uniswap v3 market depth bands with real data
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execFile as _execFile } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
const execFile = promisify(_execFile);
|
|
10
|
+
import { parseAbi, getAddress } from 'viem';
|
|
11
|
+
import {
|
|
12
|
+
analyzePool,
|
|
13
|
+
formatTimestampUTC,
|
|
14
|
+
parseDurationArg,
|
|
15
|
+
parsePercentBuckets,
|
|
16
|
+
parseTimestampArg,
|
|
17
|
+
resolveAnalysisBlockContext,
|
|
18
|
+
} from '../src/api.js';
|
|
19
|
+
import { createForkClient, simulateVaultWithdraw, resolveViemChain } from '../src/withdraw.js';
|
|
20
|
+
|
|
21
|
+
const UNISWAP_V3_FACTORY = '0x1F98431c8aD98523631AE4a59f267346ea31F984';
|
|
22
|
+
const FACTORY_ABI = parseAbi([
|
|
23
|
+
'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)'
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function parseArgs(argv) {
|
|
27
|
+
const args = {
|
|
28
|
+
percent: '1,2,5,10',
|
|
29
|
+
subgraph: undefined,
|
|
30
|
+
source: 'tevm',
|
|
31
|
+
rpc: process.env.RPC_URL,
|
|
32
|
+
usdSizes: '1000,5000,10000,25000,50000,100000',
|
|
33
|
+
prices: process.env.PRICES_SOURCE || 'auto',
|
|
34
|
+
reserveLimit: Number(process.env.RESERVE_LIMIT) || 100,
|
|
35
|
+
csv: undefined,
|
|
36
|
+
outdir: 'reports',
|
|
37
|
+
// withdraw simulation
|
|
38
|
+
vault: undefined,
|
|
39
|
+
owner: process.env.OWNER,
|
|
40
|
+
withdrawPct: undefined,
|
|
41
|
+
withdrawShares: undefined,
|
|
42
|
+
block: undefined,
|
|
43
|
+
timestamp: undefined,
|
|
44
|
+
timeStart: undefined,
|
|
45
|
+
timeEnd: undefined,
|
|
46
|
+
timeInterval: undefined,
|
|
47
|
+
timeWindow: undefined,
|
|
48
|
+
timeCount: undefined,
|
|
49
|
+
};
|
|
50
|
+
for (let i = 2; i < argv.length; i++) {
|
|
51
|
+
const a = argv[i];
|
|
52
|
+
if (a === '--pool' || a === '-p') args.pool = argv[++i];
|
|
53
|
+
else if (a === '--percent' || a === '-b') args.percent = argv[++i];
|
|
54
|
+
else if (a === '--subgraph' || a === '-s') args.subgraph = argv[++i];
|
|
55
|
+
else if (a === '--rpc' || a === '-r') args.rpc = argv[++i];
|
|
56
|
+
else if (a === '--source') args.source = argv[++i]; // tevm|subgraph|auto
|
|
57
|
+
else if (a === '--token0-usd') args.token0USD = Number(argv[++i]);
|
|
58
|
+
else if (a === '--token1-usd') args.token1USD = Number(argv[++i]);
|
|
59
|
+
else if (a === '--assume-stable') args.assumeStable = Number(argv[++i]); // 0 or 1
|
|
60
|
+
else if (a === '--usd-sizes') args.usdSizes = argv[++i];
|
|
61
|
+
else if (a === '--prices') args.prices = argv[++i]; // llama|coingecko|auto
|
|
62
|
+
// withdraw simulation flags
|
|
63
|
+
else if (a === '--vault') args.vault = argv[++i];
|
|
64
|
+
else if (a === '--owner') args.owner = argv[++i];
|
|
65
|
+
else if (a === '--withdraw-pct' || a === '--withdraw-percent') args.withdrawPct = Number(argv[++i]);
|
|
66
|
+
else if (a === '--withdraw-shares') args.withdrawShares = argv[++i];
|
|
67
|
+
else if (a === '--block') args.block = argv[++i];
|
|
68
|
+
else if (a === '--timestamp' || a === '--time') args.timestamp = argv[++i];
|
|
69
|
+
else if (a === '--time-start') args.timeStart = argv[++i];
|
|
70
|
+
else if (a === '--time-end') args.timeEnd = argv[++i];
|
|
71
|
+
else if (a === '--time-interval') args.timeInterval = argv[++i];
|
|
72
|
+
else if (a === '--time-window') args.timeWindow = argv[++i];
|
|
73
|
+
else if (a === '--time-count') args.timeCount = Number(argv[++i]);
|
|
74
|
+
else if (a === '--debug') args.debug = true;
|
|
75
|
+
else if (a === '--csv') args.csv = argv[++i];
|
|
76
|
+
else if (a === '--outdir') args.outdir = argv[++i];
|
|
77
|
+
else if (a === '--reserve-limit') args.reserveLimit = Number(argv[++i]);
|
|
78
|
+
else if (a === '--json' || a === '-j') args.json = true;
|
|
79
|
+
else if (a === '--help' || a === '-h') args.help = true;
|
|
80
|
+
else if (a === '--version' || a === '-v') args.version = true;
|
|
81
|
+
else if (!args.pool && /^0x[0-9a-fA-F]{40}$/.test(a)) args.pool = a;
|
|
82
|
+
else {
|
|
83
|
+
console.error(`Unknown arg: ${a}`);
|
|
84
|
+
args.help = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return args;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printHelp() {
|
|
91
|
+
console.log(`
|
|
92
|
+
liquidity-depth — Uniswap v3 market depth bands
|
|
93
|
+
|
|
94
|
+
Usage:
|
|
95
|
+
liquidity-depth --pool <POOL_ADDRESS> [--percent 1,2,5,10] [--source tevm|viem|subgraph|auto] [--rpc <URL>] [--block N] [--timestamp ISO|SECONDS]
|
|
96
|
+
[--time-start ISO|SECONDS] [--time-end ISO|SECONDS] [--time-interval 1h] [--time-window 7d] [--time-count N]
|
|
97
|
+
[--token0-usd N] [--token1-usd N] [--usd-sizes 1000,5000,...] [--json]
|
|
98
|
+
liquidity-depth --pool <POOL_ADDRESS> --vault <VAULT_ADDRESS> --owner <OWNER_ADDRESS> [--withdraw-pct N | --withdraw-shares <SHARES>] [same options]
|
|
99
|
+
liquidity-depth --csv <FILE.csv> [--outdir reports] [same options as above]
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
-p, --pool Uniswap v3 pool address (e.g., 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8)
|
|
103
|
+
-b, --percent Percent buckets, comma-separated (default: 1,2,5,10)
|
|
104
|
+
-s, --subgraph Subgraph URL (default: Uniswap v3 mainnet)
|
|
105
|
+
--source Data source: tevm|viem|subgraph|auto (default: tevm)
|
|
106
|
+
-r, --rpc RPC URL (for on-chain fetching via tevm fork). Also reads RPC_URL env var
|
|
107
|
+
--token0-usd Override token0 USD price
|
|
108
|
+
--token1-usd Override token1 USD price
|
|
109
|
+
--assume-stable 0|1 Assume token index is $1 stable (fills missing USD)
|
|
110
|
+
--usd-sizes Comma-separated USD sizes for price impact (default: 1000,5000,10000,25000,50000,100000)
|
|
111
|
+
--block Fork/query state at the specified block number
|
|
112
|
+
--timestamp Resolve a historical block by timestamp (seconds or ISO-8601); requires --rpc
|
|
113
|
+
--time-start Beginning timestamp (inclusive) for time-series mode (seconds or ISO-8601)
|
|
114
|
+
--time-end Ending timestamp (inclusive, default: now) for time-series mode
|
|
115
|
+
--time-interval Step between snapshots in time-series mode (e.g., 1h, 30m; default: 1h)
|
|
116
|
+
--time-window How far back from --time-end to start when --time-start omitted (e.g., 7d)
|
|
117
|
+
--time-count Number of intervals to fetch (e.g., 168 for 7 days hourly); implies backwards from --time-end
|
|
118
|
+
--prices Price source: reserve|dexscreener|llama|coingecko|auto (default: auto; env PRICES_SOURCE)
|
|
119
|
+
--reserve-limit N Limit for Reserve API (default: 100; env RESERVE_LIMIT)
|
|
120
|
+
--debug Print debug logs to stderr for pricing/inference
|
|
121
|
+
--csv CSV file with a header including a 'pool' or 'address' column
|
|
122
|
+
--outdir Output directory for per-pool reports (default: reports)
|
|
123
|
+
--vault Steer vault address (to simulate withdrawal)
|
|
124
|
+
--owner Owner address whose shares are withdrawn (also reads OWNER env)
|
|
125
|
+
--withdraw-pct Percent of owner's shares to withdraw (e.g., 25)
|
|
126
|
+
--withdraw-shares Raw share amount to withdraw (overrides --withdraw-pct)
|
|
127
|
+
-j, --json Output machine-readable JSON
|
|
128
|
+
-h, --help Show help
|
|
129
|
+
-v, --version Show version
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function looksLikeAddress(x) {
|
|
134
|
+
return typeof x === 'string' && /^0x[0-9a-fA-F]{40}$/.test(x.trim());
|
|
135
|
+
}
|
|
136
|
+
function extractFirstAddress(x) {
|
|
137
|
+
if (typeof x !== 'string') return undefined;
|
|
138
|
+
const m = x.match(/0x[0-9a-fA-F]{40}/);
|
|
139
|
+
return m ? m[0] : undefined;
|
|
140
|
+
}
|
|
141
|
+
function normKey(k) {
|
|
142
|
+
return String(k || '').toLowerCase().replace(/[ _-]+/g, '');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Minimal CSV parser: returns array of row objects keyed by header.
|
|
146
|
+
// Supports commas, quoted fields with double-quotes escaping.
|
|
147
|
+
function parseCSV(content) {
|
|
148
|
+
const outRows = [];
|
|
149
|
+
let i = 0;
|
|
150
|
+
const len = content.length;
|
|
151
|
+
const fields = [];
|
|
152
|
+
function readLine() {
|
|
153
|
+
fields.length = 0;
|
|
154
|
+
let field = '';
|
|
155
|
+
let inQuotes = false;
|
|
156
|
+
let sawAny = false;
|
|
157
|
+
while (i < len) {
|
|
158
|
+
const ch = content[i++];
|
|
159
|
+
if (!inQuotes && (ch === '\n' || ch === '\r')) {
|
|
160
|
+
if (ch === '\r' && content[i] === '\n') i++; // CRLF
|
|
161
|
+
fields.push(field);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (ch === '"') {
|
|
165
|
+
if (inQuotes) {
|
|
166
|
+
if (content[i] === '"') { field += '"'; i++; }
|
|
167
|
+
else { inQuotes = false; }
|
|
168
|
+
} else {
|
|
169
|
+
inQuotes = true;
|
|
170
|
+
sawAny = true;
|
|
171
|
+
}
|
|
172
|
+
} else if (ch === ',' && !inQuotes) {
|
|
173
|
+
fields.push(field);
|
|
174
|
+
field = '';
|
|
175
|
+
sawAny = true;
|
|
176
|
+
} else {
|
|
177
|
+
field += ch;
|
|
178
|
+
sawAny = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!sawAny && field === '') return false; // EOF after last newline
|
|
182
|
+
fields.push(field);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
// read header
|
|
186
|
+
if (!readLine()) return [];
|
|
187
|
+
const header = fields.map((h) => h.trim());
|
|
188
|
+
while (readLine()) {
|
|
189
|
+
const obj = {};
|
|
190
|
+
for (let j = 0; j < header.length; j++) obj[header[j]] = (fields[j] ?? '').trim();
|
|
191
|
+
outRows.push(obj);
|
|
192
|
+
}
|
|
193
|
+
return outRows;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function runBatch(args) {
|
|
197
|
+
const csvPath = args.csv;
|
|
198
|
+
const outdir = args.outdir || 'reports';
|
|
199
|
+
const data = await fs.readFile(csvPath, 'utf8');
|
|
200
|
+
const rows = parseCSV(data);
|
|
201
|
+
if (!rows.length) {
|
|
202
|
+
console.error('CSV is empty or could not be parsed. Expect a header row with a pool/address column.');
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
await fs.mkdir(outdir, { recursive: true });
|
|
206
|
+
|
|
207
|
+
const pick = (obj, names) => {
|
|
208
|
+
// try direct keys
|
|
209
|
+
for (const n of names) if (obj[n] != null && String(obj[n]).trim() !== '') return String(obj[n]).trim();
|
|
210
|
+
// try normalized keys
|
|
211
|
+
const map = Object.fromEntries(Object.keys(obj).map((k) => [normKey(k), k]));
|
|
212
|
+
for (const n of names) {
|
|
213
|
+
const key = map[normKey(n)];
|
|
214
|
+
if (key && obj[key] != null && String(obj[key]).trim() !== '') return String(obj[key]).trim();
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
function parseFeeToUint24(v) {
|
|
220
|
+
if (v == null) return undefined;
|
|
221
|
+
const s = String(v).trim();
|
|
222
|
+
if (!s) return undefined;
|
|
223
|
+
// raw number like 500, 3000, 10000
|
|
224
|
+
if (/^\d+$/.test(s)) {
|
|
225
|
+
const n = Number(s);
|
|
226
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
227
|
+
}
|
|
228
|
+
// like 30bps or 5 bps
|
|
229
|
+
const mBps = s.match(/([0-9]+(?:\.[0-9]+)?)\s*bps/i);
|
|
230
|
+
if (mBps) {
|
|
231
|
+
const n = Number(mBps[1]);
|
|
232
|
+
if (Number.isFinite(n)) return Math.round(n * 100);
|
|
233
|
+
}
|
|
234
|
+
// like 0.3% or 1 %
|
|
235
|
+
const mPct = s.match(/([0-9]+(?:\.[0-9]+)?)\s*%/);
|
|
236
|
+
if (mPct) {
|
|
237
|
+
const n = Number(mPct[1]);
|
|
238
|
+
if (Number.isFinite(n)) return Math.round(n * 1e4);
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function resolvePoolFromTokens(row, rpcUrl) {
|
|
244
|
+
// attempt to find token addresses + fee
|
|
245
|
+
let t0 = pick(row, ['token0', 'token0_address', 'token 0', 'token 0 address', 'tokenA', 'base']);
|
|
246
|
+
let t1 = pick(row, ['token1', 'token1_address', 'token 1', 'token 1 address', 'tokenB', 'quote']);
|
|
247
|
+
let fee = pick(row, ['fee', 'fee_tier', 'fee tier', 'feebps']);
|
|
248
|
+
|
|
249
|
+
// If not found, scan all values for first two distinct addresses
|
|
250
|
+
if (!looksLikeAddress(t0 || '') || !looksLikeAddress(t1 || '')) {
|
|
251
|
+
const found = [];
|
|
252
|
+
for (const v of Object.values(row)) {
|
|
253
|
+
const addr = extractFirstAddress(String(v || ''));
|
|
254
|
+
if (addr && !found.includes(addr)) found.push(addr);
|
|
255
|
+
if (found.length >= 2) break;
|
|
256
|
+
}
|
|
257
|
+
if (!looksLikeAddress(t0 || '') && found[0]) t0 = found[0];
|
|
258
|
+
if (!looksLikeAddress(t1 || '') && found[1]) t1 = found[1];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// parse fee
|
|
262
|
+
const feeNum = parseFeeToUint24(fee);
|
|
263
|
+
if (!looksLikeAddress(t0 || '') || !looksLikeAddress(t1 || '') || !feeNum) return undefined;
|
|
264
|
+
if (!rpcUrl) return undefined;
|
|
265
|
+
|
|
266
|
+
const { createMemoryClient, http } = await import('tevm');
|
|
267
|
+
const client = createMemoryClient({ fork: { transport: http(rpcUrl), blockTag: 'latest' }, loggingLevel: 'error' });
|
|
268
|
+
if (client.tevmReady) await client.tevmReady();
|
|
269
|
+
try {
|
|
270
|
+
const pool = await client.readContract({ address: UNISWAP_V3_FACTORY, abi: FACTORY_ABI, functionName: 'getPool', args: [getAddress(t0), getAddress(t1), feeNum] });
|
|
271
|
+
if (pool && pool !== '0x0000000000000000000000000000000000000000') return pool;
|
|
272
|
+
} catch (_) {
|
|
273
|
+
// ignore
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
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
|
+
for (let idx = 0; idx < rows.length; idx++) {
|
|
300
|
+
const row = rows[idx];
|
|
301
|
+
let pool = pick(row, ['pool', 'address', 'pool_address', 'id', 'Pool', 'Address', 'contract_address', 'contract address', 'pair_address', 'pair address']);
|
|
302
|
+
if (!looksLikeAddress(pool || '')) {
|
|
303
|
+
// try extract from any cell (e.g., Etherscan/Uniswap URL)
|
|
304
|
+
for (const v of Object.values(row)) {
|
|
305
|
+
const found = extractFirstAddress(String(v || ''));
|
|
306
|
+
if (found) { pool = found; break; }
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!looksLikeAddress(pool || '')) {
|
|
310
|
+
// try factory lookup using tokens + fee
|
|
311
|
+
const viaFactory = await resolvePoolFromTokens(row, args.rpc);
|
|
312
|
+
if (viaFactory) pool = viaFactory;
|
|
313
|
+
}
|
|
314
|
+
if (!looksLikeAddress(pool || '')) {
|
|
315
|
+
console.warn(`Row ${idx + 1}: skipping (no valid pool address)`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const per = [...base];
|
|
320
|
+
const rpcs = pick(row, ['rpc', 'RPC', 'rpc_url', 'RPC_URL']);
|
|
321
|
+
if (rpcs) per.push('--rpc', rpcs);
|
|
322
|
+
const source = pick(row, ['source', 'Source']);
|
|
323
|
+
if (source) per.push('--source', source);
|
|
324
|
+
const percent = pick(row, ['percent', 'Percents', 'buckets']);
|
|
325
|
+
if (percent) per.push('--percent', percent);
|
|
326
|
+
const blockOverride = pick(row, ['block', 'Block', 'block_number', 'block number']);
|
|
327
|
+
if (blockOverride) per.push('--block', blockOverride);
|
|
328
|
+
const tsOverride = pick(row, ['timestamp', 'Timestamp', 'time', 'Time']);
|
|
329
|
+
if (tsOverride) per.push('--timestamp', tsOverride);
|
|
330
|
+
const token0USD = pick(row, ['token0_usd', 'token0-usd', 'token0USD']);
|
|
331
|
+
const token1USD = pick(row, ['token1_usd', 'token1-usd', 'token1USD']);
|
|
332
|
+
if (token0USD) per.push('--token0-usd', String(token0USD));
|
|
333
|
+
if (token1USD) per.push('--token1-usd', String(token1USD));
|
|
334
|
+
const usdSizes = pick(row, ['usd_sizes', 'usd-sizes', 'usdSizes']);
|
|
335
|
+
if (usdSizes) per.push('--usd-sizes', usdSizes);
|
|
336
|
+
const assumeStable = pick(row, ['assume_stable', 'assume-stable', 'assumeStable']);
|
|
337
|
+
if (assumeStable != null) per.push('--assume-stable', String(assumeStable));
|
|
338
|
+
const prices = pick(row, ['prices', 'Prices']);
|
|
339
|
+
if (prices) per.push('--prices', String(prices));
|
|
340
|
+
|
|
341
|
+
const vault = pick(row, ['vault', 'Vault', 'vault_address', 'VaultAddress', 'vault address', 'strategy']);
|
|
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));
|
|
349
|
+
|
|
350
|
+
if (args.debug) {
|
|
351
|
+
const dbg = [];
|
|
352
|
+
if (vault) dbg.push(`vault=${vault}`);
|
|
353
|
+
if (owner) dbg.push(`owner=${owner}`);
|
|
354
|
+
if (wpct) dbg.push(`withdraw_pct=${wpct}`);
|
|
355
|
+
if (wshares) dbg.push(`withdraw_shares=${wshares}`);
|
|
356
|
+
if (dbg.length) console.error(`[batch] row ${idx + 1} overrides: ${dbg.join(' ')}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const argsArr = [scriptPath, '--pool', pool, ...per];
|
|
360
|
+
try {
|
|
361
|
+
const { stdout, stderr } = await execFile(process.execPath, argsArr, { env: process.env, maxBuffer: 5 * 1024 * 1024 });
|
|
362
|
+
const baseName = `${pool.toLowerCase()}`;
|
|
363
|
+
const file = path.join(outdir, `${baseName}.${args.json ? 'json' : 'txt'}`);
|
|
364
|
+
const cleaned = cleanReportOutput(stdout);
|
|
365
|
+
await fs.writeFile(file, cleaned, 'utf8');
|
|
366
|
+
if (args.debug && stderr) {
|
|
367
|
+
// Forward child's debug logs to parent stderr
|
|
368
|
+
process.stderr.write(stderr);
|
|
369
|
+
}
|
|
370
|
+
console.log(`Wrote: ${file}`);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
const errMsg = String(err?.stderr || err?.stdout || err?.message || err);
|
|
373
|
+
const errFile = path.join(outdir, `error-${pool.toLowerCase()}.txt`);
|
|
374
|
+
await fs.writeFile(errFile, errMsg, 'utf8');
|
|
375
|
+
console.error(`Failed row ${idx + 1} (${pool}): ${err?.message || err}`);
|
|
376
|
+
console.error(` -> see ${errFile}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
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
|
+
async function runTimeSeries(args) {
|
|
417
|
+
if (!args.pool) {
|
|
418
|
+
console.error('Time-series mode requires --pool');
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
if (args.block) {
|
|
422
|
+
console.error('Time-series mode cannot be combined with --block. Use --time-start/--time-window/--time-count with --timestamp.');
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
if (!args.rpc) {
|
|
426
|
+
console.error('Time-series mode requires --rpc (or RPC_URL env) so timestamps can be resolved to blocks.');
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let intervalSec;
|
|
431
|
+
try {
|
|
432
|
+
intervalSec = parseDurationArg(args.timeInterval ?? '1h');
|
|
433
|
+
} catch (e) {
|
|
434
|
+
console.error(`Invalid --time-interval value: ${e?.message || e}`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
if (intervalSec <= 0n) {
|
|
438
|
+
console.error('--time-interval must be greater than zero');
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let endSec;
|
|
443
|
+
try {
|
|
444
|
+
endSec = args.timeEnd != null ? parseTimestampArg(args.timeEnd) : BigInt(Math.floor(Date.now() / 1000));
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.error(`Invalid --time-end value: ${e?.message || e}`);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let startSec;
|
|
451
|
+
if (args.timeStart != null) {
|
|
452
|
+
try {
|
|
453
|
+
startSec = parseTimestampArg(args.timeStart);
|
|
454
|
+
} catch (e) {
|
|
455
|
+
console.error(`Invalid --time-start value: ${e?.message || e}`);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (startSec == null && args.timeWindow != null) {
|
|
460
|
+
try {
|
|
461
|
+
const windowSec = parseDurationArg(args.timeWindow);
|
|
462
|
+
startSec = endSec > windowSec ? endSec - windowSec : 0n;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.error(`Invalid --time-window value: ${e?.message || e}`);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
let count = undefined;
|
|
470
|
+
if (args.timeCount != null) {
|
|
471
|
+
if (!Number.isFinite(args.timeCount) || args.timeCount <= 0) {
|
|
472
|
+
console.error('--time-count must be a positive integer');
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
count = Math.floor(args.timeCount);
|
|
476
|
+
}
|
|
477
|
+
if (startSec == null) {
|
|
478
|
+
if (count != null) {
|
|
479
|
+
const span = intervalSec * BigInt(Math.max(count - 1, 0));
|
|
480
|
+
startSec = endSec > span ? endSec - span : 0n;
|
|
481
|
+
} else {
|
|
482
|
+
console.error('Time-series mode requires --time-start, --time-window, or --time-count');
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (startSec > endSec) {
|
|
488
|
+
[startSec, endSec] = [endSec, startSec];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const timestamps = [];
|
|
492
|
+
const maxPoints = 10000;
|
|
493
|
+
for (let ts = startSec; ts <= endSec; ts += intervalSec) {
|
|
494
|
+
timestamps.push(ts);
|
|
495
|
+
if (count != null && timestamps.length >= count) break;
|
|
496
|
+
if (timestamps.length > maxPoints) {
|
|
497
|
+
console.error(`Time-series would produce more than ${maxPoints} snapshots. Increase --time-interval or reduce the window/count.`);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (!timestamps.length) timestamps.push(startSec);
|
|
502
|
+
if (count == null && timestamps[timestamps.length - 1] < endSec) {
|
|
503
|
+
timestamps.push(endSec);
|
|
504
|
+
}
|
|
505
|
+
if (count != null && timestamps.length > count) {
|
|
506
|
+
timestamps.splice(0, timestamps.length - count);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const tsStrings = timestamps.map((ts) => ts.toString());
|
|
510
|
+
if (!tsStrings.length) {
|
|
511
|
+
console.error('No timestamps resolved for time-series mode');
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!args.json) {
|
|
516
|
+
const startIso = formatTimestampUTC(BigInt(tsStrings[0]));
|
|
517
|
+
const endIso = formatTimestampUTC(BigInt(tsStrings[tsStrings.length - 1]));
|
|
518
|
+
const intervalHuman = args.timeInterval ?? '1h';
|
|
519
|
+
console.log(`Time-series: ${tsStrings.length} snapshots from ${startIso || tsStrings[0]} to ${endIso || tsStrings[tsStrings.length - 1]} every ~${intervalHuman}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
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
|
+
const jsonResults = [];
|
|
543
|
+
for (let i = 0; i < tsStrings.length; i++) {
|
|
544
|
+
const tsStr = tsStrings[i];
|
|
545
|
+
const perArgs = [scriptPath, '--pool', args.pool, '--timestamp', tsStr, ...base];
|
|
546
|
+
const label = formatTimestampUTC(BigInt(tsStr)) || tsStr;
|
|
547
|
+
if (!args.json) {
|
|
548
|
+
console.log(`\n=== ${label} (unix ${tsStr}) ===`);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const { stdout, stderr } = await execFile(process.execPath, perArgs, { env: process.env, maxBuffer: 5 * 1024 * 1024 });
|
|
552
|
+
if (args.json) {
|
|
553
|
+
const text = stdout.trim();
|
|
554
|
+
if (text) {
|
|
555
|
+
try {
|
|
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
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
jsonResults.push(null);
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
process.stdout.write(stdout);
|
|
569
|
+
if (!stdout.endsWith('\n')) console.log('');
|
|
570
|
+
}
|
|
571
|
+
if (args.debug && stderr) process.stderr.write(stderr);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
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
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (args.json) {
|
|
581
|
+
console.log(JSON.stringify(jsonResults, null, 2));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Strip noisy logs (e.g., TEVM warnings) from report files while preserving CLI output
|
|
586
|
+
function cleanReportOutput(s) {
|
|
587
|
+
try {
|
|
588
|
+
const lines = String(s || '').split(/\r?\n/);
|
|
589
|
+
const out = [];
|
|
590
|
+
for (const line of lines) {
|
|
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;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function main() {
|
|
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
|
+
|
|
635
|
+
let percentBuckets;
|
|
636
|
+
try {
|
|
637
|
+
percentBuckets = parsePercentBuckets(args.percent);
|
|
638
|
+
} catch (e) {
|
|
639
|
+
console.error(`Invalid --percent value: ${e?.message || e}`);
|
|
640
|
+
process.exit(1);
|
|
641
|
+
}
|
|
642
|
+
if (!percentBuckets.length) {
|
|
643
|
+
console.error('No valid percent buckets parsed.');
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const wantsWithdraw =
|
|
648
|
+
args.vault && (Number.isFinite(args.withdrawPct) || args.withdrawShares);
|
|
649
|
+
let sharedClient = null;
|
|
650
|
+
let sharedBlockTag;
|
|
651
|
+
if (wantsWithdraw) {
|
|
652
|
+
if (!args.rpc) {
|
|
653
|
+
console.error('Missing --rpc (or RPC_URL env) for tevm source.');
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
const blockContext = await resolveAnalysisBlockContext({
|
|
657
|
+
block: args.block,
|
|
658
|
+
timestamp: args.timestamp,
|
|
659
|
+
rpcUrl: args.rpc,
|
|
660
|
+
debug: args.debug,
|
|
661
|
+
});
|
|
662
|
+
sharedBlockTag = blockContext.blockTag;
|
|
663
|
+
sharedClient = await createForkClient({
|
|
664
|
+
rpcUrl: args.rpc,
|
|
665
|
+
blockTag: sharedBlockTag,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let before;
|
|
670
|
+
try {
|
|
671
|
+
before = await analyzePool({
|
|
672
|
+
poolAddress: args.pool,
|
|
673
|
+
percentBuckets,
|
|
674
|
+
source: args.source,
|
|
675
|
+
rpcUrl: args.rpc,
|
|
676
|
+
subgraphUrl: args.subgraph,
|
|
677
|
+
token0USD: args.token0USD,
|
|
678
|
+
token1USD: args.token1USD,
|
|
679
|
+
assumeStable: args.assumeStable,
|
|
680
|
+
usdSizes: args.usdSizes,
|
|
681
|
+
prices: args.prices,
|
|
682
|
+
reserveLimit: args.reserveLimit,
|
|
683
|
+
block: args.block,
|
|
684
|
+
timestamp: args.timestamp,
|
|
685
|
+
debug: args.debug,
|
|
686
|
+
client: sharedClient,
|
|
687
|
+
blockTag: sharedBlockTag,
|
|
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
|
+
}
|
|
700
|
+
|
|
701
|
+
let after = null;
|
|
702
|
+
if (before.usedSource === 'tevm' && wantsWithdraw) {
|
|
703
|
+
const viemChain = await resolveViemChain(args.rpc);
|
|
704
|
+
await simulateVaultWithdraw({
|
|
705
|
+
client: before.client,
|
|
706
|
+
vault: args.vault,
|
|
707
|
+
owner: args.owner,
|
|
708
|
+
withdrawPct: args.withdrawPct,
|
|
709
|
+
withdrawShares: args.withdrawShares,
|
|
710
|
+
debug: !!args.debug,
|
|
711
|
+
cliPool: args.pool,
|
|
712
|
+
viemChain,
|
|
713
|
+
});
|
|
714
|
+
after = await analyzePool({
|
|
715
|
+
poolAddress: args.pool,
|
|
716
|
+
percentBuckets,
|
|
717
|
+
source: 'tevm',
|
|
718
|
+
rpcUrl: args.rpc,
|
|
719
|
+
token0USD: args.token0USD,
|
|
720
|
+
token1USD: args.token1USD,
|
|
721
|
+
assumeStable: args.assumeStable,
|
|
722
|
+
usdSizes: args.usdSizes,
|
|
723
|
+
prices: args.prices,
|
|
724
|
+
reserveLimit: args.reserveLimit,
|
|
725
|
+
debug: args.debug,
|
|
726
|
+
client: before.client,
|
|
727
|
+
blockTag: before.blockTag,
|
|
728
|
+
});
|
|
729
|
+
if (args.debug) {
|
|
730
|
+
console.error(`[after] tick: ${before.snap.tick} -> ${after.snap.tick}`);
|
|
731
|
+
console.error(
|
|
732
|
+
`[after] liquidity: ${before.snap.liquidity.toString()} -> ${after.snap.liquidity.toString()}`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (args.json) {
|
|
738
|
+
const output = after
|
|
739
|
+
? {
|
|
740
|
+
before: serializeAnalysis(args, before),
|
|
741
|
+
after: serializeAnalysis(args, after),
|
|
742
|
+
}
|
|
743
|
+
: serializeAnalysis(args, before);
|
|
744
|
+
console.log(JSON.stringify(output, null, 2));
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
printAnalysisSummary(args, before);
|
|
749
|
+
if (after) {
|
|
750
|
+
console.log('');
|
|
751
|
+
console.log('--- After Withdraw ---');
|
|
752
|
+
printAnalysisSummary(args, after);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function serializeAnalysis(args, analysis) {
|
|
757
|
+
const snap = analysis.snap;
|
|
758
|
+
const blockNumber =
|
|
759
|
+
snap.meta?.blockNumber ?? (analysis.blockNumber !== undefined ? analysis.blockNumber : undefined);
|
|
760
|
+
const timestamp =
|
|
761
|
+
snap.meta?.blockTimestamp ??
|
|
762
|
+
(analysis.timestampSec !== undefined ? analysis.timestampSec : undefined);
|
|
763
|
+
return {
|
|
764
|
+
pool: snap.meta.poolId,
|
|
765
|
+
tokens: {
|
|
766
|
+
token0: snap.meta.token0,
|
|
767
|
+
token1: snap.meta.token1,
|
|
768
|
+
},
|
|
769
|
+
ethPriceUSD: snap.meta.ethPriceUSD,
|
|
770
|
+
tick: snap.tick,
|
|
771
|
+
sqrtPriceX96: snap.sqrtPriceX96.toString(),
|
|
772
|
+
liquidity: snap.liquidity.toString(),
|
|
773
|
+
feePips: snap.feePips,
|
|
774
|
+
range: snap.meta.range,
|
|
775
|
+
percentBuckets: analysis.percentBuckets,
|
|
776
|
+
block: {
|
|
777
|
+
number: blockNumber != null ? String(blockNumber) : undefined,
|
|
778
|
+
timestamp: timestamp != null ? Number(timestamp) : undefined,
|
|
779
|
+
iso: formatTimestampUTC(timestamp),
|
|
780
|
+
input:
|
|
781
|
+
args.block ??
|
|
782
|
+
args.timestamp ??
|
|
783
|
+
(typeof analysis.blockTag === 'string' ? analysis.blockTag : undefined),
|
|
784
|
+
},
|
|
785
|
+
...analysis.result,
|
|
786
|
+
priceImpacts: analysis.priceImpacts
|
|
787
|
+
? {
|
|
788
|
+
usdSizes: analysis.usdSizes,
|
|
789
|
+
buyPct: analysis.priceImpacts.buyPct,
|
|
790
|
+
sellPct: analysis.priceImpacts.sellPct,
|
|
791
|
+
}
|
|
792
|
+
: undefined,
|
|
793
|
+
prices: {
|
|
794
|
+
token0: {
|
|
795
|
+
address: snap.meta.token0.id,
|
|
796
|
+
symbol: snap.meta.token0.symbol,
|
|
797
|
+
usd: analysis.token0USD,
|
|
798
|
+
source: analysis.priceSource0,
|
|
799
|
+
},
|
|
800
|
+
token1: {
|
|
801
|
+
address: snap.meta.token1.id,
|
|
802
|
+
symbol: snap.meta.token1.symbol,
|
|
803
|
+
usd: analysis.token1USD,
|
|
804
|
+
source: analysis.priceSource1,
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
buyCumToken1: analysis.result.buyCumToken1.map((value) => value.toString()),
|
|
808
|
+
sellCumToken0: analysis.result.sellCumToken0.map((value) => value.toString()),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function printAnalysisSummary(args, analysis) {
|
|
813
|
+
const snap = analysis.snap;
|
|
814
|
+
const result = analysis.result;
|
|
815
|
+
const fmtUSD = (value) =>
|
|
816
|
+
new Intl.NumberFormat('en-US', {
|
|
817
|
+
style: 'currency',
|
|
818
|
+
currency: 'USD',
|
|
819
|
+
maximumFractionDigits: 0,
|
|
820
|
+
}).format(value);
|
|
821
|
+
|
|
822
|
+
console.log(`Pool: ${args.pool.toLowerCase()} [source: ${analysis.usedSource}]`);
|
|
823
|
+
const blockNumber =
|
|
824
|
+
snap.meta?.blockNumber ?? (analysis.blockNumber !== undefined ? analysis.blockNumber : undefined);
|
|
825
|
+
const timestamp =
|
|
826
|
+
snap.meta?.blockTimestamp ??
|
|
827
|
+
(analysis.timestampSec !== undefined ? analysis.timestampSec : undefined);
|
|
828
|
+
const blockInfoParts = [];
|
|
829
|
+
if (blockNumber != null) blockInfoParts.push(`#${String(blockNumber)}`);
|
|
830
|
+
const isoTimestamp = formatTimestampUTC(timestamp);
|
|
831
|
+
if (isoTimestamp) blockInfoParts.push(isoTimestamp);
|
|
832
|
+
if (blockNumber == null && typeof analysis.blockTag === 'string') {
|
|
833
|
+
blockInfoParts.push(analysis.blockTag);
|
|
834
|
+
}
|
|
835
|
+
if (blockInfoParts.length) console.log(`Block: ${blockInfoParts.join(' | ')}`);
|
|
836
|
+
console.log(
|
|
837
|
+
`Tokens: ${snap.meta.token0.symbol} (${snap.meta.token0.id}) / ${snap.meta.token1.symbol} (${snap.meta.token1.id})`
|
|
838
|
+
);
|
|
839
|
+
if (snap.meta?.ethPriceUSD) console.log(`ETH/USD: ${snap.meta.ethPriceUSD.toFixed(2)}`);
|
|
840
|
+
const feePct = snap.feePips / 10_000;
|
|
841
|
+
const feeBps = snap.feePips / 100;
|
|
842
|
+
console.log(`Tick: ${snap.tick} Fee: ${feePct.toFixed(2)}% (${feeBps.toFixed(2)} bps)`);
|
|
843
|
+
if (analysis.token0USD || analysis.token1USD) {
|
|
844
|
+
const fmtPrice = (value) =>
|
|
845
|
+
value
|
|
846
|
+
? `$${value.toLocaleString(undefined, { maximumFractionDigits: 6 })}`
|
|
847
|
+
: 'n/a';
|
|
848
|
+
console.log(
|
|
849
|
+
`Prices: ${snap.meta.token0.symbol} ${fmtPrice(analysis.token0USD)} (${analysis.priceSource0 || 'n/a'}), ${snap.meta.token1.symbol} ${fmtPrice(analysis.token1USD)} (${analysis.priceSource1 || 'n/a'})`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
console.log(`Range: [${snap.meta.range.start}, ${snap.meta.range.end}] for ±${snap.meta.farPercent}%`);
|
|
853
|
+
console.log('');
|
|
854
|
+
|
|
855
|
+
const labels = analysis.percentBuckets.map((percent) => `${percent}%`);
|
|
856
|
+
console.log('Buy cumulative (token1 in → price up):');
|
|
857
|
+
labels.forEach((label, index) =>
|
|
858
|
+
console.log(` +${label.padEnd(4)}: ${fmtUSD(result.buyCumUSD[index])}`)
|
|
859
|
+
);
|
|
860
|
+
console.log('Sell cumulative (token0 in → price down):');
|
|
861
|
+
labels.forEach((label, index) =>
|
|
862
|
+
console.log(` -${label.padEnd(4)}: ${fmtUSD(result.sellCumUSD[index])}`)
|
|
863
|
+
);
|
|
864
|
+
console.log('');
|
|
865
|
+
console.log(`Headline Depth +2% : ${fmtUSD(result.depthPlus2USD)}`);
|
|
866
|
+
console.log(`Headline Depth -2% : ${fmtUSD(result.depthMinus2USD)}`);
|
|
867
|
+
|
|
868
|
+
if (!analysis.priceImpacts && analysis.usdSizes.length) {
|
|
869
|
+
console.log('');
|
|
870
|
+
console.log('Note: USD sizes provided but missing USD prices.');
|
|
871
|
+
console.log(' Pass --token0-usd and --token1-usd to enable price impact output.');
|
|
872
|
+
} else if (analysis.priceImpacts) {
|
|
873
|
+
console.log('');
|
|
874
|
+
console.log('Price impact by USD size:');
|
|
875
|
+
analysis.usdSizes.forEach((usd, index) => {
|
|
876
|
+
const buy = analysis.priceImpacts.buyPct[index];
|
|
877
|
+
const sell = analysis.priceImpacts.sellPct[index];
|
|
878
|
+
const fmtPct = (value) => `${value >= 0 ? '+' : ''}${value.toFixed(5)}%`;
|
|
879
|
+
console.log(` $${usd.toLocaleString()}: buy ${fmtPct(buy)} sell ${fmtPct(sell)}`);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
main().catch((err) => {
|
|
885
|
+
console.error(err?.stack || err?.message || String(err));
|
|
886
|
+
process.exit(1);
|
|
887
|
+
});
|