@steerprotocol/liquidity-meter 1.0.0 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -56
- package/bin/liquidity-depth.js +795 -0
- package/package.json +42 -32
- package/src/api.js +568 -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 +506 -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/src/depth.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/* eslint-disable no-bitwise */
|
|
2
|
+
|
|
3
|
+
// Market-depth bands for a Uniswap v3 pool snapshot.
|
|
4
|
+
// Dependency-free, uses BigInt for exact fixed-point arithmetic (Q64.96).
|
|
5
|
+
|
|
6
|
+
/////////////////////////// Fixed-point + constants ///////////////////////////
|
|
7
|
+
|
|
8
|
+
const Q32 = 1n << 32n;
|
|
9
|
+
const Q96 = 1n << 96n;
|
|
10
|
+
const MAX_UINT256 = (1n << 256n) - 1n;
|
|
11
|
+
|
|
12
|
+
const MIN_TICK = -887272;
|
|
13
|
+
const MAX_TICK = 887272;
|
|
14
|
+
const MIN_SQRT_RATIO = 4295128739n; // ~ TickMath.MIN_SQRT_RATIO
|
|
15
|
+
const MAX_SQRT_RATIO =
|
|
16
|
+
1461446703485210103287273052203988822378723970342n; // ~ TickMath.MAX_SQRT_RATIO
|
|
17
|
+
|
|
18
|
+
function mulDiv(a, b, d) {
|
|
19
|
+
return (a * b) / d;
|
|
20
|
+
}
|
|
21
|
+
function mulDivRoundingUp(a, b, d) {
|
|
22
|
+
const prod = a * b;
|
|
23
|
+
const q = prod / d;
|
|
24
|
+
return prod % d === 0n ? q : q + 1n;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//////////////////////////// TickMath (v3 exact) //////////////////////////////
|
|
28
|
+
|
|
29
|
+
// getSqrtRatioAtTick — direct port of UniswapV3 TickMath.getSqrtRatioAtTick
|
|
30
|
+
// Returns Q64.96 sqrt price at the LOWER boundary of `tick`.
|
|
31
|
+
export function getSqrtRatioAtTick(tick) {
|
|
32
|
+
if (tick < MIN_TICK || tick > MAX_TICK) throw new Error("TICK_OUT_OF_RANGE");
|
|
33
|
+
|
|
34
|
+
let absTick = tick < 0 ? -tick : tick;
|
|
35
|
+
|
|
36
|
+
// 1.0001^(2^i) constants in Q128.128
|
|
37
|
+
let ratio =
|
|
38
|
+
(absTick & 0x1) !== 0
|
|
39
|
+
? 0xfffcb933bd6fad37aa2d162d1a594001n
|
|
40
|
+
: 0x100000000000000000000000000000000n;
|
|
41
|
+
if ((absTick & 0x2) !== 0)
|
|
42
|
+
ratio = (ratio * 0xfff97272373d413259a46990580e213an) >> 128n;
|
|
43
|
+
if ((absTick & 0x4) !== 0)
|
|
44
|
+
ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdccn) >> 128n;
|
|
45
|
+
if ((absTick & 0x8) !== 0)
|
|
46
|
+
ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0n) >> 128n;
|
|
47
|
+
if ((absTick & 0x10) !== 0)
|
|
48
|
+
ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644n) >> 128n;
|
|
49
|
+
if ((absTick & 0x20) !== 0)
|
|
50
|
+
ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0n) >> 128n;
|
|
51
|
+
if ((absTick & 0x40) !== 0)
|
|
52
|
+
ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861n) >> 128n;
|
|
53
|
+
if ((absTick & 0x80) !== 0)
|
|
54
|
+
ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053n) >> 128n;
|
|
55
|
+
if ((absTick & 0x100) !== 0)
|
|
56
|
+
ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4n) >> 128n;
|
|
57
|
+
if ((absTick & 0x200) !== 0)
|
|
58
|
+
ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54n) >> 128n;
|
|
59
|
+
if ((absTick & 0x400) !== 0)
|
|
60
|
+
ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3n) >> 128n;
|
|
61
|
+
if ((absTick & 0x800) !== 0)
|
|
62
|
+
ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9n) >> 128n;
|
|
63
|
+
if ((absTick & 0x1000) !== 0)
|
|
64
|
+
ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825n) >> 128n;
|
|
65
|
+
if ((absTick & 0x2000) !== 0)
|
|
66
|
+
ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5n) >> 128n;
|
|
67
|
+
if ((absTick & 0x4000) !== 0)
|
|
68
|
+
ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7n) >> 128n;
|
|
69
|
+
if ((absTick & 0x8000) !== 0)
|
|
70
|
+
ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6n) >> 128n;
|
|
71
|
+
if ((absTick & 0x10000) !== 0)
|
|
72
|
+
ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9n) >> 128n;
|
|
73
|
+
if ((absTick & 0x20000) !== 0)
|
|
74
|
+
ratio = (ratio * 0x5d6af8dedb81196699c329225ee604n) >> 128n;
|
|
75
|
+
if ((absTick & 0x40000) !== 0)
|
|
76
|
+
ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98n) >> 128n;
|
|
77
|
+
if ((absTick & 0x80000) !== 0)
|
|
78
|
+
ratio = (ratio * 0x48a170391f7dc42444e8fa2n) >> 128n;
|
|
79
|
+
|
|
80
|
+
if (tick > 0) {
|
|
81
|
+
ratio = MAX_UINT256 / ratio;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// round up to guarantee that getTickAtSqrtRatio(sqrtRatioAtTick(tick)) == tick
|
|
85
|
+
const sqrtPriceX96 = (ratio >> 32n) + (ratio % Q32 === 0n ? 0n : 1n);
|
|
86
|
+
return sqrtPriceX96;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//////////////////////////// Amount delta helpers /////////////////////////////
|
|
90
|
+
|
|
91
|
+
// Token1 amount for price move from sqrtA -> sqrtB (A < B), no fee. Q64.96
|
|
92
|
+
// amount1 = L * (sqrtB - sqrtA) / Q96
|
|
93
|
+
function amount1DeltaUp(sqrtA, sqrtB, liquidity, roundUp = true) {
|
|
94
|
+
if (sqrtB < sqrtA) [sqrtA, sqrtB] = [sqrtB, sqrtA];
|
|
95
|
+
const num = liquidity * (sqrtB - sqrtA);
|
|
96
|
+
return roundUp ? mulDivRoundingUp(num, 1n, Q96) : mulDiv(num, 1n, Q96);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Token0 amount for price move from sqrtA -> sqrtB (A < B), no fee.
|
|
100
|
+
// amount0 = L * (sqrtB - sqrtA) * Q96 / (sqrtB * sqrtA)
|
|
101
|
+
// For downward price move (sell), call with (lower, upper) = (target, current).
|
|
102
|
+
function amount0DeltaDown(sqrtA, sqrtB, liquidity, roundUp = true) {
|
|
103
|
+
if (sqrtB < sqrtA) [sqrtA, sqrtB] = [sqrtB, sqrtA];
|
|
104
|
+
const num = liquidity * (sqrtB - sqrtA) * Q96;
|
|
105
|
+
const den = sqrtB * sqrtA;
|
|
106
|
+
return roundUp ? mulDivRoundingUp(num, 1n, den) : mulDiv(num, 1n, den);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Gross-up an input amount to include Uniswap fee (feePips in 1e6 units).
|
|
110
|
+
function grossUpForFee(amountIn, feePips) {
|
|
111
|
+
if (!feePips) return amountIn;
|
|
112
|
+
const feeDen = 1_000_000n;
|
|
113
|
+
const den = feeDen - BigInt(feePips);
|
|
114
|
+
return mulDivRoundingUp(amountIn, feeDen, den);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//////////////////////////// Utilities ////////////////////////////////////////
|
|
118
|
+
|
|
119
|
+
function toUSDFromRaw(amount, decimals, usd) {
|
|
120
|
+
if (!usd || usd <= 0) return 0;
|
|
121
|
+
const scale = 10n ** BigInt(Math.max(0, decimals));
|
|
122
|
+
const usdMicros = BigInt(Math.round(usd * 1e6));
|
|
123
|
+
const micros = (amount * usdMicros) / scale; // BigInt of USD microdollars
|
|
124
|
+
return Number(micros) / 1e6;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clamp(val, lo, hi) {
|
|
128
|
+
return Math.max(lo, Math.min(hi, val));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Binary search helpers on pre-sorted initialized ticks
|
|
132
|
+
function firstTickIndexGreaterThan(ticks, t) {
|
|
133
|
+
let lo = 0,
|
|
134
|
+
hi = ticks.length;
|
|
135
|
+
while (lo < hi) {
|
|
136
|
+
const mid = (lo + hi) >> 1;
|
|
137
|
+
if (ticks[mid].index <= t) lo = mid + 1;
|
|
138
|
+
else hi = mid;
|
|
139
|
+
}
|
|
140
|
+
return lo; // first index with ticks[i].index > t (may be ticks.length)
|
|
141
|
+
}
|
|
142
|
+
function lastTickIndexAtOrBelow(ticks, t) {
|
|
143
|
+
const i = firstTickIndexGreaterThan(ticks, t) - 1;
|
|
144
|
+
return i;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//////////////////////////// Core: integrate to targets ///////////////////////
|
|
148
|
+
|
|
149
|
+
// Convert a percent move (e.g., +2%) to an approximate tick delta.
|
|
150
|
+
// 1 tick ~= 0.01% in price -> but we use exact log(1.0001)
|
|
151
|
+
function percentToTickDelta(pct) {
|
|
152
|
+
const r = 1 + pct / 100; // price multiplier
|
|
153
|
+
const delta = Math.log(r) / Math.log(1.0001);
|
|
154
|
+
// round toward nearest integer (small effect; bucketed charts are tolerant)
|
|
155
|
+
return Math.max(1, Math.round(delta));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Integrate upward (price ↑): token1 in to move sqrtPrice to target
|
|
159
|
+
function integrateUp(
|
|
160
|
+
sqrtStart,
|
|
161
|
+
currentTick,
|
|
162
|
+
Lstart,
|
|
163
|
+
targetTick,
|
|
164
|
+
feePips,
|
|
165
|
+
ticks
|
|
166
|
+
) {
|
|
167
|
+
let s = sqrtStart;
|
|
168
|
+
let L = Lstart;
|
|
169
|
+
let tick = currentTick;
|
|
170
|
+
|
|
171
|
+
let i = firstTickIndexGreaterThan(ticks, tick); // first initialized tick above current
|
|
172
|
+
let targetSqrt = getSqrtRatioAtTick(clamp(targetTick, MIN_TICK, MAX_TICK));
|
|
173
|
+
let acc = 0n;
|
|
174
|
+
|
|
175
|
+
while (s < targetSqrt) {
|
|
176
|
+
const nextTick = i < 0 || i >= ticks.length ? null : ticks[i];
|
|
177
|
+
const sqrtAtNext = nextTick ? getSqrtRatioAtTick(nextTick.index) : MAX_SQRT_RATIO;
|
|
178
|
+
const bound = sqrtAtNext < targetSqrt ? sqrtAtNext : targetSqrt;
|
|
179
|
+
|
|
180
|
+
// token1 in over [s, bound]
|
|
181
|
+
const dy = amount1DeltaUp(s, bound, L, true);
|
|
182
|
+
const dyGross = grossUpForFee(dy, feePips);
|
|
183
|
+
acc += dyGross;
|
|
184
|
+
s = bound;
|
|
185
|
+
|
|
186
|
+
if (s === sqrtAtNext && nextTick) {
|
|
187
|
+
// crossing upward → add liquidityNet
|
|
188
|
+
L = L + nextTick.liquidityNet;
|
|
189
|
+
tick = nextTick.index;
|
|
190
|
+
i += 1;
|
|
191
|
+
} else {
|
|
192
|
+
break; // hit target
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { token1In: acc, finalSqrt: s, finalTick: tick, finalL: L };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Integrate downward (price ↓): token0 in to move sqrtPrice to target
|
|
200
|
+
function integrateDown(
|
|
201
|
+
sqrtStart,
|
|
202
|
+
currentTick,
|
|
203
|
+
Lstart,
|
|
204
|
+
targetTick,
|
|
205
|
+
feePips,
|
|
206
|
+
ticks
|
|
207
|
+
) {
|
|
208
|
+
let s = sqrtStart;
|
|
209
|
+
let L = Lstart;
|
|
210
|
+
let tick = currentTick;
|
|
211
|
+
|
|
212
|
+
let j = lastTickIndexAtOrBelow(ticks, tick); // initialized tick at/below current
|
|
213
|
+
let targetSqrt = getSqrtRatioAtTick(clamp(targetTick, MIN_TICK, MAX_TICK));
|
|
214
|
+
let acc = 0n;
|
|
215
|
+
|
|
216
|
+
while (s > targetSqrt) {
|
|
217
|
+
const prevTick = j >= 0 ? ticks[j] : null;
|
|
218
|
+
const sqrtAtPrev = prevTick ? getSqrtRatioAtTick(prevTick.index) : MIN_SQRT_RATIO;
|
|
219
|
+
const bound = sqrtAtPrev > targetSqrt ? sqrtAtPrev : targetSqrt;
|
|
220
|
+
|
|
221
|
+
// token0 in over [bound, s] (note lower, upper)
|
|
222
|
+
const dx = amount0DeltaDown(bound, s, L, true);
|
|
223
|
+
const dxGross = grossUpForFee(dx, feePips);
|
|
224
|
+
acc += dxGross;
|
|
225
|
+
s = bound;
|
|
226
|
+
|
|
227
|
+
if (s === sqrtAtPrev && prevTick) {
|
|
228
|
+
// crossing downward → subtract liquidityNet
|
|
229
|
+
L = L - prevTick.liquidityNet;
|
|
230
|
+
tick = prevTick.index - 1; // inside the lower tick now
|
|
231
|
+
j -= 1;
|
|
232
|
+
} else {
|
|
233
|
+
break; // hit target
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { token0In: acc, finalSqrt: s, finalTick: tick, finalL: L };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
//////////////////////////// Public API //////////////////////////////////////
|
|
240
|
+
|
|
241
|
+
// Compute stacked depth bands for ±{1,2,5,10}% (defaults) at a snapshot.
|
|
242
|
+
export function computeMarketDepthBands(params) {
|
|
243
|
+
const {
|
|
244
|
+
sqrtPriceX96,
|
|
245
|
+
tick,
|
|
246
|
+
liquidity,
|
|
247
|
+
feePips = 0,
|
|
248
|
+
ticks,
|
|
249
|
+
token0,
|
|
250
|
+
token1,
|
|
251
|
+
percentBuckets = [1, 2, 5, 10],
|
|
252
|
+
} = params;
|
|
253
|
+
|
|
254
|
+
// safety: ensure ticks sorted asc
|
|
255
|
+
const sortedTicks = [...ticks].sort((a, b) => a.index - b.index);
|
|
256
|
+
|
|
257
|
+
// Build target ticks for each bucket
|
|
258
|
+
const upTargets = percentBuckets.map((p) =>
|
|
259
|
+
clamp(tick + percentToTickDelta(p), MIN_TICK, MAX_TICK)
|
|
260
|
+
);
|
|
261
|
+
const downTargets = percentBuckets.map((p) =>
|
|
262
|
+
clamp(tick - percentToTickDelta(p), MIN_TICK, MAX_TICK)
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Integrate cumulatively to each target
|
|
266
|
+
const buyCumToken1 = [];
|
|
267
|
+
const sellCumToken0 = [];
|
|
268
|
+
|
|
269
|
+
// Upward cumulative
|
|
270
|
+
let sU = sqrtPriceX96;
|
|
271
|
+
let tU = tick;
|
|
272
|
+
let LU = liquidity;
|
|
273
|
+
for (const tgt of upTargets) {
|
|
274
|
+
const { token1In, finalSqrt, finalTick, finalL } = integrateUp(
|
|
275
|
+
sU,
|
|
276
|
+
tU,
|
|
277
|
+
LU,
|
|
278
|
+
tgt,
|
|
279
|
+
feePips,
|
|
280
|
+
sortedTicks
|
|
281
|
+
);
|
|
282
|
+
const cum = (buyCumToken1.length ? buyCumToken1[buyCumToken1.length - 1] : 0n) + token1In;
|
|
283
|
+
buyCumToken1.push(cum);
|
|
284
|
+
sU = finalSqrt;
|
|
285
|
+
tU = finalTick;
|
|
286
|
+
LU = finalL;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Downward cumulative
|
|
290
|
+
let sD = sqrtPriceX96;
|
|
291
|
+
let tD = tick;
|
|
292
|
+
let LD = liquidity;
|
|
293
|
+
for (const tgt of downTargets) {
|
|
294
|
+
const { token0In, finalSqrt, finalTick, finalL } = integrateDown(
|
|
295
|
+
sD,
|
|
296
|
+
tD,
|
|
297
|
+
LD,
|
|
298
|
+
tgt,
|
|
299
|
+
feePips,
|
|
300
|
+
sortedTicks
|
|
301
|
+
);
|
|
302
|
+
const cum = (sellCumToken0.length ? sellCumToken0[sellCumToken0.length - 1] : 0n) + token0In;
|
|
303
|
+
sellCumToken0.push(cum);
|
|
304
|
+
sD = finalSqrt;
|
|
305
|
+
tD = finalTick;
|
|
306
|
+
LD = finalL;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Convert to USD notionals using input token for each side
|
|
310
|
+
const buyCumUSD = buyCumToken1.map((a) =>
|
|
311
|
+
toUSDFromRaw(a, token1.decimals, token1.usdPrice)
|
|
312
|
+
);
|
|
313
|
+
const sellCumUSD = sellCumToken0.map((a) =>
|
|
314
|
+
toUSDFromRaw(a, token0.decimals, token0.usdPrice)
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Stacked bands (diffs of cumulative)
|
|
318
|
+
const toBands = (cum) => [
|
|
319
|
+
cum[0],
|
|
320
|
+
cum[1] - cum[0],
|
|
321
|
+
cum[2] - cum[1],
|
|
322
|
+
cum[3] - cum[2],
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
const buyBandsUSD = toBands(buyCumUSD);
|
|
326
|
+
const sellBandsUSD = toBands(sellCumUSD);
|
|
327
|
+
|
|
328
|
+
// Headline +/− 2%
|
|
329
|
+
const idx2 = percentBuckets.findIndex((p) => p === 2);
|
|
330
|
+
const depthPlus2USD = idx2 >= 0 ? buyCumUSD[idx2] : NaN;
|
|
331
|
+
const depthMinus2USD = idx2 >= 0 ? sellCumUSD[idx2] : NaN;
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
buyCumUSD,
|
|
335
|
+
sellCumUSD,
|
|
336
|
+
buyBandsUSD,
|
|
337
|
+
sellBandsUSD,
|
|
338
|
+
depthPlus2USD,
|
|
339
|
+
depthMinus2USD,
|
|
340
|
+
buyCumToken1,
|
|
341
|
+
sellCumToken0,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export const DepthInternals = {
|
|
346
|
+
MIN_TICK,
|
|
347
|
+
MAX_TICK,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
//////////////////////////// Price impact by size /////////////////////////////
|
|
351
|
+
|
|
352
|
+
function integrateUpByAmount(
|
|
353
|
+
sqrtStart,
|
|
354
|
+
currentTick,
|
|
355
|
+
Lstart,
|
|
356
|
+
budgetGross, // token1 gross in
|
|
357
|
+
feePips,
|
|
358
|
+
ticks
|
|
359
|
+
) {
|
|
360
|
+
let s = sqrtStart;
|
|
361
|
+
let L = Lstart;
|
|
362
|
+
let tick = currentTick;
|
|
363
|
+
let i = firstTickIndexGreaterThan(ticks, tick);
|
|
364
|
+
const feeDen = 1_000_000n;
|
|
365
|
+
const den = feeDen - BigInt(feePips || 0);
|
|
366
|
+
|
|
367
|
+
while (budgetGross > 0n && s < MAX_SQRT_RATIO) {
|
|
368
|
+
const nextTick = i < 0 || i >= ticks.length ? null : ticks[i];
|
|
369
|
+
const sqrtAtNext = nextTick ? getSqrtRatioAtTick(nextTick.index) : MAX_SQRT_RATIO;
|
|
370
|
+
|
|
371
|
+
const dyNetToNext = amount1DeltaUp(s, sqrtAtNext, L, true);
|
|
372
|
+
const dyGrossToNext = grossUpForFee(dyNetToNext, feePips || 0);
|
|
373
|
+
|
|
374
|
+
if (budgetGross < dyGrossToNext) {
|
|
375
|
+
// Partial step within this segment. Compute net from gross, then delta sqrt.
|
|
376
|
+
const netIn = mulDiv(budgetGross, den, feeDen);
|
|
377
|
+
const deltaS = mulDivRoundingUp(netIn, Q96, L);
|
|
378
|
+
s = s + deltaS;
|
|
379
|
+
budgetGross = 0n;
|
|
380
|
+
break;
|
|
381
|
+
} else {
|
|
382
|
+
budgetGross -= dyGrossToNext;
|
|
383
|
+
s = sqrtAtNext;
|
|
384
|
+
if (nextTick) {
|
|
385
|
+
L = L + nextTick.liquidityNet;
|
|
386
|
+
tick = nextTick.index;
|
|
387
|
+
i += 1;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { finalSqrt: s, finalTick: tick, finalL: L };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function integrateDownByAmount(
|
|
395
|
+
sqrtStart,
|
|
396
|
+
currentTick,
|
|
397
|
+
Lstart,
|
|
398
|
+
budgetGross, // token0 gross in
|
|
399
|
+
feePips,
|
|
400
|
+
ticks
|
|
401
|
+
) {
|
|
402
|
+
let s = sqrtStart;
|
|
403
|
+
let L = Lstart;
|
|
404
|
+
let tick = currentTick;
|
|
405
|
+
let j = lastTickIndexAtOrBelow(ticks, tick);
|
|
406
|
+
const feeDen = 1_000_000n;
|
|
407
|
+
const den = feeDen - BigInt(feePips || 0);
|
|
408
|
+
|
|
409
|
+
while (budgetGross > 0n && s > MIN_SQRT_RATIO) {
|
|
410
|
+
const prevTick = j >= 0 ? ticks[j] : null;
|
|
411
|
+
const sqrtAtPrev = prevTick ? getSqrtRatioAtTick(prevTick.index) : MIN_SQRT_RATIO;
|
|
412
|
+
|
|
413
|
+
const dxNetToPrev = amount0DeltaDown(sqrtAtPrev, s, L, true);
|
|
414
|
+
const dxGrossToPrev = grossUpForFee(dxNetToPrev, feePips || 0);
|
|
415
|
+
|
|
416
|
+
if (budgetGross < dxGrossToPrev) {
|
|
417
|
+
// Partial step within this segment. Solve bound:
|
|
418
|
+
// bound = s * L*Q96 / (L*Q96 + netIn*s)
|
|
419
|
+
const netIn = mulDiv(budgetGross, den, feeDen);
|
|
420
|
+
const LQ = L * Q96;
|
|
421
|
+
const num = LQ * s;
|
|
422
|
+
const den2 = LQ + netIn * s;
|
|
423
|
+
const bound = den2 === 0n ? MIN_SQRT_RATIO : mulDiv(num, 1n, den2);
|
|
424
|
+
s = bound;
|
|
425
|
+
budgetGross = 0n;
|
|
426
|
+
break;
|
|
427
|
+
} else {
|
|
428
|
+
budgetGross -= dxGrossToPrev;
|
|
429
|
+
s = sqrtAtPrev;
|
|
430
|
+
if (prevTick) {
|
|
431
|
+
L = L - prevTick.liquidityNet;
|
|
432
|
+
tick = prevTick.index - 1;
|
|
433
|
+
j -= 1;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return { finalSqrt: s, finalTick: tick, finalL: L };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function priceImpactPercent(startSqrt, endSqrt) {
|
|
441
|
+
// percent = ((b^2 - a^2)/a^2) * 100 = ((b-a)(b+a)/a^2)*100
|
|
442
|
+
const a = startSqrt;
|
|
443
|
+
const b = endSqrt;
|
|
444
|
+
const scale = 1_000_000n; // 1e6 -> return percent with 6 decimals as float
|
|
445
|
+
const numRaw = (b - a) * (b + a) * 100n * scale;
|
|
446
|
+
const den = a * a;
|
|
447
|
+
// signed rounding toward nearest
|
|
448
|
+
let val;
|
|
449
|
+
if (numRaw >= 0n) val = (numRaw + den / 2n) / den;
|
|
450
|
+
else val = -((-numRaw + den / 2n) / den);
|
|
451
|
+
return Number(val) / Number(scale);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function computePriceImpactsBySizes({
|
|
455
|
+
sqrtPriceX96,
|
|
456
|
+
tick,
|
|
457
|
+
liquidity,
|
|
458
|
+
feePips = 0,
|
|
459
|
+
ticks,
|
|
460
|
+
buySizesToken1, // bigint[] gross token1 (buy/up)
|
|
461
|
+
sellSizesToken0, // bigint[] gross token0 (sell/down)
|
|
462
|
+
}) {
|
|
463
|
+
const sortedTicks = [...ticks].sort((a, b) => a.index - b.index);
|
|
464
|
+
|
|
465
|
+
const buyPct = [];
|
|
466
|
+
for (const size of buySizesToken1) {
|
|
467
|
+
const { finalSqrt } = integrateUpByAmount(
|
|
468
|
+
sqrtPriceX96,
|
|
469
|
+
tick,
|
|
470
|
+
liquidity,
|
|
471
|
+
size,
|
|
472
|
+
feePips,
|
|
473
|
+
sortedTicks
|
|
474
|
+
);
|
|
475
|
+
buyPct.push(priceImpactPercent(sqrtPriceX96, finalSqrt));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const sellPct = [];
|
|
479
|
+
for (const size of sellSizesToken0) {
|
|
480
|
+
const { finalSqrt } = integrateDownByAmount(
|
|
481
|
+
sqrtPriceX96,
|
|
482
|
+
tick,
|
|
483
|
+
liquidity,
|
|
484
|
+
size,
|
|
485
|
+
feePips,
|
|
486
|
+
sortedTicks
|
|
487
|
+
);
|
|
488
|
+
sellPct.push(priceImpactPercent(sqrtPriceX96, finalSqrt));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return { buyPct, sellPct };
|
|
492
|
+
}
|
package/src/index.js
ADDED
package/src/prices.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createPublicClient, http } from 'viem';
|
|
3
|
+
|
|
4
|
+
function norm(addr) {
|
|
5
|
+
return addr.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function dexSlugFromChainId(chainId) {
|
|
9
|
+
switch (Number(chainId)) {
|
|
10
|
+
case 1: return 'ethereum';
|
|
11
|
+
case 10: return 'optimism';
|
|
12
|
+
case 56: return 'bsc';
|
|
13
|
+
case 137: return 'polygon';
|
|
14
|
+
case 8453: return 'base';
|
|
15
|
+
case 42161: return 'arbitrum';
|
|
16
|
+
case 43114: return 'avalanche';
|
|
17
|
+
case 100: return 'gnosis';
|
|
18
|
+
case 324: return 'zksync';
|
|
19
|
+
case 1101: return 'polygon-zkevm';
|
|
20
|
+
default: return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function fetchPricesLlama(addresses) {
|
|
25
|
+
const uniq = Array.from(new Set(addresses.map(norm)));
|
|
26
|
+
if (!uniq.length) return {};
|
|
27
|
+
const coinsParam = uniq.map((a) => `ethereum:${a}`).join(',');
|
|
28
|
+
const url = `https://coins.llama.fi/prices/current/${coinsParam}`;
|
|
29
|
+
const res = await axios.get(url, { timeout: 8000, maxRedirects: 3 });
|
|
30
|
+
const coins = res.data?.coins || {};
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const a of uniq) {
|
|
33
|
+
const key = `ethereum:${a}`;
|
|
34
|
+
const p = coins[key]?.price;
|
|
35
|
+
if (typeof p === 'number' && Number.isFinite(p)) out[a] = p;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function fetchPricesCoingecko(addresses, apiKey) {
|
|
41
|
+
const uniq = Array.from(new Set(addresses.map(norm)));
|
|
42
|
+
if (!uniq.length) return {};
|
|
43
|
+
const url = 'https://api.coingecko.com/api/v3/simple/token_price/ethereum';
|
|
44
|
+
const params = {
|
|
45
|
+
contract_addresses: uniq.join(','),
|
|
46
|
+
vs_currencies: 'usd',
|
|
47
|
+
};
|
|
48
|
+
const headers = apiKey ? { 'x-cg-pro-api-key': apiKey } : undefined;
|
|
49
|
+
const res = await axios.get(url, { params, headers, timeout: 8000, maxRedirects: 3 });
|
|
50
|
+
const body = res.data || {};
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const a of uniq) {
|
|
53
|
+
const ent = body[a];
|
|
54
|
+
const p = ent?.usd;
|
|
55
|
+
if (typeof p === 'number' && Number.isFinite(p)) out[a] = p;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function fetchTokenUSDPrices({ addresses, source = 'auto', rpcUrl, chainId, debug = false, reserveLimit = 100 }) {
|
|
61
|
+
const uniq = Array.from(new Set(addresses.map(norm)));
|
|
62
|
+
const byAddress = {};
|
|
63
|
+
const coingeckoKey = process.env.COINGECKO_API_KEY;
|
|
64
|
+
const envRpc = rpcUrl || process.env.RPC_URL;
|
|
65
|
+
|
|
66
|
+
async function resolveChainId(rpcUrl) {
|
|
67
|
+
try {
|
|
68
|
+
if (!rpcUrl) return undefined;
|
|
69
|
+
const client = createPublicClient({ transport: http(rpcUrl) });
|
|
70
|
+
return await client.getChainId();
|
|
71
|
+
} catch (_) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function fetchPricesReserve({ addresses, chainId, limit = 100 }) {
|
|
77
|
+
if (!chainId) return {};
|
|
78
|
+
const url = `https://api.reserve.org/discover/dtf`;
|
|
79
|
+
const params = { chainId, limit };
|
|
80
|
+
const res = await axios.get(url, { params, timeout: 8000, maxRedirects: 3 });
|
|
81
|
+
const arr = Array.isArray(res.data) ? res.data : [];
|
|
82
|
+
const out = {};
|
|
83
|
+
const set = new Set(addresses.map(norm));
|
|
84
|
+
for (const ent of arr) {
|
|
85
|
+
const addr = norm(ent.address || '');
|
|
86
|
+
if (set.has(addr)) {
|
|
87
|
+
const p = Number(ent.price);
|
|
88
|
+
if (Number.isFinite(p)) out[addr] = p;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (debug) {
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.error(`[prices] reserve: chainId=${chainId} limit=${limit} returned=${arr.length} matched=${Object.keys(out).length}`);
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function fetchPricesDexscreener({ addresses, chainId }) {
|
|
99
|
+
if (!chainId) return {};
|
|
100
|
+
const slug = dexSlugFromChainId(chainId);
|
|
101
|
+
if (!slug) return {};
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const addr of addresses.map(norm)) {
|
|
104
|
+
try {
|
|
105
|
+
const url = `https://api.dexscreener.com/token-pairs/v1/${slug}/${addr}`;
|
|
106
|
+
const res = await axios.get(url, { timeout: 8000, maxRedirects: 3 });
|
|
107
|
+
const arr = Array.isArray(res.data) ? res.data : [];
|
|
108
|
+
// choose entry where token is baseToken; pick highest liquidity.usd
|
|
109
|
+
let best = null;
|
|
110
|
+
for (const ent of arr) {
|
|
111
|
+
const base = norm(ent?.baseToken?.address || '');
|
|
112
|
+
if (base !== addr) continue;
|
|
113
|
+
const liq = Number(ent?.liquidity?.usd ?? 0);
|
|
114
|
+
if (!best || liq > (Number(best?.liquidity?.usd ?? 0))) best = ent;
|
|
115
|
+
}
|
|
116
|
+
if (best) {
|
|
117
|
+
const p = Number(best.priceUsd);
|
|
118
|
+
if (Number.isFinite(p)) out[addr] = p;
|
|
119
|
+
if (debug) {
|
|
120
|
+
// eslint-disable-next-line no-console
|
|
121
|
+
console.error(`[prices] dexscreener: addr=${addr} pairs=${arr.length} bestLiq=${best?.liquidity?.usd ?? 0} priceUsd=${best?.priceUsd ?? 'n/a'}`);
|
|
122
|
+
}
|
|
123
|
+
} else if (debug) {
|
|
124
|
+
// eslint-disable-next-line no-console
|
|
125
|
+
console.error(`[prices] dexscreener: addr=${addr} no baseToken match in ${arr.length} pairs`);
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
if (debug) {
|
|
129
|
+
// eslint-disable-next-line no-console
|
|
130
|
+
console.error(`[prices] dexscreener error for ${addr}: ${e?.message || e}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function tryLlama() {
|
|
138
|
+
try {
|
|
139
|
+
const p = await fetchPricesLlama(uniq);
|
|
140
|
+
for (const k of Object.keys(p)) if (byAddress[k] == null) byAddress[k] = { price: p[k], source: 'llama' };
|
|
141
|
+
return Object.keys(p).length > 0;
|
|
142
|
+
} catch (_) { return false; }
|
|
143
|
+
}
|
|
144
|
+
async function tryReserve(chainId) {
|
|
145
|
+
try {
|
|
146
|
+
if (!chainId) return false;
|
|
147
|
+
const p = await fetchPricesReserve({ addresses: uniq, chainId, limit: reserveLimit });
|
|
148
|
+
for (const k of Object.keys(p)) if (byAddress[k] == null) byAddress[k] = { price: p[k], source: 'reserve' };
|
|
149
|
+
return Object.keys(p).length > 0;
|
|
150
|
+
} catch (_) { return false; }
|
|
151
|
+
}
|
|
152
|
+
async function tryDex(chainId) {
|
|
153
|
+
try {
|
|
154
|
+
if (!chainId) return false;
|
|
155
|
+
const p = await fetchPricesDexscreener({ addresses: uniq, chainId });
|
|
156
|
+
for (const k of Object.keys(p)) if (byAddress[k] == null) byAddress[k] = { price: p[k], source: 'dexscreener' };
|
|
157
|
+
return Object.keys(p).length > 0;
|
|
158
|
+
} catch (_) { return false; }
|
|
159
|
+
}
|
|
160
|
+
async function tryCG() {
|
|
161
|
+
try {
|
|
162
|
+
const p = await fetchPricesCoingecko(uniq, coingeckoKey);
|
|
163
|
+
for (const k of Object.keys(p)) if (byAddress[k] == null) byAddress[k] = { price: p[k], source: 'coingecko' };
|
|
164
|
+
return Object.keys(p).length > 0;
|
|
165
|
+
} catch (_) { return false; }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Resolve chainId if needed for reserve/dexscreener
|
|
169
|
+
let cid = chainId;
|
|
170
|
+
if (!cid && (source === 'reserve' || source === 'dexscreener' || source === 'auto')) cid = await resolveChainId(envRpc);
|
|
171
|
+
if (debug) {
|
|
172
|
+
// eslint-disable-next-line no-console
|
|
173
|
+
console.error(`[prices] source=${source} rpc=${envRpc ? 'yes' : 'no'} chainId=${cid ?? 'n/a'} addresses=${uniq.length}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (source === 'reserve') await tryReserve(cid);
|
|
177
|
+
else if (source === 'dexscreener') await tryDex(cid);
|
|
178
|
+
else if (source === 'llama') await tryLlama();
|
|
179
|
+
else if (source === 'coingecko') await tryCG();
|
|
180
|
+
else {
|
|
181
|
+
// auto: try reserve (if chainId), then dexscreener (if chainId), then llama, then CG
|
|
182
|
+
await tryReserve(cid);
|
|
183
|
+
await tryDex(cid);
|
|
184
|
+
await tryLlama();
|
|
185
|
+
await tryCG();
|
|
186
|
+
}
|
|
187
|
+
return { byAddress };
|
|
188
|
+
}
|