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