@panoptic-eng/sdk 1.0.46 → 1.0.48
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/dist/cow/index.js +48 -4
- package/dist/cow/index.js.map +1 -1
- package/dist/{cow-64lD-1vF.js → cow-BPcuMjYR.js} +1 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +203 -162
- package/dist/index.js.map +1 -1
- package/dist/{irm-Cg08pWot.js → irm-C7QhFiwL.js} +7 -246
- package/dist/panoptic/v2/index.d.ts +167 -32
- package/dist/panoptic/v2/index.d.ts.map +1 -1
- package/dist/panoptic/v2/index.js +1562 -930
- package/dist/panoptic/v2/index.js.map +1 -1
- package/dist/panoptic/v2/react-public.d.ts +167 -32
- package/dist/panoptic/v2/react-public.d.ts.map +1 -1
- package/dist/panoptic/v2/react-public.js +1164 -528
- package/dist/panoptic/v2/react-public.js.map +1 -1
- package/dist/{getTrackedPositionIds-BwBtLpz-.js → rates-5oQD-SuM.js} +256 -4
- package/dist/{router-DR1NpQhi.js → router-C26q7i8w.js} +2 -2
- package/dist/{router-Di45CFq9.js → router-ClhAkKjh.js} +1 -1
- package/dist/uniswap/index.js +59 -15
- package/dist/uniswap/index.js.map +1 -1
- package/dist/{v2-BYlZLfDi.js → v2-DVcophLf.js} +1379 -2671
- package/dist/{writes-CmRf1Mok.js → writes-DBHwfbDD.js} +2855 -944
- package/dist/zodiac/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { StateViewAbi,
|
|
2
|
-
import { AccountInsolventError, ChunkLimitError, DEFAULT_VEGOID, InvalidHistoryRangeError,
|
|
3
|
-
import { MAX_TICK, MAX_TRACKED_CHUNKS, MIN_TICK, NO_LOWER_LIQUIDATION_TICK, NO_UPPER_LIQUIDATION_TICK, WAD, calculatePositionDelta, calculatePositionGamma, calculatePositionValue, priceToTick,
|
|
4
|
-
import { addLegToTokenId, buildBatchDispatchArgs,
|
|
1
|
+
import { StateViewAbi, getAccountCollateral, panopticQueryAbi, readBlockAndAggregate, requireReturnData } from "./irm-C7QhFiwL.js";
|
|
2
|
+
import { AccountInsolventError, ChunkLimitError, DEFAULT_VEGOID, InvalidHistoryRangeError, NotEnoughTokensError, PanopticError, PanopticValidationError, SafeModeError, StaleDataError, StorageDataNotFoundError, UnhealthyPoolError, WrongUniswapPoolError, collateralTrackerV2Abi, decodeLeftRightSigned, decodeLeftRightUnsigned, decodePositionBalance, getBlockMeta, getClosedPositionsKey, getPendingPositionsKey, getPool, getPoolMetaKey, getPositionMetaKey, getPositionsKey, getTrackedChunksKey, getTrackedPositionIds, jsonSerializer, mintableAfterBuffer, panopticFactoryV3Abi, panopticFactoryV4Abi, panopticGuardianAbi, panopticPoolV2Abi, panopticQueryAbi$1, parsePanopticError, riskEngineAbi, semiFungiblePositionManagerV3Abi, semiFungiblePositionManagerV4Abi, stateViewAbi, uniswapV3PoolAbi } from "./rates-5oQD-SuM.js";
|
|
3
|
+
import { MAX_TICK, MAX_TRACKED_CHUNKS, MIN_TICK, NO_LOWER_LIQUIDATION_TICK, NO_UPPER_LIQUIDATION_TICK, WAD, calculatePositionDelta, calculatePositionGamma, calculatePositionValue, priceToTick, sqrtPriceX96ToTick, tickLimits, tickToPriceDecimalScaled, tickToSqrtPriceX96 } from "./greeks-9aTE-DwO.js";
|
|
4
|
+
import { addLegToTokenId, buildBatchDispatchArgs, buildUniqueCredit, buildUniqueLoan, countLegs, createTokenIdBuilder, decodeAllLegs, decodeTokenId, getPoolTokensForSimulation, loadCheckpoint, resolveTokenIndex, simulateOpenPosition, simulateWithTokenFlow } from "./writes-DBHwfbDD.js";
|
|
5
5
|
import { BaseError, ContractFunctionExecutionError, ContractFunctionRevertedError, ExecutionRevertedError, decodeFunctionResult, encodeAbiParameters, encodeFunctionData, getAddress, keccak256, toHex, zeroAddress } from "viem";
|
|
6
6
|
import { multicall } from "viem/actions";
|
|
7
7
|
|
|
@@ -204,1671 +204,665 @@ function interpolateBlocks(startBlock, endBlock, points) {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
//#endregion
|
|
207
|
-
//#region src/panoptic/v2/
|
|
208
|
-
const POOL_ID_MASK$3 = (1n << 64n) - 1n;
|
|
209
|
-
const MAX_LEGS = TOKEN_ID_BITS.MAX_LEGS;
|
|
210
|
-
const MAX_OPTION_RATIO = LEG_LIMITS.MAX_RATIO;
|
|
211
|
-
const Q192$3 = 1n << 192n;
|
|
207
|
+
//#region src/panoptic/v2/sfpmSwap/calldata.ts
|
|
212
208
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
209
|
+
* Encode the `multicall([mint, burn])` for a swap plan.
|
|
210
|
+
*
|
|
211
|
+
* The order is always `[mint, burn]` — the ERC1155 must be minted before it is
|
|
212
|
+
* burned. Which call carries the inverted (swap) limits is decided in the plan.
|
|
215
213
|
*/
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
* `[MIN_POOL_TICK, MAX_POOL_TICK]` — otherwise the getSqrtRatioAtTick
|
|
244
|
-
* call reverts with `InvalidTick`.
|
|
245
|
-
*
|
|
246
|
-
* Closed form: `1.0001^signedStrike = target/positionSize`, so
|
|
247
|
-
* `sqrtKrawX96 = isqrt(target · 2^192 / positionSize)` and
|
|
248
|
-
* `signedStrike = sqrtPriceX96ToTick(sqrtKrawX96)`. If the notional-optimal
|
|
249
|
-
* strike falls outside the safe range, we clamp inward. Clamping raises the
|
|
250
|
-
* notional but keeps it << position size for any reasonable strike.
|
|
251
|
-
*/
|
|
252
|
-
function computeTinyCreditSignedStrike(positionSize, tickSpacing) {
|
|
253
|
-
if (positionSize <= 0n) throw new PanopticError("computeTinyCreditSignedStrike: positionSize must be > 0");
|
|
254
|
-
const minSafe = MIN_TICK + tickSpacing + 1n;
|
|
255
|
-
const maxSafe = MAX_TICK - tickSpacing - 1n;
|
|
256
|
-
let signedStrike;
|
|
257
|
-
try {
|
|
258
|
-
const sqrtKrawX96 = isqrt$1(TINY_CREDIT_TARGET_NOTIONAL_WEI * Q192$3 / positionSize);
|
|
259
|
-
signedStrike = sqrtPriceX96ToTick(sqrtKrawX96);
|
|
260
|
-
} catch {
|
|
261
|
-
signedStrike = minSafe;
|
|
262
|
-
}
|
|
263
|
-
if (signedStrike < minSafe) return minSafe;
|
|
264
|
-
if (signedStrike > maxSafe) return maxSafe;
|
|
265
|
-
return signedStrike;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Assemble the tiny credit leg struct, picking a strike that avoids
|
|
269
|
-
* colliding with any existing width=0 credit leg on the same (asset,
|
|
270
|
-
* tokenType) pair.
|
|
271
|
-
*
|
|
272
|
-
* The encoded strike stored in the tokenId is
|
|
273
|
-
* `asset === 0 ? signedStrike : -signedStrike` (mirrors
|
|
274
|
-
* {@link buildNeutralLeg} in reads/collateralEstimate.ts).
|
|
275
|
-
*/
|
|
276
|
-
function pickUniqueTinyCreditLeg(baseTokenId, legIndex, positionSize, tickSpacing) {
|
|
277
|
-
const asset = 1n;
|
|
278
|
-
const tokenType = 0n;
|
|
279
|
-
const existingLegs = decodeAllLegs(baseTokenId);
|
|
280
|
-
const maxSafe = MAX_TICK - tickSpacing - 1n;
|
|
281
|
-
let signedStrike = computeTinyCreditSignedStrike(positionSize, tickSpacing);
|
|
282
|
-
while (signedStrike <= maxSafe) {
|
|
283
|
-
const candidate = asset === 0n ? signedStrike : -signedStrike;
|
|
284
|
-
let collides = false;
|
|
285
|
-
for (const leg of existingLegs) if (leg.width === 0n && leg.isLong && leg.asset === asset && leg.tokenType === tokenType && leg.strike === candidate) {
|
|
286
|
-
collides = true;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
if (!collides) break;
|
|
290
|
-
signedStrike += 1n;
|
|
291
|
-
}
|
|
292
|
-
if (signedStrike > maxSafe) throw new PanopticError("deriveUniqueTokenId: exhausted strike space picking a unique tiny credit leg");
|
|
293
|
-
const encodedStrike = asset === 0n ? signedStrike : -signedStrike;
|
|
294
|
-
return {
|
|
295
|
-
index: legIndex,
|
|
296
|
-
asset,
|
|
297
|
-
tokenType,
|
|
298
|
-
optionRatio: 1n,
|
|
299
|
-
isLong: 1n,
|
|
300
|
-
riskPartner: legIndex,
|
|
301
|
-
strike: encodedStrike,
|
|
302
|
-
width: 0n
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
function appendTinyCreditLeg(baseTokenId, baseLegCount, positionSize, tickSpacing) {
|
|
306
|
-
const newLeg = pickUniqueTinyCreditLeg(baseTokenId, baseLegCount, positionSize, tickSpacing);
|
|
307
|
-
return addLegToTokenId(baseTokenId, newLeg);
|
|
308
|
-
}
|
|
309
|
-
function scaleRatios(baseTokenId, targetPositionSize) {
|
|
310
|
-
const legs = decodeAllLegs(baseTokenId);
|
|
311
|
-
const maxRatio = legs.reduce((m, leg) => leg.optionRatio > m ? leg.optionRatio : m, 0n);
|
|
312
|
-
const N = MAX_OPTION_RATIO / maxRatio;
|
|
313
|
-
if (N < 2n) throw new PanopticError("deriveUniqueTokenId: cannot derive a unique tokenId — all 4 leg slots used and optionRatios already near the 127 ceiling");
|
|
314
|
-
const newPositionSize = (targetPositionSize + N - 1n) / N;
|
|
315
|
-
const poolId = baseTokenId & POOL_ID_MASK$3;
|
|
316
|
-
let out = poolId;
|
|
317
|
-
for (const leg of legs) out = addLegToTokenId(out, {
|
|
318
|
-
index: leg.index,
|
|
319
|
-
asset: leg.asset,
|
|
320
|
-
tokenType: leg.tokenType,
|
|
321
|
-
optionRatio: leg.optionRatio * N,
|
|
322
|
-
isLong: leg.isLong ? 1n : 0n,
|
|
323
|
-
riskPartner: leg.riskPartner,
|
|
324
|
-
strike: leg.strike,
|
|
325
|
-
width: leg.width
|
|
214
|
+
function buildSfpmSwapCalldata(plan) {
|
|
215
|
+
const mintData = encodeFunctionData({
|
|
216
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
217
|
+
functionName: "mintTokenizedPosition",
|
|
218
|
+
args: [
|
|
219
|
+
plan.poolKey,
|
|
220
|
+
plan.tokenId,
|
|
221
|
+
plan.positionSize,
|
|
222
|
+
plan.mintTickLimits[0],
|
|
223
|
+
plan.mintTickLimits[1]
|
|
224
|
+
]
|
|
225
|
+
});
|
|
226
|
+
const burnData = encodeFunctionData({
|
|
227
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
228
|
+
functionName: "burnTokenizedPosition",
|
|
229
|
+
args: [
|
|
230
|
+
plan.poolKey,
|
|
231
|
+
plan.tokenId,
|
|
232
|
+
plan.positionSize,
|
|
233
|
+
plan.burnTickLimits[0],
|
|
234
|
+
plan.burnTickLimits[1]
|
|
235
|
+
]
|
|
236
|
+
});
|
|
237
|
+
const multicallData = encodeFunctionData({
|
|
238
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
239
|
+
functionName: "multicall",
|
|
240
|
+
args: [[mintData, burnData]]
|
|
326
241
|
});
|
|
327
242
|
return {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
strategy: "ratio-scale"
|
|
243
|
+
multicallData,
|
|
244
|
+
mintData,
|
|
245
|
+
burnData
|
|
332
246
|
};
|
|
333
247
|
}
|
|
334
|
-
/**
|
|
335
|
-
* Derive a tokenId unique from `baseTokenId` for a partial reduction.
|
|
336
|
-
*
|
|
337
|
-
* Prefers a tiny-credit-leg extension (arbitrary new size). Falls back to
|
|
338
|
-
* optionRatio scaling only when the base tokenId already occupies all 4 leg
|
|
339
|
-
* slots.
|
|
340
|
-
*/
|
|
341
|
-
function deriveUniqueTokenId(params) {
|
|
342
|
-
const { baseTokenId, targetPositionSize, tickSpacing = DEFAULT_TICK_SPACING } = params;
|
|
343
|
-
if (targetPositionSize <= 0n) throw new PanopticError("deriveUniqueTokenId: targetPositionSize must be > 0");
|
|
344
|
-
const legCount = countLegs(baseTokenId);
|
|
345
|
-
if (legCount === 0n) throw new PanopticError("deriveUniqueTokenId: baseTokenId has no legs");
|
|
346
|
-
if (legCount < MAX_LEGS) {
|
|
347
|
-
const newTokenId = appendTinyCreditLeg(baseTokenId, legCount, targetPositionSize, tickSpacing);
|
|
348
|
-
return {
|
|
349
|
-
newTokenId,
|
|
350
|
-
newPositionSize: targetPositionSize,
|
|
351
|
-
effectivePositionSize: targetPositionSize,
|
|
352
|
-
strategy: "tiny-credit"
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
return scaleRatios(baseTokenId, targetPositionSize);
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Re-export decoded leg count so callers can gate UI on the strategy that
|
|
359
|
-
* would be chosen (e.g. show a divisibility hint on ratio-scale positions).
|
|
360
|
-
*/
|
|
361
|
-
function planDeriveStrategy(baseTokenId) {
|
|
362
|
-
return countLegs(baseTokenId) < MAX_LEGS ? "tiny-credit" : "ratio-scale";
|
|
363
|
-
}
|
|
364
248
|
|
|
365
249
|
//#endregion
|
|
366
|
-
//#region src/panoptic/v2/
|
|
367
|
-
/**
|
|
368
|
-
* Percentage and ratio formatters.
|
|
369
|
-
*
|
|
370
|
-
* All formatters require explicit precision - no hidden defaults.
|
|
371
|
-
*
|
|
372
|
-
* @module v2/formatters/percentage
|
|
373
|
-
*/
|
|
374
|
-
const TEN$2 = 10n;
|
|
375
|
-
function formatRatio$2(numerator, denominator, precision) {
|
|
376
|
-
if (precision < 0n) throw new RangeError("Precision must be non-negative");
|
|
377
|
-
if (denominator === 0n) return "0";
|
|
378
|
-
const isNegative = numerator < 0n !== denominator < 0n;
|
|
379
|
-
const absNumerator = numerator < 0n ? -numerator : numerator;
|
|
380
|
-
const absDenominator = denominator < 0n ? -denominator : denominator;
|
|
381
|
-
const scale = TEN$2 ** precision;
|
|
382
|
-
const scaled = (absNumerator * scale + absDenominator / 2n) / absDenominator;
|
|
383
|
-
const integerPart = scaled / scale;
|
|
384
|
-
const fractionalPart = scaled % scale;
|
|
385
|
-
const sign = isNegative ? "-" : "";
|
|
386
|
-
if (precision === 0n) return `${sign}${integerPart}`;
|
|
387
|
-
return `${sign}${integerPart}.${fractionalPart.toString().padStart(Number(precision), "0")}`;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Format basis points as a percentage string.
|
|
391
|
-
* 100 bps = 1%
|
|
392
|
-
*
|
|
393
|
-
* @param bps - Basis points value
|
|
394
|
-
* @param precision - Number of decimal places to display
|
|
395
|
-
* @returns Formatted percentage string
|
|
396
|
-
*
|
|
397
|
-
* @example
|
|
398
|
-
* ```typescript
|
|
399
|
-
* formatBps(50n, 2n) // "0.50%"
|
|
400
|
-
* formatBps(50n, 1n) // "0.5%"
|
|
401
|
-
* formatBps(100n, 2n) // "1.00%"
|
|
402
|
-
* formatBps(1500n, 2n) // "15.00%"
|
|
403
|
-
* formatBps(-50n, 2n) // "-0.50%"
|
|
404
|
-
* ```
|
|
405
|
-
*/
|
|
406
|
-
function formatBps(bps, precision) {
|
|
407
|
-
const isNegative = bps < 0n;
|
|
408
|
-
const absBps = isNegative ? -bps : bps;
|
|
409
|
-
const scaleFactor = 10n ** precision;
|
|
410
|
-
const scaled = absBps * scaleFactor / 100n;
|
|
411
|
-
const integerPart = scaled / scaleFactor;
|
|
412
|
-
const fractionalPart = scaled % scaleFactor;
|
|
413
|
-
const fractionalStr = fractionalPart.toString().padStart(Number(precision), "0");
|
|
414
|
-
const sign = isNegative ? "-" : "";
|
|
415
|
-
return precision > 0n ? `${sign}${integerPart}.${fractionalStr}%` : `${sign}${integerPart}%`;
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Format utilization as a percentage string.
|
|
419
|
-
* Utilization is stored as 0n-10000n, where 10000n = 100%.
|
|
420
|
-
*
|
|
421
|
-
* @param util - Utilization value (0-10000)
|
|
422
|
-
* @param precision - Number of decimal places to display
|
|
423
|
-
* @returns Formatted percentage string
|
|
424
|
-
*
|
|
425
|
-
* @example
|
|
426
|
-
* ```typescript
|
|
427
|
-
* formatUtilization(7500n, 2n) // "75.00%"
|
|
428
|
-
* formatUtilization(7500n, 0n) // "75%"
|
|
429
|
-
* formatUtilization(10000n, 2n) // "100.00%"
|
|
430
|
-
* formatUtilization(123n, 2n) // "1.23%"
|
|
431
|
-
* ```
|
|
432
|
-
*/
|
|
433
|
-
function formatUtilization(util, precision) {
|
|
434
|
-
return formatBps(util, precision);
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Parse a percentage string to basis points.
|
|
438
|
-
*
|
|
439
|
-
* @param percent - Percentage string (e.g., "1.5%" or "1.5")
|
|
440
|
-
* @returns Basis points value
|
|
441
|
-
*
|
|
442
|
-
* @example
|
|
443
|
-
* ```typescript
|
|
444
|
-
* parseBps("1.5%") // 150n
|
|
445
|
-
* parseBps("1.5") // 150n
|
|
446
|
-
* parseBps("100%") // 10000n
|
|
447
|
-
* parseBps("0.5%") // 50n
|
|
448
|
-
* ```
|
|
449
|
-
*/
|
|
450
|
-
function parseBps(percent) {
|
|
451
|
-
const cleaned = percent.trim().replace(/%$/, "");
|
|
452
|
-
const isNegative = cleaned.startsWith("-");
|
|
453
|
-
const absValue = isNegative ? cleaned.slice(1) : cleaned;
|
|
454
|
-
const [integerStr, fractionalStr = ""] = absValue.split(".");
|
|
455
|
-
const paddedFractional = fractionalStr.padEnd(2, "0").slice(0, 2);
|
|
456
|
-
const integerPart = BigInt(integerStr || "0") * 100n;
|
|
457
|
-
const fractionalPart = BigInt(paddedFractional || "0");
|
|
458
|
-
const result = integerPart + fractionalPart;
|
|
459
|
-
return isNegative ? -result : result;
|
|
460
|
-
}
|
|
250
|
+
//#region src/panoptic/v2/sfpmSwap/init.ts
|
|
461
251
|
/**
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
* @param numerator - Numerator of the ratio
|
|
465
|
-
* @param denominator - Denominator of the ratio
|
|
466
|
-
* @param precision - Number of decimal places to display
|
|
467
|
-
* @returns Formatted percentage string
|
|
252
|
+
* Resolve the `uint64` SFPM poolId for a Uniswap v3 pool, initializing it if needed.
|
|
468
253
|
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* ```
|
|
254
|
+
* `initializeAMMPool` is permissionless and idempotent and **returns the poolId**
|
|
255
|
+
* whether or not the pool was already registered, so a `simulateContract` call is
|
|
256
|
+
* enough to learn the id without sending a transaction. Always resolve the id this
|
|
257
|
+
* way rather than encoding it offline — the SFPM can collision-increment ids.
|
|
474
258
|
*/
|
|
475
|
-
function
|
|
476
|
-
const
|
|
477
|
-
|
|
259
|
+
async function fetchSfpmV3PoolId(params) {
|
|
260
|
+
const { client, sfpmAddress, token0, token1, fee } = params;
|
|
261
|
+
const vegoid = params.vegoid ?? Number(DEFAULT_VEGOID);
|
|
262
|
+
const { result } = await client.simulateContract({
|
|
263
|
+
address: sfpmAddress,
|
|
264
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
265
|
+
functionName: "initializeAMMPool",
|
|
266
|
+
args: [
|
|
267
|
+
token0,
|
|
268
|
+
token1,
|
|
269
|
+
fee,
|
|
270
|
+
vegoid
|
|
271
|
+
]
|
|
272
|
+
});
|
|
273
|
+
return BigInt(result);
|
|
478
274
|
}
|
|
479
|
-
|
|
480
|
-
//#endregion
|
|
481
|
-
//#region src/panoptic/v2/formatters/poolFormatters.ts
|
|
482
275
|
/**
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
* Use this factory when working with a single pool to avoid passing
|
|
486
|
-
* decimals at every call site.
|
|
487
|
-
*
|
|
488
|
-
* @param config - Pool configuration with token decimals
|
|
489
|
-
* @returns Pool-bound formatter functions
|
|
490
|
-
*
|
|
491
|
-
* @example
|
|
492
|
-
* ```typescript
|
|
493
|
-
* // Get pool data
|
|
494
|
-
* const pool = await getPool({ client, poolAddress })
|
|
495
|
-
*
|
|
496
|
-
* // Create formatters bound to this pool
|
|
497
|
-
* const fmt = createPoolFormatters({
|
|
498
|
-
* decimals0: pool.token0Decimals,
|
|
499
|
-
* decimals1: pool.token1Decimals,
|
|
500
|
-
* })
|
|
501
|
-
*
|
|
502
|
-
* // Now use without passing decimals each time
|
|
503
|
-
* const priceStr = fmt.tickToPriceScaled(position.currentTick, 4n)
|
|
504
|
-
* const amount0Str = fmt.formatAmount0(collateral.assets, 4n)
|
|
505
|
-
* const amount1Str = fmt.formatAmount1(premia.token1, 2n)
|
|
276
|
+
* Ensure a Uniswap v3 pool is registered on the SFPM, sending `initializeAMMPool`
|
|
277
|
+
* only if it is not already registered. Returns the resolved poolId.
|
|
506
278
|
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* const rawAmount1 = fmt.parseAmount1("3000")
|
|
510
|
-
* ```
|
|
279
|
+
* When `expectedPool` is given, the resolved id is checked to map back to it via
|
|
280
|
+
* `getUniswapV3PoolFromId` — guards against a wrong token/fee triple.
|
|
511
281
|
*/
|
|
512
|
-
function
|
|
513
|
-
const {
|
|
282
|
+
async function ensureSfpmV3PoolInitialized(params) {
|
|
283
|
+
const { client, wallet, sfpmAddress, token0, token1, fee, expectedPool } = params;
|
|
284
|
+
const vegoid = params.vegoid ?? Number(DEFAULT_VEGOID);
|
|
285
|
+
const poolId = await fetchSfpmV3PoolId({
|
|
286
|
+
client,
|
|
287
|
+
sfpmAddress,
|
|
288
|
+
token0,
|
|
289
|
+
token1,
|
|
290
|
+
fee,
|
|
291
|
+
vegoid
|
|
292
|
+
});
|
|
293
|
+
const registered = await client.readContract({
|
|
294
|
+
address: sfpmAddress,
|
|
295
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
296
|
+
functionName: "getUniswapV3PoolFromId",
|
|
297
|
+
args: [poolId]
|
|
298
|
+
});
|
|
299
|
+
let initialized = false;
|
|
300
|
+
let resolved = registered;
|
|
301
|
+
if (getAddress(registered) === zeroAddress) {
|
|
302
|
+
const account = wallet.account;
|
|
303
|
+
if (account === void 0) throw new PanopticError("wallet client has no account");
|
|
304
|
+
const hash = await wallet.writeContract({
|
|
305
|
+
account,
|
|
306
|
+
chain: wallet.chain ?? null,
|
|
307
|
+
address: sfpmAddress,
|
|
308
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
309
|
+
functionName: "initializeAMMPool",
|
|
310
|
+
args: [
|
|
311
|
+
token0,
|
|
312
|
+
token1,
|
|
313
|
+
fee,
|
|
314
|
+
vegoid
|
|
315
|
+
]
|
|
316
|
+
});
|
|
317
|
+
await client.waitForTransactionReceipt({ hash });
|
|
318
|
+
initialized = true;
|
|
319
|
+
resolved = await client.readContract({
|
|
320
|
+
address: sfpmAddress,
|
|
321
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
322
|
+
functionName: "getUniswapV3PoolFromId",
|
|
323
|
+
args: [poolId]
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (expectedPool !== void 0 && getAddress(resolved) !== getAddress(expectedPool)) throw new WrongUniswapPoolError(new PanopticError(`SFPM poolId ${poolId} resolves to ${resolved}, expected ${expectedPool}`));
|
|
514
327
|
return {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
tickToPrice(tick) {
|
|
518
|
-
return tickToPrice(tick);
|
|
519
|
-
},
|
|
520
|
-
tickToPriceScaled(tick, precision) {
|
|
521
|
-
return tickToPriceDecimalScaled(tick, decimals0, decimals1, precision);
|
|
522
|
-
},
|
|
523
|
-
tickToInversePriceScaled(tick, precision) {
|
|
524
|
-
return tickToPriceDecimalScaled(tick, decimals1, decimals0, precision);
|
|
525
|
-
},
|
|
526
|
-
priceToTick(price) {
|
|
527
|
-
return priceToTick(price, decimals0, decimals1);
|
|
528
|
-
},
|
|
529
|
-
formatAmount0(amount, precision) {
|
|
530
|
-
return formatTokenAmount(amount, decimals0, precision);
|
|
531
|
-
},
|
|
532
|
-
formatAmount1(amount, precision) {
|
|
533
|
-
return formatTokenAmount(amount, decimals1, precision);
|
|
534
|
-
},
|
|
535
|
-
parseAmount0(amount) {
|
|
536
|
-
return parseTokenAmount(amount, decimals0);
|
|
537
|
-
},
|
|
538
|
-
parseAmount1(amount) {
|
|
539
|
-
return parseTokenAmount(amount, decimals1);
|
|
540
|
-
}
|
|
328
|
+
poolId,
|
|
329
|
+
initialized
|
|
541
330
|
};
|
|
542
331
|
}
|
|
543
332
|
|
|
544
333
|
//#endregion
|
|
545
|
-
//#region src/panoptic/v2/
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (precision < 0n) throw new RangeError("Precision must be non-negative");
|
|
549
|
-
const scale = TEN$1 ** precision;
|
|
550
|
-
const scaled = (numerator * scale + denominator / 2n) / denominator;
|
|
551
|
-
const integerPart = scaled / scale;
|
|
552
|
-
const fractionalPart = scaled % scale;
|
|
553
|
-
if (precision === 0n) return integerPart.toString();
|
|
554
|
-
return `${integerPart}.${fractionalPart.toString().padStart(Number(precision), "0")}`;
|
|
555
|
-
}
|
|
334
|
+
//#region src/panoptic/v2/sfpmSwap/plan.ts
|
|
335
|
+
/** Wide, non-inverted band for the paired (non-swapping) call — never triggers a swap. */
|
|
336
|
+
const WIDE_LIMITS = [Number(MIN_TICK) + 1, Number(MAX_TICK) - 1];
|
|
556
337
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
* Token lists use a standardized format: `chainId:address`
|
|
560
|
-
* This is compatible with most token list standards.
|
|
561
|
-
*
|
|
562
|
-
* @param chainId - The chain ID
|
|
563
|
-
* @param address - The token address
|
|
564
|
-
* @returns Token list ID string
|
|
565
|
-
*
|
|
566
|
-
* @example
|
|
567
|
-
* ```typescript
|
|
568
|
-
* getTokenListId(1n, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')
|
|
569
|
-
* // "1:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
|
338
|
+
* Convert a slippage tolerance in bps to a conservative Uniswap tick distance.
|
|
570
339
|
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
340
|
+
* Ticks are 1.0001^tick, so each tick ≈ 1 bps. This walks up powers of 1.0001
|
|
341
|
+
* until the cumulative price move covers `slippageBps`, matching the hedger-bot's
|
|
342
|
+
* dispatch-path helper so both swap paths agree on band width.
|
|
574
343
|
*/
|
|
575
|
-
function
|
|
576
|
-
|
|
344
|
+
function slippageBpsToTickDistance(slippageBps) {
|
|
345
|
+
if (slippageBps < 0n || slippageBps > 1000n) throw new PanopticError(`slippage bps ${slippageBps} out of bounds (0..1000)`);
|
|
346
|
+
if (slippageBps === 0n) return 0n;
|
|
347
|
+
let numerator = 1n;
|
|
348
|
+
let denominator = 1n;
|
|
349
|
+
let ticks = 0n;
|
|
350
|
+
while (numerator * 10000n < denominator * (10000n + slippageBps)) {
|
|
351
|
+
numerator *= 10001n;
|
|
352
|
+
denominator *= 10000n;
|
|
353
|
+
ticks += 1n;
|
|
354
|
+
}
|
|
355
|
+
return ticks;
|
|
577
356
|
}
|
|
578
357
|
/**
|
|
579
|
-
*
|
|
358
|
+
* Build the swap plan.
|
|
580
359
|
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
360
|
+
* Mechanism (verified in the Phase 0 fork test):
|
|
361
|
+
* - A single-leg **loan** tokenId (width=0, isLong=false, `asset == tokenType`) moves
|
|
362
|
+
* exactly `positionSize` of the `tokenType` token when its call carries inverted
|
|
363
|
+
* tick limits (`low > high`); the paired call uses a wide band and moves nothing.
|
|
364
|
+
* - `exactIn`: swap on the **mint**; `tokenType` = the **input** token index.
|
|
365
|
+
* - `exactOut`: swap on the **burn** (isLong flips → exact-output); `tokenType` = the
|
|
366
|
+
* **output** token index.
|
|
583
367
|
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* parseTokenListId("1:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
|
|
587
|
-
* // { chainId: 1n, address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" }
|
|
588
|
-
* ```
|
|
368
|
+
* The inverted band is centered on `currentTick` at ±`slippageBpsToTickDistance`,
|
|
369
|
+
* which the SFPM re-sorts and enforces as an open interval on the post-swap tick.
|
|
589
370
|
*/
|
|
590
|
-
function
|
|
591
|
-
const
|
|
592
|
-
if (
|
|
371
|
+
function buildSfpmSwapPlan(params) {
|
|
372
|
+
const { sfpmAddress, poolAddress, poolId, kind, zeroForOne, amount, currentTick, slippageBps } = params;
|
|
373
|
+
if (amount <= 0n) throw new PanopticError(`swap amount must be positive (got ${amount})`);
|
|
374
|
+
const distance = slippageBpsToTickDistance(slippageBps);
|
|
375
|
+
if (distance < 1n) throw new PanopticError(`slippageBps ${slippageBps} yields a zero-width tick band; use a larger tolerance`);
|
|
376
|
+
const tokenType = kind === "exactIn" ? zeroForOne ? 0n : 1n : zeroForOne ? 1n : 0n;
|
|
377
|
+
const tokenId = createTokenIdBuilder(poolId).addLoan({
|
|
378
|
+
asset: tokenType,
|
|
379
|
+
tokenType,
|
|
380
|
+
strike: 0n
|
|
381
|
+
}).build();
|
|
382
|
+
const d = Number(distance);
|
|
383
|
+
const low = Math.min(Number(MAX_TICK), currentTick + d);
|
|
384
|
+
const high = Math.max(Number(MIN_TICK), currentTick - d);
|
|
385
|
+
if (low <= high) throw new PanopticError(`slippageBps ${slippageBps} at tick ${currentTick} yields a non-inverted band after clamping`);
|
|
386
|
+
const invertedLimits = [low, high];
|
|
387
|
+
const swapOn = kind === "exactIn" ? "mint" : "burn";
|
|
593
388
|
return {
|
|
594
|
-
|
|
595
|
-
|
|
389
|
+
sfpmAddress,
|
|
390
|
+
poolAddress,
|
|
391
|
+
poolKey: encodeAbiParameters([{ type: "address" }], [poolAddress]),
|
|
392
|
+
tokenId,
|
|
393
|
+
positionSize: amount,
|
|
394
|
+
mintTickLimits: swapOn === "mint" ? invertedLimits : WIDE_LIMITS,
|
|
395
|
+
burnTickLimits: swapOn === "burn" ? invertedLimits : WIDE_LIMITS,
|
|
396
|
+
swapOn,
|
|
397
|
+
kind
|
|
596
398
|
};
|
|
597
399
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
400
|
+
|
|
401
|
+
//#endregion
|
|
402
|
+
//#region src/panoptic/v2/sfpmSwap/quote.ts
|
|
403
|
+
const UINT128 = 1n << 128n;
|
|
404
|
+
const INT128_MAX = (1n << 127n) - 1n;
|
|
405
|
+
/** Extract the two signed 128-bit slots from a packed `LeftRightSigned` int256. */
|
|
406
|
+
function unpackLeftRightSigned(packed) {
|
|
407
|
+
const u = packed < 0n ? packed + (1n << 256n) : packed;
|
|
408
|
+
const toInt128 = (half) => half > INT128_MAX ? half - UINT128 : half;
|
|
409
|
+
return {
|
|
410
|
+
right: toInt128(u & UINT128 - 1n),
|
|
411
|
+
left: toInt128(u >> 128n & UINT128 - 1n)
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/** Wrap an unknown thrown value as a PanopticError, decoding Panoptic reverts when possible. */
|
|
415
|
+
function toPanopticError(err) {
|
|
416
|
+
if (err instanceof PanopticError) return err;
|
|
417
|
+
const parsed = parsePanopticError(err);
|
|
418
|
+
if (parsed) return parsed.error;
|
|
419
|
+
return new PanopticError(err instanceof Error ? err.message : "SFPM swap simulation failed", err instanceof Error ? err : void 0);
|
|
617
420
|
}
|
|
618
421
|
/**
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
* @returns Fee tier string
|
|
623
|
-
*
|
|
624
|
-
* @example
|
|
625
|
-
* ```typescript
|
|
626
|
-
* formatFeeTier(500n) // "0.05%"
|
|
627
|
-
* formatFeeTier(3000n) // "0.30%"
|
|
628
|
-
* formatFeeTier(10000n) // "1.0%"
|
|
629
|
-
* ```
|
|
422
|
+
* Quote a swap by simulating `SFPM.multicall([mint, burn])` and decoding the swap
|
|
423
|
+
* call's `totalMoved` return. Authoritative — captures the width-0 loan-leg wei
|
|
424
|
+
* rounding a raw QuoterV2 quote would miss.
|
|
630
425
|
*/
|
|
631
|
-
function
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
|
|
426
|
+
async function quoteSfpmSwap(params) {
|
|
427
|
+
const { client, plan, account, stateOverride, blockNumber } = params;
|
|
428
|
+
const { mintData, burnData } = buildSfpmSwapCalldata(plan);
|
|
429
|
+
const _meta = await getBlockMeta({
|
|
430
|
+
client,
|
|
431
|
+
blockNumber
|
|
432
|
+
});
|
|
433
|
+
try {
|
|
434
|
+
const { result } = await client.simulateContract({
|
|
435
|
+
account,
|
|
436
|
+
address: plan.sfpmAddress,
|
|
437
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
438
|
+
functionName: "multicall",
|
|
439
|
+
args: [[mintData, burnData]],
|
|
440
|
+
blockNumber,
|
|
441
|
+
stateOverride
|
|
442
|
+
});
|
|
443
|
+
const swapIndex = plan.swapOn === "mint" ? 0 : 1;
|
|
444
|
+
const decoded = decodeFunctionResult({
|
|
445
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
446
|
+
functionName: plan.swapOn === "mint" ? "mintTokenizedPosition" : "burnTokenizedPosition",
|
|
447
|
+
data: result[swapIndex]
|
|
448
|
+
});
|
|
449
|
+
const finalTick = Number(decoded[2]);
|
|
450
|
+
const { right, left } = unpackLeftRightSigned(decoded[1]);
|
|
451
|
+
const inSlot = [right, left].find((s) => s > 0n);
|
|
452
|
+
const outSlot = [right, left].find((s) => s < 0n);
|
|
453
|
+
if (inSlot === void 0 || outSlot === void 0) return {
|
|
454
|
+
success: false,
|
|
455
|
+
error: new PanopticError(`SFPM swap simulation moved no tokens (totalMoved=${decoded[1]})`),
|
|
456
|
+
_meta
|
|
457
|
+
};
|
|
458
|
+
return {
|
|
459
|
+
success: true,
|
|
460
|
+
data: {
|
|
461
|
+
amountIn: inSlot,
|
|
462
|
+
amountOut: -outSlot,
|
|
463
|
+
finalTick
|
|
464
|
+
},
|
|
465
|
+
gasEstimate: 0n,
|
|
466
|
+
_meta
|
|
467
|
+
};
|
|
468
|
+
} catch (err) {
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
error: toPanopticError(err),
|
|
472
|
+
_meta
|
|
473
|
+
};
|
|
474
|
+
}
|
|
635
475
|
}
|
|
636
476
|
|
|
637
477
|
//#endregion
|
|
638
|
-
//#region src/panoptic/v2/
|
|
639
|
-
const TEN = 10n;
|
|
640
|
-
function formatRatio(numerator, denominator, precision) {
|
|
641
|
-
if (precision < 0n) throw new RangeError("Precision must be non-negative");
|
|
642
|
-
const scale = TEN ** precision;
|
|
643
|
-
const scaled = (numerator * scale + denominator / 2n) / denominator;
|
|
644
|
-
const integerPart = scaled / scale;
|
|
645
|
-
const fractionalPart = scaled % scale;
|
|
646
|
-
if (precision === 0n) return integerPart.toString();
|
|
647
|
-
return `${integerPart}.${fractionalPart.toString().padStart(Number(precision), "0")}`;
|
|
648
|
-
}
|
|
478
|
+
//#region src/panoptic/v2/reads/sfpm.ts
|
|
649
479
|
/**
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
* @param address - Full address
|
|
653
|
-
* @param chars - Characters to show on each side (default: 4)
|
|
654
|
-
* @returns Truncated address like "0x1234...5678"
|
|
655
|
-
*
|
|
656
|
-
* @example
|
|
657
|
-
* ```typescript
|
|
658
|
-
* truncateAddress('0x1234567890abcdef1234567890abcdef12345678')
|
|
659
|
-
* // "0x1234...5678"
|
|
480
|
+
* Resolve an SFPM poolId to its corresponding Uniswap V3 pool address.
|
|
660
481
|
*
|
|
661
|
-
*
|
|
662
|
-
* // "0x123456...345678"
|
|
663
|
-
* ```
|
|
482
|
+
* Calls `SemiFungiblePositionManagerV3.getUniswapV3PoolFromId(poolId)`.
|
|
664
483
|
*/
|
|
665
|
-
function
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
484
|
+
async function getUniswapV3PoolFromId(params) {
|
|
485
|
+
const { client, sfpmAddress, poolId } = params;
|
|
486
|
+
return client.readContract({
|
|
487
|
+
address: sfpmAddress,
|
|
488
|
+
abi: semiFungiblePositionManagerV3Abi,
|
|
489
|
+
functionName: "getUniswapV3PoolFromId",
|
|
490
|
+
args: [poolId]
|
|
491
|
+
});
|
|
670
492
|
}
|
|
671
493
|
/**
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* @param timestamp - Unix timestamp in seconds
|
|
675
|
-
* @returns ISO date string
|
|
494
|
+
* Resolve an SFPM poolId to its corresponding Uniswap V4 pool key.
|
|
676
495
|
*
|
|
677
|
-
*
|
|
678
|
-
* ```typescript
|
|
679
|
-
* formatTimestamp(1700000000n) // "2023-11-14"
|
|
680
|
-
* ```
|
|
496
|
+
* Calls `SemiFungiblePositionManagerV4.getUniswapV4PoolKeyFromId(poolId)`.
|
|
681
497
|
*/
|
|
682
|
-
function
|
|
683
|
-
const
|
|
684
|
-
|
|
498
|
+
async function getUniswapV4PoolKeyFromId(params) {
|
|
499
|
+
const { client, sfpmAddress, poolId } = params;
|
|
500
|
+
const raw = await client.readContract({
|
|
501
|
+
address: sfpmAddress,
|
|
502
|
+
abi: semiFungiblePositionManagerV4Abi,
|
|
503
|
+
functionName: "getUniswapV4PoolKeyFromId",
|
|
504
|
+
args: [poolId]
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
507
|
+
currency0: raw.currency0,
|
|
508
|
+
currency1: raw.currency1,
|
|
509
|
+
fee: BigInt(raw.fee),
|
|
510
|
+
tickSpacing: BigInt(raw.tickSpacing),
|
|
511
|
+
hooks: raw.hooks
|
|
512
|
+
};
|
|
685
513
|
}
|
|
686
514
|
/**
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* @param timestamp - Unix timestamp in seconds
|
|
690
|
-
* @returns ISO datetime string
|
|
515
|
+
* Get the enforced tick limits for a pool from the SFPM.
|
|
691
516
|
*
|
|
692
|
-
* @
|
|
693
|
-
*
|
|
694
|
-
* formatDatetime(1700000000n) // "2023-11-14T22:13:20.000Z"
|
|
695
|
-
* ```
|
|
517
|
+
* @param params - The parameters
|
|
518
|
+
* @returns The min and max enforced ticks
|
|
696
519
|
*/
|
|
697
|
-
function
|
|
698
|
-
const
|
|
699
|
-
|
|
520
|
+
async function getEnforcedTickLimits(params) {
|
|
521
|
+
const { client, sfpmAddress, poolId } = params;
|
|
522
|
+
const [minTick, maxTick] = await client.readContract({
|
|
523
|
+
address: sfpmAddress,
|
|
524
|
+
abi: semiFungiblePositionManagerV4Abi,
|
|
525
|
+
functionName: "getEnforcedTickLimits",
|
|
526
|
+
args: [poolId]
|
|
527
|
+
});
|
|
528
|
+
return {
|
|
529
|
+
minEnforcedTick: minTick,
|
|
530
|
+
maxEnforcedTick: maxTick
|
|
531
|
+
};
|
|
700
532
|
}
|
|
701
533
|
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
* @param timestamp - Unix timestamp in seconds
|
|
705
|
-
* @param locale - Locale string (default: system locale)
|
|
706
|
-
* @param options - Intl.DateTimeFormat options
|
|
707
|
-
* @returns Formatted date string
|
|
708
|
-
*
|
|
709
|
-
* @example
|
|
710
|
-
* ```typescript
|
|
711
|
-
* formatTimestampLocale(1700000000n)
|
|
712
|
-
* // "11/14/2023" (US locale)
|
|
534
|
+
* Fetch liquidity breakdown for a batch of chunks via SFPM.getAccountLiquidity().
|
|
713
535
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
* ```
|
|
536
|
+
* Uses multicall for efficiency. Returns one result per input chunk,
|
|
537
|
+
* along with block metadata for freshness tracking.
|
|
717
538
|
*/
|
|
718
|
-
function
|
|
719
|
-
const
|
|
720
|
-
|
|
539
|
+
async function getChunkLiquidities(params) {
|
|
540
|
+
const { client, sfpmAddress, poolKeyBytes, chunks } = params;
|
|
541
|
+
if (chunks.length > MAX_TRACKED_CHUNKS) throw new ChunkLimitError(BigInt(chunks.length), 0n);
|
|
542
|
+
const _meta = params._meta ?? await getBlockMeta({ client });
|
|
543
|
+
if (chunks.length === 0) return {
|
|
544
|
+
results: [],
|
|
545
|
+
_meta
|
|
546
|
+
};
|
|
547
|
+
const multicallResults = await client.multicall({
|
|
548
|
+
contracts: chunks.map((chunk) => ({
|
|
549
|
+
address: sfpmAddress,
|
|
550
|
+
abi: semiFungiblePositionManagerV4Abi,
|
|
551
|
+
functionName: "getAccountLiquidity",
|
|
552
|
+
args: [
|
|
553
|
+
poolKeyBytes,
|
|
554
|
+
chunk.owner,
|
|
555
|
+
chunk.tokenType,
|
|
556
|
+
Number(chunk.tickLower),
|
|
557
|
+
Number(chunk.tickUpper)
|
|
558
|
+
]
|
|
559
|
+
})),
|
|
560
|
+
allowFailure: true
|
|
561
|
+
});
|
|
562
|
+
const results = multicallResults.map((result) => {
|
|
563
|
+
if (result.status === "failure") return {
|
|
564
|
+
netLiquidity: 0n,
|
|
565
|
+
removedLiquidity: 0n,
|
|
566
|
+
totalLiquidity: 0n,
|
|
567
|
+
shortLiquidity: 0n,
|
|
568
|
+
longLiquidity: 0n
|
|
569
|
+
};
|
|
570
|
+
const packed = result.result;
|
|
571
|
+
const netLiquidity = packed & (1n << 128n) - 1n;
|
|
572
|
+
const removedLiquidity = packed >> 128n;
|
|
573
|
+
const totalLiquidity = netLiquidity + removedLiquidity;
|
|
574
|
+
return {
|
|
575
|
+
netLiquidity,
|
|
576
|
+
removedLiquidity,
|
|
577
|
+
totalLiquidity,
|
|
578
|
+
shortLiquidity: totalLiquidity,
|
|
579
|
+
longLiquidity: removedLiquidity
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
return {
|
|
583
|
+
results,
|
|
584
|
+
_meta
|
|
585
|
+
};
|
|
721
586
|
}
|
|
587
|
+
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/panoptic/v2/reads/factory.ts
|
|
722
590
|
/**
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* @param ms - Duration in milliseconds
|
|
726
|
-
* @returns Formatted duration string
|
|
727
|
-
*
|
|
728
|
-
* @example
|
|
729
|
-
* ```typescript
|
|
730
|
-
* formatDuration(1500n) // "1.5s"
|
|
731
|
-
* formatDuration(150n) // "150ms"
|
|
732
|
-
* formatDuration(90000n) // "1m 30s"
|
|
733
|
-
* formatDuration(3661000n) // "1h 1m"
|
|
734
|
-
* ```
|
|
591
|
+
* Get the token URI from a PanopticFactory NFT.
|
|
735
592
|
*/
|
|
736
|
-
function
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
return `${sign}${seconds}.${tenths}s`;
|
|
746
|
-
}
|
|
747
|
-
if (absMs < 3600000n) {
|
|
748
|
-
const minutes$1 = absMs / 60000n;
|
|
749
|
-
const seconds = (absMs % 60000n + 500n) / 1000n;
|
|
750
|
-
return seconds > 0n ? `${sign}${minutes$1}m ${seconds}s` : `${sign}${minutes$1}m`;
|
|
751
|
-
}
|
|
752
|
-
const hours = absMs / 3600000n;
|
|
753
|
-
const minutes = (absMs % 3600000n + 30000n) / 60000n;
|
|
754
|
-
return minutes > 0n ? `${sign}${hours}h ${minutes}m` : `${sign}${hours}h`;
|
|
593
|
+
async function getFactoryTokenURI(params) {
|
|
594
|
+
const { client, factoryAddress, version, tokenId } = params;
|
|
595
|
+
const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
|
|
596
|
+
return client.readContract({
|
|
597
|
+
address: factoryAddress,
|
|
598
|
+
abi,
|
|
599
|
+
functionName: "tokenURI",
|
|
600
|
+
args: [tokenId]
|
|
601
|
+
});
|
|
755
602
|
}
|
|
756
603
|
/**
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
* @param seconds - Duration in seconds
|
|
760
|
-
* @returns Formatted duration string
|
|
604
|
+
* Get the owner of a PanopticFactory NFT.
|
|
761
605
|
*/
|
|
762
|
-
function
|
|
763
|
-
|
|
606
|
+
async function getFactoryOwnerOf(params) {
|
|
607
|
+
const { client, factoryAddress, version, tokenId } = params;
|
|
608
|
+
const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
|
|
609
|
+
return client.readContract({
|
|
610
|
+
address: factoryAddress,
|
|
611
|
+
abi,
|
|
612
|
+
functionName: "ownerOf",
|
|
613
|
+
args: [tokenId]
|
|
614
|
+
});
|
|
764
615
|
}
|
|
765
616
|
/**
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
* @param blockNumber - The block number
|
|
769
|
-
* @returns Formatted block number with commas
|
|
770
|
-
*
|
|
771
|
-
* @example
|
|
772
|
-
* ```typescript
|
|
773
|
-
* formatBlockNumber(18000000n) // "18,000,000"
|
|
774
|
-
* ```
|
|
617
|
+
* Construct NFT metadata for a pool via the factory contract.
|
|
775
618
|
*/
|
|
776
|
-
function
|
|
777
|
-
|
|
619
|
+
async function getFactoryConstructMetadata(params) {
|
|
620
|
+
const { client, factoryAddress, version, panopticPoolAddress, symbol0, symbol1, fee } = params;
|
|
621
|
+
const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
|
|
622
|
+
return client.readContract({
|
|
623
|
+
address: factoryAddress,
|
|
624
|
+
abi,
|
|
625
|
+
functionName: "constructMetadata",
|
|
626
|
+
args: [
|
|
627
|
+
panopticPoolAddress,
|
|
628
|
+
symbol0,
|
|
629
|
+
symbol1,
|
|
630
|
+
fee
|
|
631
|
+
]
|
|
632
|
+
});
|
|
778
633
|
}
|
|
779
634
|
/**
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
* @param gas - Gas units
|
|
783
|
-
* @returns Formatted gas string
|
|
635
|
+
* Get the PanopticPool address for a given pool and risk engine.
|
|
784
636
|
*/
|
|
785
|
-
function
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return `${hash.slice(0, Number(charsBig + 2n))}...${hash.slice(-Number(charsBig))}`;
|
|
806
|
-
}
|
|
807
|
-
function formatHex(value) {
|
|
808
|
-
return `0x${value.toString(16)}`;
|
|
637
|
+
async function getPanopticPoolAddress(params) {
|
|
638
|
+
const { client, factoryAddress, riskEngine } = params;
|
|
639
|
+
if (params.version === "v3") return client.readContract({
|
|
640
|
+
address: factoryAddress,
|
|
641
|
+
abi: panopticFactoryV3Abi,
|
|
642
|
+
functionName: "getPanopticPool",
|
|
643
|
+
args: [params.univ3pool, riskEngine]
|
|
644
|
+
});
|
|
645
|
+
return client.readContract({
|
|
646
|
+
address: factoryAddress,
|
|
647
|
+
abi: panopticFactoryV4Abi,
|
|
648
|
+
functionName: "getPanopticPool",
|
|
649
|
+
args: [{
|
|
650
|
+
currency0: params.poolKey.currency0,
|
|
651
|
+
currency1: params.poolKey.currency1,
|
|
652
|
+
fee: Number(params.poolKey.fee),
|
|
653
|
+
tickSpacing: Number(params.poolKey.tickSpacing),
|
|
654
|
+
hooks: params.poolKey.hooks
|
|
655
|
+
}, riskEngine]
|
|
656
|
+
});
|
|
809
657
|
}
|
|
810
658
|
/**
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
* @param tokenId - TokenId value
|
|
814
|
-
* @returns Hex string representation
|
|
659
|
+
* Mine for an optimal pool address salt with high rarity.
|
|
815
660
|
*/
|
|
816
|
-
function
|
|
817
|
-
|
|
661
|
+
async function minePoolAddress(params) {
|
|
662
|
+
const { client, factoryAddress, deployerAddress, riskEngine, salt, loops, minTargetRarity } = params;
|
|
663
|
+
let result;
|
|
664
|
+
if (params.version === "v3") result = await client.readContract({
|
|
665
|
+
address: factoryAddress,
|
|
666
|
+
abi: panopticFactoryV3Abi,
|
|
667
|
+
functionName: "minePoolAddress",
|
|
668
|
+
args: [
|
|
669
|
+
deployerAddress,
|
|
670
|
+
params.v3Pool,
|
|
671
|
+
riskEngine,
|
|
672
|
+
salt,
|
|
673
|
+
loops,
|
|
674
|
+
minTargetRarity
|
|
675
|
+
]
|
|
676
|
+
});
|
|
677
|
+
else result = await client.readContract({
|
|
678
|
+
address: factoryAddress,
|
|
679
|
+
abi: panopticFactoryV4Abi,
|
|
680
|
+
functionName: "minePoolAddress",
|
|
681
|
+
args: [
|
|
682
|
+
deployerAddress,
|
|
683
|
+
{
|
|
684
|
+
currency0: params.poolKey.currency0,
|
|
685
|
+
currency1: params.poolKey.currency1,
|
|
686
|
+
fee: Number(params.poolKey.fee),
|
|
687
|
+
tickSpacing: Number(params.poolKey.tickSpacing),
|
|
688
|
+
hooks: params.poolKey.hooks
|
|
689
|
+
},
|
|
690
|
+
riskEngine,
|
|
691
|
+
salt,
|
|
692
|
+
loops,
|
|
693
|
+
minTargetRarity
|
|
694
|
+
]
|
|
695
|
+
});
|
|
696
|
+
return {
|
|
697
|
+
bestSalt: BigInt(result[0]),
|
|
698
|
+
highestRarity: result[1]
|
|
699
|
+
};
|
|
818
700
|
}
|
|
819
701
|
/**
|
|
820
|
-
*
|
|
702
|
+
* Simulate a pool deployment to get the predicted pool address.
|
|
821
703
|
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
704
|
+
* Uses `simulateContract` on `deployNewPool` — the return value is the new pool address
|
|
705
|
+
* without actually executing the transaction.
|
|
824
706
|
*/
|
|
825
|
-
function
|
|
826
|
-
|
|
707
|
+
async function simulateDeployNewPool(params) {
|
|
708
|
+
const { client, factoryAddress, account, riskEngine, salt } = params;
|
|
709
|
+
if (params.version === "v3") {
|
|
710
|
+
const { result: result$1 } = await client.simulateContract({
|
|
711
|
+
address: factoryAddress,
|
|
712
|
+
abi: panopticFactoryV3Abi,
|
|
713
|
+
functionName: "deployNewPool",
|
|
714
|
+
args: [
|
|
715
|
+
params.token0,
|
|
716
|
+
params.token1,
|
|
717
|
+
Number(params.fee),
|
|
718
|
+
riskEngine,
|
|
719
|
+
salt
|
|
720
|
+
],
|
|
721
|
+
account
|
|
722
|
+
});
|
|
723
|
+
return result$1;
|
|
724
|
+
}
|
|
725
|
+
const { result } = await client.simulateContract({
|
|
726
|
+
address: factoryAddress,
|
|
727
|
+
abi: panopticFactoryV4Abi,
|
|
728
|
+
functionName: "deployNewPool",
|
|
729
|
+
args: [
|
|
730
|
+
{
|
|
731
|
+
currency0: params.poolKey.currency0,
|
|
732
|
+
currency1: params.poolKey.currency1,
|
|
733
|
+
fee: Number(params.poolKey.fee),
|
|
734
|
+
tickSpacing: Number(params.poolKey.tickSpacing),
|
|
735
|
+
hooks: params.poolKey.hooks
|
|
736
|
+
},
|
|
737
|
+
riskEngine,
|
|
738
|
+
salt
|
|
739
|
+
],
|
|
740
|
+
account
|
|
741
|
+
});
|
|
742
|
+
return result;
|
|
827
743
|
}
|
|
828
744
|
/**
|
|
829
|
-
*
|
|
745
|
+
* Resolve an SFPM poolId to its PanopticPool address.
|
|
830
746
|
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
747
|
+
* Chains two on-chain lookups:
|
|
748
|
+
* 1. SFPM: poolId → Uniswap pool address (V3) or pool key (V4)
|
|
749
|
+
* 2. Factory: Uniswap pool + riskEngine → PanopticPool address
|
|
834
750
|
*/
|
|
835
|
-
function
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
751
|
+
async function getPanopticPoolFromPoolId(params) {
|
|
752
|
+
const { client, sfpmAddress, factoryAddress, riskEngine, poolId, version } = params;
|
|
753
|
+
if (version === "v3") {
|
|
754
|
+
const univ3pool = await getUniswapV3PoolFromId({
|
|
755
|
+
client,
|
|
756
|
+
sfpmAddress,
|
|
757
|
+
poolId
|
|
758
|
+
});
|
|
759
|
+
return getPanopticPoolAddress({
|
|
760
|
+
version: "v3",
|
|
761
|
+
client,
|
|
762
|
+
factoryAddress,
|
|
763
|
+
univ3pool,
|
|
764
|
+
riskEngine
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
const poolKey = await getUniswapV4PoolKeyFromId({
|
|
768
|
+
client,
|
|
769
|
+
sfpmAddress,
|
|
770
|
+
poolId
|
|
771
|
+
});
|
|
772
|
+
return getPanopticPoolAddress({
|
|
773
|
+
version: "v4",
|
|
774
|
+
client,
|
|
775
|
+
factoryAddress,
|
|
776
|
+
poolKey,
|
|
777
|
+
riskEngine
|
|
778
|
+
});
|
|
841
779
|
}
|
|
842
780
|
/**
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
* @param value - The numeric value
|
|
846
|
-
* @param precision - Number of decimal places (default: 1n)
|
|
847
|
-
* @returns Formatted string with suffix
|
|
781
|
+
* Resolve an SFPM poolId to its PanopticPool address without knowing the version.
|
|
848
782
|
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
* formatCompact(1234n) // "1.2K"
|
|
852
|
-
* formatCompact(1234567n) // "1.2M"
|
|
853
|
-
* formatCompact(1234567890n) // "1.2B"
|
|
854
|
-
* formatCompact(999n) // "999"
|
|
855
|
-
* ```
|
|
856
|
-
*/
|
|
857
|
-
function formatCompact(value, precision = 1n) {
|
|
858
|
-
const isNegative = value < 0n;
|
|
859
|
-
const absValue = isNegative ? -value : value;
|
|
860
|
-
let formatted;
|
|
861
|
-
if (absValue < 1000n) formatted = absValue.toString();
|
|
862
|
-
else if (absValue < 1000000n) formatted = `${formatRatio(absValue, 1000n, precision)}K`;
|
|
863
|
-
else if (absValue < 1000000000n) formatted = `${formatRatio(absValue, 1000000n, precision)}M`;
|
|
864
|
-
else formatted = `${formatRatio(absValue, 1000000000n, precision)}B`;
|
|
865
|
-
return isNegative ? `-${formatted}` : formatted;
|
|
866
|
-
}
|
|
867
|
-
/**
|
|
868
|
-
* Format a wei amount as a display string.
|
|
783
|
+
* Tries both V3 and V4 lookups in parallel. The factory returns `address(0)` for
|
|
784
|
+
* non-existent pools, so the non-zero result identifies the correct version.
|
|
869
785
|
*
|
|
870
|
-
*
|
|
871
|
-
* @returns Formatted string with unit
|
|
872
|
-
*/
|
|
873
|
-
function formatWei(wei) {
|
|
874
|
-
return `${wei} wei`;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Format a wei amount as gwei.
|
|
786
|
+
* At least one of `v3` or `v4` must be provided.
|
|
878
787
|
*
|
|
879
|
-
* @
|
|
880
|
-
* @param precision - Number of decimal places to display
|
|
881
|
-
* @returns Formatted string with unit
|
|
788
|
+
* @throws {PanopticValidationError} If no version config is provided or neither resolves.
|
|
882
789
|
*/
|
|
883
|
-
function
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const MAX_TOKEN_DECIMALS = 255n;
|
|
893
|
-
const MAX_DECIMAL_EXPONENT = 512n;
|
|
894
|
-
function invalid$1(message) {
|
|
895
|
-
throw new PanopticValidationError(`generateOverlappingTokenIds: ${message}`);
|
|
896
|
-
}
|
|
897
|
-
function pow10(exponent) {
|
|
898
|
-
return 10n ** exponent;
|
|
899
|
-
}
|
|
900
|
-
function parsePriceSpacing(value, quoteDecimals) {
|
|
901
|
-
const match = /^\+?(?:(\d+)(?:\.(\d*))?|\.(\d+))(?:e([+-]?\d+))?$/i.exec(value.trim());
|
|
902
|
-
if (match === null) invalid$1("strikePriceSpacing must be a positive decimal string");
|
|
903
|
-
const integerPart = match[1] ?? "0";
|
|
904
|
-
const fractionalPart = match[2] ?? match[3] ?? "";
|
|
905
|
-
const exponent = BigInt(match[4] ?? "0");
|
|
906
|
-
if (exponent < -MAX_DECIMAL_EXPONENT || exponent > MAX_DECIMAL_EXPONENT) invalid$1("strikePriceSpacing exponent is too large");
|
|
907
|
-
let units = BigInt(`${integerPart}${fractionalPart}`);
|
|
908
|
-
let scale = BigInt(fractionalPart.length) - exponent;
|
|
909
|
-
if (scale < 0n) {
|
|
910
|
-
units *= pow10(-scale);
|
|
911
|
-
scale = 0n;
|
|
912
|
-
}
|
|
913
|
-
while (scale > 0n && units % 10n === 0n) {
|
|
914
|
-
units /= 10n;
|
|
915
|
-
scale -= 1n;
|
|
916
|
-
}
|
|
917
|
-
if (units <= 0n) invalid$1("strikePriceSpacing must be positive");
|
|
918
|
-
if (scale > quoteDecimals) invalid$1("strikePriceSpacing exceeds the quote token decimal precision");
|
|
919
|
-
return {
|
|
920
|
-
units,
|
|
921
|
-
scale,
|
|
922
|
-
denominator: pow10(scale)
|
|
790
|
+
async function resolvePanopticPoolFromPoolId(params) {
|
|
791
|
+
const { client, poolId, riskEngine, v3, v4 } = params;
|
|
792
|
+
if (!v3 && !v4) throw new Error("At least one of v3 or v4 must be provided");
|
|
793
|
+
const isNotFoundError = (err) => {
|
|
794
|
+
if (typeof err === "object" && err !== null && "name" in err) {
|
|
795
|
+
const name = err.name;
|
|
796
|
+
return name === "ContractFunctionExecutionError" || name === "ContractFunctionRevertedError";
|
|
797
|
+
}
|
|
798
|
+
return false;
|
|
923
799
|
};
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
800
|
+
const results = await Promise.all([v3 ? getPanopticPoolFromPoolId({
|
|
801
|
+
version: "v3",
|
|
802
|
+
client,
|
|
803
|
+
sfpmAddress: v3.sfpmAddress,
|
|
804
|
+
factoryAddress: v3.factoryAddress,
|
|
805
|
+
riskEngine,
|
|
806
|
+
poolId
|
|
807
|
+
}).catch((err) => {
|
|
808
|
+
if (isNotFoundError(err)) return zeroAddress;
|
|
809
|
+
throw err;
|
|
810
|
+
}) : Promise.resolve(zeroAddress), v4 ? getPanopticPoolFromPoolId({
|
|
811
|
+
version: "v4",
|
|
812
|
+
client,
|
|
813
|
+
sfpmAddress: v4.sfpmAddress,
|
|
814
|
+
factoryAddress: v4.factoryAddress,
|
|
815
|
+
riskEngine,
|
|
816
|
+
poolId
|
|
817
|
+
}).catch((err) => {
|
|
818
|
+
if (isNotFoundError(err)) return zeroAddress;
|
|
819
|
+
throw err;
|
|
820
|
+
}) : Promise.resolve(zeroAddress)]);
|
|
821
|
+
const [v3Result, v4Result] = results;
|
|
822
|
+
if (v3Result !== zeroAddress) return {
|
|
823
|
+
panopticPoolAddress: v3Result,
|
|
824
|
+
version: "v3"
|
|
944
825
|
};
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const floorMultiple = price.numerator * spacing.denominator / (price.denominator * spacing.units);
|
|
949
|
-
for (const multiple of [floorMultiple, floorMultiple + 1n]) {
|
|
950
|
-
if (multiple <= 0n) continue;
|
|
951
|
-
const orientedTick = priceToTick(formatGridPrice(multiple, spacing), params.assetDecimals, params.quoteDecimals);
|
|
952
|
-
const poolTick = params.asset === 0n ? orientedTick : -orientedTick;
|
|
953
|
-
if (roundToTickSpacing(poolTick, tickSpacing) === strike) return true;
|
|
954
|
-
}
|
|
955
|
-
return false;
|
|
956
|
-
}
|
|
957
|
-
function ceilDiv$1(value, divisor) {
|
|
958
|
-
const quotient = value / divisor;
|
|
959
|
-
const remainder = value % divisor;
|
|
960
|
-
return remainder > 0n ? quotient + 1n : quotient;
|
|
961
|
-
}
|
|
962
|
-
function floorDiv(value, divisor) {
|
|
963
|
-
const quotient = value / divisor;
|
|
964
|
-
const remainder = value % divisor;
|
|
965
|
-
return remainder < 0n ? quotient - 1n : quotient;
|
|
966
|
-
}
|
|
967
|
-
function resolveLegs(legs) {
|
|
968
|
-
if (legs.length === 0 || legs.length > Number(TOKEN_ID_BITS.MAX_LEGS)) invalid$1("legs must contain between 1 and 4 entries");
|
|
969
|
-
const resolved = legs.map((leg, index) => {
|
|
970
|
-
if (leg.optionType !== "call" && leg.optionType !== "put") invalid$1(`leg ${index} has an invalid optionType`);
|
|
971
|
-
if (typeof leg.isLong !== "boolean") invalid$1(`leg ${index} has an invalid isLong value`);
|
|
972
|
-
if (leg.optionRatio < 1n || leg.optionRatio > LEG_LIMITS.MAX_RATIO) invalid$1(`leg ${index} optionRatio must be between 1 and ${LEG_LIMITS.MAX_RATIO}`);
|
|
973
|
-
const riskPartner = leg.riskPartner ?? BigInt(index);
|
|
974
|
-
if (riskPartner < 0n || riskPartner >= BigInt(legs.length)) invalid$1(`leg ${index} references an inactive risk partner`);
|
|
975
|
-
return {
|
|
976
|
-
...leg,
|
|
977
|
-
riskPartner
|
|
978
|
-
};
|
|
979
|
-
});
|
|
980
|
-
for (const [index, leg] of resolved.entries()) {
|
|
981
|
-
if (leg.riskPartner === BigInt(index)) continue;
|
|
982
|
-
const partner = resolved[Number(leg.riskPartner)];
|
|
983
|
-
if (partner === void 0 || partner.riskPartner !== BigInt(index)) invalid$1(`leg ${index} has a non-mutual risk partner`);
|
|
984
|
-
}
|
|
985
|
-
return resolved;
|
|
986
|
-
}
|
|
987
|
-
function getCandidateStrikes(params, spacing, tickSpacing, halfWidth) {
|
|
988
|
-
const lower = MIN_TICK + halfWidth > params.currentTick - halfWidth + 1n ? MIN_TICK + halfWidth : params.currentTick - halfWidth + 1n;
|
|
989
|
-
const upper = MAX_TICK - halfWidth < params.currentTick + halfWidth - 1n ? MAX_TICK - halfWidth : params.currentTick + halfWidth - 1n;
|
|
990
|
-
const firstStrike = ceilDiv$1(lower, tickSpacing) * tickSpacing;
|
|
991
|
-
const lastStrike = floorDiv(upper, tickSpacing) * tickSpacing;
|
|
992
|
-
const strikes = [];
|
|
993
|
-
for (let strike = firstStrike; strike <= lastStrike; strike += tickSpacing) if (isPriceGridStrike(strike, params, spacing, tickSpacing)) strikes.push(strike);
|
|
994
|
-
if (params.asset === 1n) strikes.reverse();
|
|
995
|
-
return strikes;
|
|
996
|
-
}
|
|
997
|
-
function buildTokenId$1(poolId, strike, width, asset, legs) {
|
|
998
|
-
const builder = createTokenIdBuilder(poolId);
|
|
999
|
-
for (const leg of legs) {
|
|
1000
|
-
const config = {
|
|
1001
|
-
strike,
|
|
1002
|
-
width,
|
|
1003
|
-
optionRatio: leg.optionRatio,
|
|
1004
|
-
isLong: leg.isLong,
|
|
1005
|
-
riskPartner: leg.riskPartner,
|
|
1006
|
-
asset
|
|
1007
|
-
};
|
|
1008
|
-
if (leg.optionType === "call") builder.addCall(config);
|
|
1009
|
-
else builder.addPut(config);
|
|
1010
|
-
}
|
|
1011
|
-
return builder.build();
|
|
1012
|
-
}
|
|
1013
|
-
function dividePositionSize$1(positionSize, count) {
|
|
1014
|
-
if (positionSize < count) invalid$1(`positionSize ${positionSize} is too small for ${count} nonzero positions`);
|
|
1015
|
-
const quotient = positionSize / count;
|
|
1016
|
-
const remainder = positionSize % count;
|
|
1017
|
-
const sizes = [];
|
|
1018
|
-
for (let index = 0n; index < count; index += 1n) sizes.push(quotient + (index < remainder ? 1n : 0n));
|
|
1019
|
-
return sizes;
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Generate a fixed quote-price lattice of co-strike TokenIds whose liquidity
|
|
1023
|
-
* ranges strictly contain the current tick. Returned arrays are ordered by
|
|
1024
|
-
* ascending quote strike price and are ready for `dispatch()`.
|
|
1025
|
-
*/
|
|
1026
|
-
function generateOverlappingTokenIds(params) {
|
|
1027
|
-
if (params.poolId < 0n || params.poolId > MAX_UINT64$2) invalid$1("poolId must fit in uint64");
|
|
1028
|
-
if (params.currentTick < MIN_TICK || params.currentTick > MAX_TICK) invalid$1(`currentTick must be between ${MIN_TICK} and ${MAX_TICK}`);
|
|
1029
|
-
if (params.asset !== 0n && params.asset !== 1n) invalid$1("asset must be 0 or 1");
|
|
1030
|
-
if (params.assetDecimals < 0n || params.assetDecimals > MAX_TOKEN_DECIMALS || params.quoteDecimals < 0n || params.quoteDecimals > MAX_TOKEN_DECIMALS) invalid$1("assetDecimals and quoteDecimals must be between 0 and 255");
|
|
1031
|
-
if (params.positionSize <= 0n || params.positionSize > MAX_UINT128$3) invalid$1("positionSize must be between 1 and uint128.max");
|
|
1032
|
-
const tickSpacing = decodeTickSpacing$1(params.poolId);
|
|
1033
|
-
if (tickSpacing <= 0n) invalid$1("poolId tick spacing must be positive");
|
|
1034
|
-
const standardTickWidth = STANDARD_TICK_WIDTHS[params.timescale];
|
|
1035
|
-
if (standardTickWidth === void 0) invalid$1(`unknown timescale ${String(params.timescale)}`);
|
|
1036
|
-
const width = (standardTickWidth + tickSpacing - 1n) / tickSpacing;
|
|
1037
|
-
if (width <= 0n || width > LEG_LIMITS.MAX_WIDTH) invalid$1(`timescale width does not fit in the TokenId width field for tick spacing ${tickSpacing}`);
|
|
1038
|
-
const halfWidth = width * tickSpacing / 2n;
|
|
1039
|
-
if (halfWidth <= 0n) invalid$1("timescale width must span at least two ticks");
|
|
1040
|
-
const spacing = parsePriceSpacing(params.strikePriceSpacing, params.quoteDecimals);
|
|
1041
|
-
const legs = resolveLegs(params.legs);
|
|
1042
|
-
const strikes = getCandidateStrikes(params, spacing, tickSpacing, halfWidth);
|
|
1043
|
-
if (strikes.length === 0) invalid$1("no price-grid strikes overlap the current tick");
|
|
1044
|
-
return {
|
|
1045
|
-
positionIdList: strikes.map((strike) => buildTokenId$1(params.poolId, strike, width, params.asset, legs)),
|
|
1046
|
-
positionSizes: dividePositionSize$1(params.positionSize, BigInt(strikes.length))
|
|
826
|
+
if (v4Result !== zeroAddress) return {
|
|
827
|
+
panopticPoolAddress: v4Result,
|
|
828
|
+
version: "v4"
|
|
1047
829
|
};
|
|
830
|
+
throw new Error(`No PanopticPool found for poolId ${poolId}`);
|
|
1048
831
|
}
|
|
1049
832
|
|
|
1050
833
|
//#endregion
|
|
1051
|
-
//#region src/panoptic/v2/
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
for (const leg of legs) {
|
|
1068
|
-
const partner = byIndex.get(leg.riskPartner);
|
|
1069
|
-
if (partner === void 0) invalid(`leg ${leg.index} references an inactive risk partner`);
|
|
1070
|
-
if (partner.index !== leg.index && partner.riskPartner !== leg.index) invalid(`leg ${leg.index} has a non-mutual risk partner`);
|
|
834
|
+
//#region src/panoptic/v2/reads/minePoolAddressLocal.ts
|
|
835
|
+
/**
|
|
836
|
+
* keccak256 of the CREATE3 proxy initcode used by ClonesWithImmutableArgs.
|
|
837
|
+
* Source: packages/panoptic-v2-core/lib/clones-with-immutable-args/src/ClonesWithImmutableArgs.sol
|
|
838
|
+
*/
|
|
839
|
+
const CREATE3_PROXY_BYTECODE_HASH = "0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f";
|
|
840
|
+
const MASK_80 = (1n << 80n) - 1n;
|
|
841
|
+
const MASK_40 = (1n << 40n) - 1n;
|
|
842
|
+
const MASK_96 = (1n << 96n) - 1n;
|
|
843
|
+
/** Encode a BigInt as a big-endian fixed-length byte array. */
|
|
844
|
+
function bigintToBytes(value, byteLength) {
|
|
845
|
+
const bytes = new Uint8Array(byteLength);
|
|
846
|
+
let v = value;
|
|
847
|
+
for (let i = byteLength - 1; i >= 0; i--) {
|
|
848
|
+
bytes[i] = Number(v & 0xffn);
|
|
849
|
+
v >>= 8n;
|
|
1071
850
|
}
|
|
1072
|
-
return
|
|
1073
|
-
}
|
|
1074
|
-
function isCall(leg) {
|
|
1075
|
-
return leg.tokenType === leg.asset;
|
|
1076
|
-
}
|
|
1077
|
-
function isEconomicStraddlePair(left, right) {
|
|
1078
|
-
return isCall(left) !== isCall(right) && left.asset === right.asset && left.optionRatio === right.optionRatio && left.isLong === right.isLong && left.strike === right.strike && left.width === right.width;
|
|
851
|
+
return bytes;
|
|
1079
852
|
}
|
|
1080
|
-
/**
|
|
1081
|
-
function
|
|
1082
|
-
const
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if (consumed.has(leg.index)) continue;
|
|
1087
|
-
if (leg.riskPartner !== leg.index) {
|
|
1088
|
-
const riskPartner = byIndex.get(leg.riskPartner);
|
|
1089
|
-
if (riskPartner === void 0) invalid(`mutual risk pair containing leg ${leg.index} cannot be separated`);
|
|
1090
|
-
consumed.add(leg.index);
|
|
1091
|
-
consumed.add(riskPartner.index);
|
|
1092
|
-
groups.push([leg, riskPartner]);
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
const partner = optionLegs.find((candidate) => candidate.index > leg.index && !consumed.has(candidate.index) && candidate.riskPartner === candidate.index && isEconomicStraddlePair(leg, candidate));
|
|
1096
|
-
consumed.add(leg.index);
|
|
1097
|
-
if (partner === void 0) {
|
|
1098
|
-
groups.push([leg]);
|
|
1099
|
-
continue;
|
|
1100
|
-
}
|
|
1101
|
-
consumed.add(partner.index);
|
|
1102
|
-
groups.push([leg, partner]);
|
|
1103
|
-
}
|
|
1104
|
-
return groups;
|
|
853
|
+
/** Parse a 0x-prefixed address into 20 bytes. */
|
|
854
|
+
function addressToBytes(addr) {
|
|
855
|
+
const hex = addr.slice(2).padStart(40, "0");
|
|
856
|
+
const bytes = new Uint8Array(20);
|
|
857
|
+
for (let i = 0; i < 20; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
858
|
+
return bytes;
|
|
1105
859
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const indexBySource = new Map(legs.map((leg, index) => [leg.index, BigInt(index)]));
|
|
1113
|
-
let output = poolId;
|
|
1114
|
-
for (const [index, leg] of legs.entries()) {
|
|
1115
|
-
const newIndex = BigInt(index);
|
|
1116
|
-
const { strike, width } = transform(leg);
|
|
1117
|
-
if (strike < LEG_LIMITS.MIN_STRIKE || strike > LEG_LIMITS.MAX_STRIKE) invalid(`generated strike ${strike} does not fit in int24`);
|
|
1118
|
-
if (width < 0n || width > LEG_LIMITS.MAX_WIDTH) invalid(`generated width ${width} does not fit in the TokenId width field`);
|
|
1119
|
-
output = addLegToTokenId(output, {
|
|
1120
|
-
index: newIndex,
|
|
1121
|
-
asset: leg.asset,
|
|
1122
|
-
optionRatio: leg.optionRatio,
|
|
1123
|
-
isLong: leg.isLong ? 1n : 0n,
|
|
1124
|
-
tokenType: leg.tokenType,
|
|
1125
|
-
riskPartner: remapRiskPartner(leg, indexBySource),
|
|
1126
|
-
strike,
|
|
1127
|
-
width
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
return output;
|
|
1131
|
-
}
|
|
1132
|
-
function childWidths(sourceWidth, targetWidth) {
|
|
1133
|
-
if (sourceWidth <= targetWidth) return [sourceWidth];
|
|
1134
|
-
const fullWidthCount = sourceWidth / targetWidth;
|
|
1135
|
-
const remainder = sourceWidth % targetWidth;
|
|
1136
|
-
const widths = [];
|
|
1137
|
-
for (let remaining = fullWidthCount; remaining > 0n; remaining -= 1n) widths.push(targetWidth);
|
|
1138
|
-
if (remainder > 0n) widths.push(remainder);
|
|
1139
|
-
return widths;
|
|
1140
|
-
}
|
|
1141
|
-
function dividePositionSize(positionSize, count) {
|
|
1142
|
-
const countBigInt = BigInt(count);
|
|
1143
|
-
if (positionSize < countBigInt) invalid(`positionSize ${positionSize} is too small for ${count} nonzero child positions`);
|
|
1144
|
-
const quotient = positionSize / countBigInt;
|
|
1145
|
-
const remainder = positionSize % countBigInt;
|
|
1146
|
-
return Array.from({ length: count }, (_, index) => quotient + (BigInt(index) < remainder ? 1n : 0n));
|
|
1147
|
-
}
|
|
1148
|
-
function splitGroup(poolId, group, tickSpacing, targetWidth, positionSize) {
|
|
1149
|
-
const sourceWidth = group[0]?.width;
|
|
1150
|
-
if (sourceWidth === void 0 || sourceWidth === 0n) invalid("option group must have nonzero width");
|
|
1151
|
-
if (group.some((leg) => leg.width !== sourceWidth)) invalid("all legs in an option group must have the same width");
|
|
1152
|
-
const widths = childWidths(sourceWidth, targetWidth);
|
|
1153
|
-
const positionSizes = dividePositionSize(positionSize, widths.length);
|
|
1154
|
-
let consumedWidth = 0n;
|
|
1155
|
-
const positionIdList = widths.map((width) => {
|
|
1156
|
-
const offset = consumedWidth;
|
|
1157
|
-
consumedWidth += width;
|
|
1158
|
-
return buildTokenId(poolId, group, (leg) => {
|
|
1159
|
-
const sourceLower = leg.strike - sourceWidth * tickSpacing / 2n;
|
|
1160
|
-
const childLower = sourceLower + offset * tickSpacing;
|
|
1161
|
-
const strike = childLower + width * tickSpacing / 2n;
|
|
1162
|
-
return {
|
|
1163
|
-
strike,
|
|
1164
|
-
width
|
|
1165
|
-
};
|
|
1166
|
-
});
|
|
1167
|
-
});
|
|
1168
|
-
return {
|
|
1169
|
-
positionIdList,
|
|
1170
|
-
positionSizes
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
/**
|
|
1174
|
-
* Split every standalone option, economic straddle, or mutual risk pair in a
|
|
1175
|
-
* TokenId into a shorter standard timescale. Width-zero credit/loan legs are
|
|
1176
|
-
* emitted together as one final position. Returned arrays are parallel and
|
|
1177
|
-
* ready for `dispatch()`.
|
|
1178
|
-
*
|
|
1179
|
-
* Each option group divides `positionSize` independently across its children.
|
|
1180
|
-
* Integer remainder units are assigned to the lower children first.
|
|
1181
|
-
*/
|
|
1182
|
-
function splitTokenIdByTimescale(tokenId, positionSize, targetTimescale) {
|
|
1183
|
-
const legs = validateInput(tokenId, positionSize);
|
|
1184
|
-
const tickSpacing = decodeTickSpacing$1(tokenId);
|
|
1185
|
-
if (tickSpacing <= 0n) invalid("tokenId tick spacing must be positive");
|
|
1186
|
-
const targetTickWidth = STANDARD_TICK_WIDTHS[targetTimescale];
|
|
1187
|
-
if (targetTickWidth === void 0) invalid(`unknown target timescale ${String(targetTimescale)}`);
|
|
1188
|
-
const targetWidth = (targetTickWidth + tickSpacing - 1n) / tickSpacing;
|
|
1189
|
-
const poolId = tokenId & POOL_ID_MASK$2;
|
|
1190
|
-
const optionLegs = legs.filter((leg) => leg.width > 0n);
|
|
1191
|
-
const fundingLegs = legs.filter((leg) => leg.width === 0n);
|
|
1192
|
-
const positionIdList = [];
|
|
1193
|
-
const positionSizes = [];
|
|
1194
|
-
for (const group of groupOptionLegs(optionLegs)) {
|
|
1195
|
-
const split = splitGroup(poolId, group, tickSpacing, targetWidth, positionSize);
|
|
1196
|
-
positionIdList.push(...split.positionIdList);
|
|
1197
|
-
positionSizes.push(...split.positionSizes);
|
|
1198
|
-
}
|
|
1199
|
-
if (fundingLegs.length > 0) {
|
|
1200
|
-
positionIdList.push(buildTokenId(poolId, fundingLegs, (leg) => ({
|
|
1201
|
-
strike: leg.strike,
|
|
1202
|
-
width: leg.width
|
|
1203
|
-
})));
|
|
1204
|
-
positionSizes.push(positionSize);
|
|
1205
|
-
}
|
|
1206
|
-
return {
|
|
1207
|
-
positionIdList,
|
|
1208
|
-
positionSizes
|
|
1209
|
-
};
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
//#endregion
|
|
1213
|
-
//#region src/panoptic/v2/sfpmSwap/calldata.ts
|
|
1214
|
-
/**
|
|
1215
|
-
* Encode the `multicall([mint, burn])` for a swap plan.
|
|
1216
|
-
*
|
|
1217
|
-
* The order is always `[mint, burn]` — the ERC1155 must be minted before it is
|
|
1218
|
-
* burned. Which call carries the inverted (swap) limits is decided in the plan.
|
|
1219
|
-
*/
|
|
1220
|
-
function buildSfpmSwapCalldata(plan) {
|
|
1221
|
-
const mintData = encodeFunctionData({
|
|
1222
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1223
|
-
functionName: "mintTokenizedPosition",
|
|
1224
|
-
args: [
|
|
1225
|
-
plan.poolKey,
|
|
1226
|
-
plan.tokenId,
|
|
1227
|
-
plan.positionSize,
|
|
1228
|
-
plan.mintTickLimits[0],
|
|
1229
|
-
plan.mintTickLimits[1]
|
|
1230
|
-
]
|
|
1231
|
-
});
|
|
1232
|
-
const burnData = encodeFunctionData({
|
|
1233
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1234
|
-
functionName: "burnTokenizedPosition",
|
|
1235
|
-
args: [
|
|
1236
|
-
plan.poolKey,
|
|
1237
|
-
plan.tokenId,
|
|
1238
|
-
plan.positionSize,
|
|
1239
|
-
plan.burnTickLimits[0],
|
|
1240
|
-
plan.burnTickLimits[1]
|
|
1241
|
-
]
|
|
1242
|
-
});
|
|
1243
|
-
const multicallData = encodeFunctionData({
|
|
1244
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1245
|
-
functionName: "multicall",
|
|
1246
|
-
args: [[mintData, burnData]]
|
|
1247
|
-
});
|
|
1248
|
-
return {
|
|
1249
|
-
multicallData,
|
|
1250
|
-
mintData,
|
|
1251
|
-
burnData
|
|
1252
|
-
};
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
//#endregion
|
|
1256
|
-
//#region src/panoptic/v2/sfpmSwap/init.ts
|
|
1257
|
-
/**
|
|
1258
|
-
* Resolve the `uint64` SFPM poolId for a Uniswap v3 pool, initializing it if needed.
|
|
1259
|
-
*
|
|
1260
|
-
* `initializeAMMPool` is permissionless and idempotent and **returns the poolId**
|
|
1261
|
-
* whether or not the pool was already registered, so a `simulateContract` call is
|
|
1262
|
-
* enough to learn the id without sending a transaction. Always resolve the id this
|
|
1263
|
-
* way rather than encoding it offline — the SFPM can collision-increment ids.
|
|
1264
|
-
*/
|
|
1265
|
-
async function fetchSfpmV3PoolId(params) {
|
|
1266
|
-
const { client, sfpmAddress, token0, token1, fee } = params;
|
|
1267
|
-
const vegoid = params.vegoid ?? Number(DEFAULT_VEGOID);
|
|
1268
|
-
const { result } = await client.simulateContract({
|
|
1269
|
-
address: sfpmAddress,
|
|
1270
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1271
|
-
functionName: "initializeAMMPool",
|
|
1272
|
-
args: [
|
|
1273
|
-
token0,
|
|
1274
|
-
token1,
|
|
1275
|
-
fee,
|
|
1276
|
-
vegoid
|
|
1277
|
-
]
|
|
1278
|
-
});
|
|
1279
|
-
return BigInt(result);
|
|
1280
|
-
}
|
|
1281
|
-
/**
|
|
1282
|
-
* Ensure a Uniswap v3 pool is registered on the SFPM, sending `initializeAMMPool`
|
|
1283
|
-
* only if it is not already registered. Returns the resolved poolId.
|
|
1284
|
-
*
|
|
1285
|
-
* When `expectedPool` is given, the resolved id is checked to map back to it via
|
|
1286
|
-
* `getUniswapV3PoolFromId` — guards against a wrong token/fee triple.
|
|
1287
|
-
*/
|
|
1288
|
-
async function ensureSfpmV3PoolInitialized(params) {
|
|
1289
|
-
const { client, wallet, sfpmAddress, token0, token1, fee, expectedPool } = params;
|
|
1290
|
-
const vegoid = params.vegoid ?? Number(DEFAULT_VEGOID);
|
|
1291
|
-
const poolId = await fetchSfpmV3PoolId({
|
|
1292
|
-
client,
|
|
1293
|
-
sfpmAddress,
|
|
1294
|
-
token0,
|
|
1295
|
-
token1,
|
|
1296
|
-
fee,
|
|
1297
|
-
vegoid
|
|
1298
|
-
});
|
|
1299
|
-
const registered = await client.readContract({
|
|
1300
|
-
address: sfpmAddress,
|
|
1301
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1302
|
-
functionName: "getUniswapV3PoolFromId",
|
|
1303
|
-
args: [poolId]
|
|
1304
|
-
});
|
|
1305
|
-
let initialized = false;
|
|
1306
|
-
let resolved = registered;
|
|
1307
|
-
if (getAddress(registered) === zeroAddress) {
|
|
1308
|
-
const account = wallet.account;
|
|
1309
|
-
if (account === void 0) throw new PanopticError("wallet client has no account");
|
|
1310
|
-
const hash = await wallet.writeContract({
|
|
1311
|
-
account,
|
|
1312
|
-
chain: wallet.chain ?? null,
|
|
1313
|
-
address: sfpmAddress,
|
|
1314
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1315
|
-
functionName: "initializeAMMPool",
|
|
1316
|
-
args: [
|
|
1317
|
-
token0,
|
|
1318
|
-
token1,
|
|
1319
|
-
fee,
|
|
1320
|
-
vegoid
|
|
1321
|
-
]
|
|
1322
|
-
});
|
|
1323
|
-
await client.waitForTransactionReceipt({ hash });
|
|
1324
|
-
initialized = true;
|
|
1325
|
-
resolved = await client.readContract({
|
|
1326
|
-
address: sfpmAddress,
|
|
1327
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1328
|
-
functionName: "getUniswapV3PoolFromId",
|
|
1329
|
-
args: [poolId]
|
|
1330
|
-
});
|
|
1331
|
-
}
|
|
1332
|
-
if (expectedPool !== void 0 && getAddress(resolved) !== getAddress(expectedPool)) throw new WrongUniswapPoolError(new PanopticError(`SFPM poolId ${poolId} resolves to ${resolved}, expected ${expectedPool}`));
|
|
1333
|
-
return {
|
|
1334
|
-
poolId,
|
|
1335
|
-
initialized
|
|
1336
|
-
};
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
//#endregion
|
|
1340
|
-
//#region src/panoptic/v2/sfpmSwap/plan.ts
|
|
1341
|
-
/** Wide, non-inverted band for the paired (non-swapping) call — never triggers a swap. */
|
|
1342
|
-
const WIDE_LIMITS = [Number(MIN_TICK) + 1, Number(MAX_TICK) - 1];
|
|
1343
|
-
/**
|
|
1344
|
-
* Convert a slippage tolerance in bps to a conservative Uniswap tick distance.
|
|
1345
|
-
*
|
|
1346
|
-
* Ticks are 1.0001^tick, so each tick ≈ 1 bps. This walks up powers of 1.0001
|
|
1347
|
-
* until the cumulative price move covers `slippageBps`, matching the hedger-bot's
|
|
1348
|
-
* dispatch-path helper so both swap paths agree on band width.
|
|
1349
|
-
*/
|
|
1350
|
-
function slippageBpsToTickDistance(slippageBps) {
|
|
1351
|
-
if (slippageBps < 0n || slippageBps > 1000n) throw new PanopticError(`slippage bps ${slippageBps} out of bounds (0..1000)`);
|
|
1352
|
-
if (slippageBps === 0n) return 0n;
|
|
1353
|
-
let numerator = 1n;
|
|
1354
|
-
let denominator = 1n;
|
|
1355
|
-
let ticks = 0n;
|
|
1356
|
-
while (numerator * 10000n < denominator * (10000n + slippageBps)) {
|
|
1357
|
-
numerator *= 10001n;
|
|
1358
|
-
denominator *= 10000n;
|
|
1359
|
-
ticks += 1n;
|
|
1360
|
-
}
|
|
1361
|
-
return ticks;
|
|
1362
|
-
}
|
|
1363
|
-
/**
|
|
1364
|
-
* Build the swap plan.
|
|
1365
|
-
*
|
|
1366
|
-
* Mechanism (verified in the Phase 0 fork test):
|
|
1367
|
-
* - A single-leg **loan** tokenId (width=0, isLong=false, `asset == tokenType`) moves
|
|
1368
|
-
* exactly `positionSize` of the `tokenType` token when its call carries inverted
|
|
1369
|
-
* tick limits (`low > high`); the paired call uses a wide band and moves nothing.
|
|
1370
|
-
* - `exactIn`: swap on the **mint**; `tokenType` = the **input** token index.
|
|
1371
|
-
* - `exactOut`: swap on the **burn** (isLong flips → exact-output); `tokenType` = the
|
|
1372
|
-
* **output** token index.
|
|
1373
|
-
*
|
|
1374
|
-
* The inverted band is centered on `currentTick` at ±`slippageBpsToTickDistance`,
|
|
1375
|
-
* which the SFPM re-sorts and enforces as an open interval on the post-swap tick.
|
|
1376
|
-
*/
|
|
1377
|
-
function buildSfpmSwapPlan(params) {
|
|
1378
|
-
const { sfpmAddress, poolAddress, poolId, kind, zeroForOne, amount, currentTick, slippageBps } = params;
|
|
1379
|
-
if (amount <= 0n) throw new PanopticError(`swap amount must be positive (got ${amount})`);
|
|
1380
|
-
const distance = slippageBpsToTickDistance(slippageBps);
|
|
1381
|
-
if (distance < 1n) throw new PanopticError(`slippageBps ${slippageBps} yields a zero-width tick band; use a larger tolerance`);
|
|
1382
|
-
const tokenType = kind === "exactIn" ? zeroForOne ? 0n : 1n : zeroForOne ? 1n : 0n;
|
|
1383
|
-
const tokenId = createTokenIdBuilder(poolId).addLoan({
|
|
1384
|
-
asset: tokenType,
|
|
1385
|
-
tokenType,
|
|
1386
|
-
strike: 0n
|
|
1387
|
-
}).build();
|
|
1388
|
-
const d = Number(distance);
|
|
1389
|
-
const low = Math.min(Number(MAX_TICK), currentTick + d);
|
|
1390
|
-
const high = Math.max(Number(MIN_TICK), currentTick - d);
|
|
1391
|
-
if (low <= high) throw new PanopticError(`slippageBps ${slippageBps} at tick ${currentTick} yields a non-inverted band after clamping`);
|
|
1392
|
-
const invertedLimits = [low, high];
|
|
1393
|
-
const swapOn = kind === "exactIn" ? "mint" : "burn";
|
|
1394
|
-
return {
|
|
1395
|
-
sfpmAddress,
|
|
1396
|
-
poolAddress,
|
|
1397
|
-
poolKey: encodeAbiParameters([{ type: "address" }], [poolAddress]),
|
|
1398
|
-
tokenId,
|
|
1399
|
-
positionSize: amount,
|
|
1400
|
-
mintTickLimits: swapOn === "mint" ? invertedLimits : WIDE_LIMITS,
|
|
1401
|
-
burnTickLimits: swapOn === "burn" ? invertedLimits : WIDE_LIMITS,
|
|
1402
|
-
swapOn,
|
|
1403
|
-
kind
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
//#endregion
|
|
1408
|
-
//#region src/panoptic/v2/sfpmSwap/quote.ts
|
|
1409
|
-
const UINT128 = 1n << 128n;
|
|
1410
|
-
const INT128_MAX = (1n << 127n) - 1n;
|
|
1411
|
-
/** Extract the two signed 128-bit slots from a packed `LeftRightSigned` int256. */
|
|
1412
|
-
function unpackLeftRightSigned(packed) {
|
|
1413
|
-
const u = packed < 0n ? packed + (1n << 256n) : packed;
|
|
1414
|
-
const toInt128 = (half) => half > INT128_MAX ? half - UINT128 : half;
|
|
1415
|
-
return {
|
|
1416
|
-
right: toInt128(u & UINT128 - 1n),
|
|
1417
|
-
left: toInt128(u >> 128n & UINT128 - 1n)
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
/** Wrap an unknown thrown value as a PanopticError, decoding Panoptic reverts when possible. */
|
|
1421
|
-
function toPanopticError(err) {
|
|
1422
|
-
if (err instanceof PanopticError) return err;
|
|
1423
|
-
const parsed = parsePanopticError(err);
|
|
1424
|
-
if (parsed) return parsed.error;
|
|
1425
|
-
return new PanopticError(err instanceof Error ? err.message : "SFPM swap simulation failed", err instanceof Error ? err : void 0);
|
|
1426
|
-
}
|
|
1427
|
-
/**
|
|
1428
|
-
* Quote a swap by simulating `SFPM.multicall([mint, burn])` and decoding the swap
|
|
1429
|
-
* call's `totalMoved` return. Authoritative — captures the width-0 loan-leg wei
|
|
1430
|
-
* rounding a raw QuoterV2 quote would miss.
|
|
1431
|
-
*/
|
|
1432
|
-
async function quoteSfpmSwap(params) {
|
|
1433
|
-
const { client, plan, account, stateOverride, blockNumber } = params;
|
|
1434
|
-
const { mintData, burnData } = buildSfpmSwapCalldata(plan);
|
|
1435
|
-
const _meta = await getBlockMeta({
|
|
1436
|
-
client,
|
|
1437
|
-
blockNumber
|
|
1438
|
-
});
|
|
1439
|
-
try {
|
|
1440
|
-
const { result } = await client.simulateContract({
|
|
1441
|
-
account,
|
|
1442
|
-
address: plan.sfpmAddress,
|
|
1443
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1444
|
-
functionName: "multicall",
|
|
1445
|
-
args: [[mintData, burnData]],
|
|
1446
|
-
blockNumber,
|
|
1447
|
-
stateOverride
|
|
1448
|
-
});
|
|
1449
|
-
const swapIndex = plan.swapOn === "mint" ? 0 : 1;
|
|
1450
|
-
const decoded = decodeFunctionResult({
|
|
1451
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1452
|
-
functionName: plan.swapOn === "mint" ? "mintTokenizedPosition" : "burnTokenizedPosition",
|
|
1453
|
-
data: result[swapIndex]
|
|
1454
|
-
});
|
|
1455
|
-
const finalTick = Number(decoded[2]);
|
|
1456
|
-
const { right, left } = unpackLeftRightSigned(decoded[1]);
|
|
1457
|
-
const inSlot = [right, left].find((s) => s > 0n);
|
|
1458
|
-
const outSlot = [right, left].find((s) => s < 0n);
|
|
1459
|
-
if (inSlot === void 0 || outSlot === void 0) return {
|
|
1460
|
-
success: false,
|
|
1461
|
-
error: new PanopticError(`SFPM swap simulation moved no tokens (totalMoved=${decoded[1]})`),
|
|
1462
|
-
_meta
|
|
1463
|
-
};
|
|
1464
|
-
return {
|
|
1465
|
-
success: true,
|
|
1466
|
-
data: {
|
|
1467
|
-
amountIn: inSlot,
|
|
1468
|
-
amountOut: -outSlot,
|
|
1469
|
-
finalTick
|
|
1470
|
-
},
|
|
1471
|
-
gasEstimate: 0n,
|
|
1472
|
-
_meta
|
|
1473
|
-
};
|
|
1474
|
-
} catch (err) {
|
|
1475
|
-
return {
|
|
1476
|
-
success: false,
|
|
1477
|
-
error: toPanopticError(err),
|
|
1478
|
-
_meta
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
//#endregion
|
|
1484
|
-
//#region src/panoptic/v2/reads/sfpm.ts
|
|
1485
|
-
/**
|
|
1486
|
-
* Resolve an SFPM poolId to its corresponding Uniswap V3 pool address.
|
|
1487
|
-
*
|
|
1488
|
-
* Calls `SemiFungiblePositionManagerV3.getUniswapV3PoolFromId(poolId)`.
|
|
1489
|
-
*/
|
|
1490
|
-
async function getUniswapV3PoolFromId(params) {
|
|
1491
|
-
const { client, sfpmAddress, poolId } = params;
|
|
1492
|
-
return client.readContract({
|
|
1493
|
-
address: sfpmAddress,
|
|
1494
|
-
abi: semiFungiblePositionManagerV3Abi,
|
|
1495
|
-
functionName: "getUniswapV3PoolFromId",
|
|
1496
|
-
args: [poolId]
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Resolve an SFPM poolId to its corresponding Uniswap V4 pool key.
|
|
1501
|
-
*
|
|
1502
|
-
* Calls `SemiFungiblePositionManagerV4.getUniswapV4PoolKeyFromId(poolId)`.
|
|
1503
|
-
*/
|
|
1504
|
-
async function getUniswapV4PoolKeyFromId(params) {
|
|
1505
|
-
const { client, sfpmAddress, poolId } = params;
|
|
1506
|
-
const raw = await client.readContract({
|
|
1507
|
-
address: sfpmAddress,
|
|
1508
|
-
abi: semiFungiblePositionManagerV4Abi,
|
|
1509
|
-
functionName: "getUniswapV4PoolKeyFromId",
|
|
1510
|
-
args: [poolId]
|
|
1511
|
-
});
|
|
1512
|
-
return {
|
|
1513
|
-
currency0: raw.currency0,
|
|
1514
|
-
currency1: raw.currency1,
|
|
1515
|
-
fee: BigInt(raw.fee),
|
|
1516
|
-
tickSpacing: BigInt(raw.tickSpacing),
|
|
1517
|
-
hooks: raw.hooks
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
/**
|
|
1521
|
-
* Get the enforced tick limits for a pool from the SFPM.
|
|
1522
|
-
*
|
|
1523
|
-
* @param params - The parameters
|
|
1524
|
-
* @returns The min and max enforced ticks
|
|
1525
|
-
*/
|
|
1526
|
-
async function getEnforcedTickLimits(params) {
|
|
1527
|
-
const { client, sfpmAddress, poolId } = params;
|
|
1528
|
-
const [minTick, maxTick] = await client.readContract({
|
|
1529
|
-
address: sfpmAddress,
|
|
1530
|
-
abi: semiFungiblePositionManagerV4Abi,
|
|
1531
|
-
functionName: "getEnforcedTickLimits",
|
|
1532
|
-
args: [poolId]
|
|
1533
|
-
});
|
|
1534
|
-
return {
|
|
1535
|
-
minEnforcedTick: minTick,
|
|
1536
|
-
maxEnforcedTick: maxTick
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
/**
|
|
1540
|
-
* Fetch liquidity breakdown for a batch of chunks via SFPM.getAccountLiquidity().
|
|
1541
|
-
*
|
|
1542
|
-
* Uses multicall for efficiency. Returns one result per input chunk,
|
|
1543
|
-
* along with block metadata for freshness tracking.
|
|
1544
|
-
*/
|
|
1545
|
-
async function getChunkLiquidities(params) {
|
|
1546
|
-
const { client, sfpmAddress, poolKeyBytes, chunks } = params;
|
|
1547
|
-
if (chunks.length > MAX_TRACKED_CHUNKS) throw new ChunkLimitError(BigInt(chunks.length), 0n);
|
|
1548
|
-
const _meta = params._meta ?? await getBlockMeta({ client });
|
|
1549
|
-
if (chunks.length === 0) return {
|
|
1550
|
-
results: [],
|
|
1551
|
-
_meta
|
|
1552
|
-
};
|
|
1553
|
-
const multicallResults = await client.multicall({
|
|
1554
|
-
contracts: chunks.map((chunk) => ({
|
|
1555
|
-
address: sfpmAddress,
|
|
1556
|
-
abi: semiFungiblePositionManagerV4Abi,
|
|
1557
|
-
functionName: "getAccountLiquidity",
|
|
1558
|
-
args: [
|
|
1559
|
-
poolKeyBytes,
|
|
1560
|
-
chunk.owner,
|
|
1561
|
-
chunk.tokenType,
|
|
1562
|
-
Number(chunk.tickLower),
|
|
1563
|
-
Number(chunk.tickUpper)
|
|
1564
|
-
]
|
|
1565
|
-
})),
|
|
1566
|
-
allowFailure: true
|
|
1567
|
-
});
|
|
1568
|
-
const results = multicallResults.map((result) => {
|
|
1569
|
-
if (result.status === "failure") return {
|
|
1570
|
-
netLiquidity: 0n,
|
|
1571
|
-
removedLiquidity: 0n,
|
|
1572
|
-
totalLiquidity: 0n,
|
|
1573
|
-
shortLiquidity: 0n,
|
|
1574
|
-
longLiquidity: 0n
|
|
1575
|
-
};
|
|
1576
|
-
const packed = result.result;
|
|
1577
|
-
const netLiquidity = packed & (1n << 128n) - 1n;
|
|
1578
|
-
const removedLiquidity = packed >> 128n;
|
|
1579
|
-
const totalLiquidity = netLiquidity + removedLiquidity;
|
|
1580
|
-
return {
|
|
1581
|
-
netLiquidity,
|
|
1582
|
-
removedLiquidity,
|
|
1583
|
-
totalLiquidity,
|
|
1584
|
-
shortLiquidity: totalLiquidity,
|
|
1585
|
-
longLiquidity: removedLiquidity
|
|
1586
|
-
};
|
|
1587
|
-
});
|
|
1588
|
-
return {
|
|
1589
|
-
results,
|
|
1590
|
-
_meta
|
|
1591
|
-
};
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
//#endregion
|
|
1595
|
-
//#region src/panoptic/v2/reads/factory.ts
|
|
1596
|
-
/**
|
|
1597
|
-
* Get the token URI from a PanopticFactory NFT.
|
|
1598
|
-
*/
|
|
1599
|
-
async function getFactoryTokenURI(params) {
|
|
1600
|
-
const { client, factoryAddress, version, tokenId } = params;
|
|
1601
|
-
const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
|
|
1602
|
-
return client.readContract({
|
|
1603
|
-
address: factoryAddress,
|
|
1604
|
-
abi,
|
|
1605
|
-
functionName: "tokenURI",
|
|
1606
|
-
args: [tokenId]
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
/**
|
|
1610
|
-
* Get the owner of a PanopticFactory NFT.
|
|
1611
|
-
*/
|
|
1612
|
-
async function getFactoryOwnerOf(params) {
|
|
1613
|
-
const { client, factoryAddress, version, tokenId } = params;
|
|
1614
|
-
const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
|
|
1615
|
-
return client.readContract({
|
|
1616
|
-
address: factoryAddress,
|
|
1617
|
-
abi,
|
|
1618
|
-
functionName: "ownerOf",
|
|
1619
|
-
args: [tokenId]
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Construct NFT metadata for a pool via the factory contract.
|
|
1624
|
-
*/
|
|
1625
|
-
async function getFactoryConstructMetadata(params) {
|
|
1626
|
-
const { client, factoryAddress, version, panopticPoolAddress, symbol0, symbol1, fee } = params;
|
|
1627
|
-
const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
|
|
1628
|
-
return client.readContract({
|
|
1629
|
-
address: factoryAddress,
|
|
1630
|
-
abi,
|
|
1631
|
-
functionName: "constructMetadata",
|
|
1632
|
-
args: [
|
|
1633
|
-
panopticPoolAddress,
|
|
1634
|
-
symbol0,
|
|
1635
|
-
symbol1,
|
|
1636
|
-
fee
|
|
1637
|
-
]
|
|
1638
|
-
});
|
|
1639
|
-
}
|
|
1640
|
-
/**
|
|
1641
|
-
* Get the PanopticPool address for a given pool and risk engine.
|
|
1642
|
-
*/
|
|
1643
|
-
async function getPanopticPoolAddress(params) {
|
|
1644
|
-
const { client, factoryAddress, riskEngine } = params;
|
|
1645
|
-
if (params.version === "v3") return client.readContract({
|
|
1646
|
-
address: factoryAddress,
|
|
1647
|
-
abi: panopticFactoryV3Abi,
|
|
1648
|
-
functionName: "getPanopticPool",
|
|
1649
|
-
args: [params.univ3pool, riskEngine]
|
|
1650
|
-
});
|
|
1651
|
-
return client.readContract({
|
|
1652
|
-
address: factoryAddress,
|
|
1653
|
-
abi: panopticFactoryV4Abi,
|
|
1654
|
-
functionName: "getPanopticPool",
|
|
1655
|
-
args: [{
|
|
1656
|
-
currency0: params.poolKey.currency0,
|
|
1657
|
-
currency1: params.poolKey.currency1,
|
|
1658
|
-
fee: Number(params.poolKey.fee),
|
|
1659
|
-
tickSpacing: Number(params.poolKey.tickSpacing),
|
|
1660
|
-
hooks: params.poolKey.hooks
|
|
1661
|
-
}, riskEngine]
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
/**
|
|
1665
|
-
* Mine for an optimal pool address salt with high rarity.
|
|
1666
|
-
*/
|
|
1667
|
-
async function minePoolAddress(params) {
|
|
1668
|
-
const { client, factoryAddress, deployerAddress, riskEngine, salt, loops, minTargetRarity } = params;
|
|
1669
|
-
let result;
|
|
1670
|
-
if (params.version === "v3") result = await client.readContract({
|
|
1671
|
-
address: factoryAddress,
|
|
1672
|
-
abi: panopticFactoryV3Abi,
|
|
1673
|
-
functionName: "minePoolAddress",
|
|
1674
|
-
args: [
|
|
1675
|
-
deployerAddress,
|
|
1676
|
-
params.v3Pool,
|
|
1677
|
-
riskEngine,
|
|
1678
|
-
salt,
|
|
1679
|
-
loops,
|
|
1680
|
-
minTargetRarity
|
|
1681
|
-
]
|
|
1682
|
-
});
|
|
1683
|
-
else result = await client.readContract({
|
|
1684
|
-
address: factoryAddress,
|
|
1685
|
-
abi: panopticFactoryV4Abi,
|
|
1686
|
-
functionName: "minePoolAddress",
|
|
1687
|
-
args: [
|
|
1688
|
-
deployerAddress,
|
|
1689
|
-
{
|
|
1690
|
-
currency0: params.poolKey.currency0,
|
|
1691
|
-
currency1: params.poolKey.currency1,
|
|
1692
|
-
fee: Number(params.poolKey.fee),
|
|
1693
|
-
tickSpacing: Number(params.poolKey.tickSpacing),
|
|
1694
|
-
hooks: params.poolKey.hooks
|
|
1695
|
-
},
|
|
1696
|
-
riskEngine,
|
|
1697
|
-
salt,
|
|
1698
|
-
loops,
|
|
1699
|
-
minTargetRarity
|
|
1700
|
-
]
|
|
1701
|
-
});
|
|
1702
|
-
return {
|
|
1703
|
-
bestSalt: BigInt(result[0]),
|
|
1704
|
-
highestRarity: result[1]
|
|
1705
|
-
};
|
|
1706
|
-
}
|
|
1707
|
-
/**
|
|
1708
|
-
* Simulate a pool deployment to get the predicted pool address.
|
|
1709
|
-
*
|
|
1710
|
-
* Uses `simulateContract` on `deployNewPool` — the return value is the new pool address
|
|
1711
|
-
* without actually executing the transaction.
|
|
1712
|
-
*/
|
|
1713
|
-
async function simulateDeployNewPool(params) {
|
|
1714
|
-
const { client, factoryAddress, account, riskEngine, salt } = params;
|
|
1715
|
-
if (params.version === "v3") {
|
|
1716
|
-
const { result: result$1 } = await client.simulateContract({
|
|
1717
|
-
address: factoryAddress,
|
|
1718
|
-
abi: panopticFactoryV3Abi,
|
|
1719
|
-
functionName: "deployNewPool",
|
|
1720
|
-
args: [
|
|
1721
|
-
params.token0,
|
|
1722
|
-
params.token1,
|
|
1723
|
-
Number(params.fee),
|
|
1724
|
-
riskEngine,
|
|
1725
|
-
salt
|
|
1726
|
-
],
|
|
1727
|
-
account
|
|
1728
|
-
});
|
|
1729
|
-
return result$1;
|
|
1730
|
-
}
|
|
1731
|
-
const { result } = await client.simulateContract({
|
|
1732
|
-
address: factoryAddress,
|
|
1733
|
-
abi: panopticFactoryV4Abi,
|
|
1734
|
-
functionName: "deployNewPool",
|
|
1735
|
-
args: [
|
|
1736
|
-
{
|
|
1737
|
-
currency0: params.poolKey.currency0,
|
|
1738
|
-
currency1: params.poolKey.currency1,
|
|
1739
|
-
fee: Number(params.poolKey.fee),
|
|
1740
|
-
tickSpacing: Number(params.poolKey.tickSpacing),
|
|
1741
|
-
hooks: params.poolKey.hooks
|
|
1742
|
-
},
|
|
1743
|
-
riskEngine,
|
|
1744
|
-
salt
|
|
1745
|
-
],
|
|
1746
|
-
account
|
|
1747
|
-
});
|
|
1748
|
-
return result;
|
|
1749
|
-
}
|
|
1750
|
-
/**
|
|
1751
|
-
* Resolve an SFPM poolId to its PanopticPool address.
|
|
1752
|
-
*
|
|
1753
|
-
* Chains two on-chain lookups:
|
|
1754
|
-
* 1. SFPM: poolId → Uniswap pool address (V3) or pool key (V4)
|
|
1755
|
-
* 2. Factory: Uniswap pool + riskEngine → PanopticPool address
|
|
1756
|
-
*/
|
|
1757
|
-
async function getPanopticPoolFromPoolId(params) {
|
|
1758
|
-
const { client, sfpmAddress, factoryAddress, riskEngine, poolId, version } = params;
|
|
1759
|
-
if (version === "v3") {
|
|
1760
|
-
const univ3pool = await getUniswapV3PoolFromId({
|
|
1761
|
-
client,
|
|
1762
|
-
sfpmAddress,
|
|
1763
|
-
poolId
|
|
1764
|
-
});
|
|
1765
|
-
return getPanopticPoolAddress({
|
|
1766
|
-
version: "v3",
|
|
1767
|
-
client,
|
|
1768
|
-
factoryAddress,
|
|
1769
|
-
univ3pool,
|
|
1770
|
-
riskEngine
|
|
1771
|
-
});
|
|
1772
|
-
}
|
|
1773
|
-
const poolKey = await getUniswapV4PoolKeyFromId({
|
|
1774
|
-
client,
|
|
1775
|
-
sfpmAddress,
|
|
1776
|
-
poolId
|
|
1777
|
-
});
|
|
1778
|
-
return getPanopticPoolAddress({
|
|
1779
|
-
version: "v4",
|
|
1780
|
-
client,
|
|
1781
|
-
factoryAddress,
|
|
1782
|
-
poolKey,
|
|
1783
|
-
riskEngine
|
|
1784
|
-
});
|
|
1785
|
-
}
|
|
1786
|
-
/**
|
|
1787
|
-
* Resolve an SFPM poolId to its PanopticPool address without knowing the version.
|
|
1788
|
-
*
|
|
1789
|
-
* Tries both V3 and V4 lookups in parallel. The factory returns `address(0)` for
|
|
1790
|
-
* non-existent pools, so the non-zero result identifies the correct version.
|
|
1791
|
-
*
|
|
1792
|
-
* At least one of `v3` or `v4` must be provided.
|
|
1793
|
-
*
|
|
1794
|
-
* @throws {PanopticValidationError} If no version config is provided or neither resolves.
|
|
1795
|
-
*/
|
|
1796
|
-
async function resolvePanopticPoolFromPoolId(params) {
|
|
1797
|
-
const { client, poolId, riskEngine, v3, v4 } = params;
|
|
1798
|
-
if (!v3 && !v4) throw new Error("At least one of v3 or v4 must be provided");
|
|
1799
|
-
const isNotFoundError = (err) => {
|
|
1800
|
-
if (typeof err === "object" && err !== null && "name" in err) {
|
|
1801
|
-
const name = err.name;
|
|
1802
|
-
return name === "ContractFunctionExecutionError" || name === "ContractFunctionRevertedError";
|
|
1803
|
-
}
|
|
1804
|
-
return false;
|
|
1805
|
-
};
|
|
1806
|
-
const results = await Promise.all([v3 ? getPanopticPoolFromPoolId({
|
|
1807
|
-
version: "v3",
|
|
1808
|
-
client,
|
|
1809
|
-
sfpmAddress: v3.sfpmAddress,
|
|
1810
|
-
factoryAddress: v3.factoryAddress,
|
|
1811
|
-
riskEngine,
|
|
1812
|
-
poolId
|
|
1813
|
-
}).catch((err) => {
|
|
1814
|
-
if (isNotFoundError(err)) return zeroAddress;
|
|
1815
|
-
throw err;
|
|
1816
|
-
}) : Promise.resolve(zeroAddress), v4 ? getPanopticPoolFromPoolId({
|
|
1817
|
-
version: "v4",
|
|
1818
|
-
client,
|
|
1819
|
-
sfpmAddress: v4.sfpmAddress,
|
|
1820
|
-
factoryAddress: v4.factoryAddress,
|
|
1821
|
-
riskEngine,
|
|
1822
|
-
poolId
|
|
1823
|
-
}).catch((err) => {
|
|
1824
|
-
if (isNotFoundError(err)) return zeroAddress;
|
|
1825
|
-
throw err;
|
|
1826
|
-
}) : Promise.resolve(zeroAddress)]);
|
|
1827
|
-
const [v3Result, v4Result] = results;
|
|
1828
|
-
if (v3Result !== zeroAddress) return {
|
|
1829
|
-
panopticPoolAddress: v3Result,
|
|
1830
|
-
version: "v3"
|
|
1831
|
-
};
|
|
1832
|
-
if (v4Result !== zeroAddress) return {
|
|
1833
|
-
panopticPoolAddress: v4Result,
|
|
1834
|
-
version: "v4"
|
|
1835
|
-
};
|
|
1836
|
-
throw new Error(`No PanopticPool found for poolId ${poolId}`);
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
//#endregion
|
|
1840
|
-
//#region src/panoptic/v2/reads/minePoolAddressLocal.ts
|
|
1841
|
-
/**
|
|
1842
|
-
* keccak256 of the CREATE3 proxy initcode used by ClonesWithImmutableArgs.
|
|
1843
|
-
* Source: packages/panoptic-v2-core/lib/clones-with-immutable-args/src/ClonesWithImmutableArgs.sol
|
|
1844
|
-
*/
|
|
1845
|
-
const CREATE3_PROXY_BYTECODE_HASH = "0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f";
|
|
1846
|
-
const MASK_80 = (1n << 80n) - 1n;
|
|
1847
|
-
const MASK_40 = (1n << 40n) - 1n;
|
|
1848
|
-
const MASK_96 = (1n << 96n) - 1n;
|
|
1849
|
-
/** Encode a BigInt as a big-endian fixed-length byte array. */
|
|
1850
|
-
function bigintToBytes(value, byteLength) {
|
|
1851
|
-
const bytes = new Uint8Array(byteLength);
|
|
1852
|
-
let v = value;
|
|
1853
|
-
for (let i = byteLength - 1; i >= 0; i--) {
|
|
1854
|
-
bytes[i] = Number(v & 0xffn);
|
|
1855
|
-
v >>= 8n;
|
|
1856
|
-
}
|
|
1857
|
-
return bytes;
|
|
1858
|
-
}
|
|
1859
|
-
/** Parse a 0x-prefixed address into 20 bytes. */
|
|
1860
|
-
function addressToBytes(addr) {
|
|
1861
|
-
const hex = addr.slice(2).padStart(40, "0");
|
|
1862
|
-
const bytes = new Uint8Array(20);
|
|
1863
|
-
for (let i = 0; i < 20; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1864
|
-
return bytes;
|
|
1865
|
-
}
|
|
1866
|
-
/** Parse a 0x-prefixed 32-byte hex string into bytes. */
|
|
1867
|
-
function hex32ToBytes(hex) {
|
|
1868
|
-
const h = hex.slice(2).padStart(64, "0");
|
|
1869
|
-
const bytes = new Uint8Array(32);
|
|
1870
|
-
for (let i = 0; i < 32; i++) bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
1871
|
-
return bytes;
|
|
860
|
+
/** Parse a 0x-prefixed 32-byte hex string into bytes. */
|
|
861
|
+
function hex32ToBytes(hex) {
|
|
862
|
+
const h = hex.slice(2).padStart(64, "0");
|
|
863
|
+
const bytes = new Uint8Array(32);
|
|
864
|
+
for (let i = 0; i < 32; i++) bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
|
|
865
|
+
return bytes;
|
|
1872
866
|
}
|
|
1873
867
|
/**
|
|
1874
868
|
* Compute the CREATE3 deployed address for a given factory and packed salt.
|
|
@@ -1962,330 +956,75 @@ function computePoolIdV4(poolKey) {
|
|
|
1962
956
|
const encoded = encodeAbiParameters([
|
|
1963
957
|
{ type: "address" },
|
|
1964
958
|
{ type: "address" },
|
|
1965
|
-
{ type: "uint24" },
|
|
1966
|
-
{ type: "int24" },
|
|
1967
|
-
{ type: "address" }
|
|
1968
|
-
], [
|
|
1969
|
-
poolKey.currency0,
|
|
1970
|
-
poolKey.currency1,
|
|
1971
|
-
Number(poolKey.fee),
|
|
1972
|
-
Number(poolKey.tickSpacing),
|
|
1973
|
-
poolKey.hooks
|
|
1974
|
-
]);
|
|
1975
|
-
return BigInt(keccak256(encoded));
|
|
1976
|
-
}
|
|
1977
|
-
/**
|
|
1978
|
-
* Construct the bytes32 CREATE3 salt for PanopticFactoryV4.
|
|
1979
|
-
*
|
|
1980
|
-
* Mirrors:
|
|
1981
|
-
* bytes32(abi.encodePacked(
|
|
1982
|
-
* uint80(uint160(deployerAddress) >> 80),
|
|
1983
|
-
* uint40(uint256(PoolId.unwrap(key.toId())) >> 120), // bits [159:120] of poolId
|
|
1984
|
-
* uint40(uint160(riskEngine) >> 120),
|
|
1985
|
-
* salt
|
|
1986
|
-
* ))
|
|
1987
|
-
*/
|
|
1988
|
-
function computeSaltPrefixV4(deployerAddress, poolKey, riskEngine) {
|
|
1989
|
-
const deployer80 = BigInt(deployerAddress) >> 80n & MASK_80;
|
|
1990
|
-
const poolId40 = computePoolIdV4(poolKey) >> 120n & MASK_40;
|
|
1991
|
-
const risk40 = BigInt(riskEngine) >> 120n & MASK_40;
|
|
1992
|
-
return deployer80 << 176n | poolId40 << 136n | risk40 << 96n;
|
|
1993
|
-
}
|
|
1994
|
-
/** Number of iterations per chunk before yielding back to the event loop. */
|
|
1995
|
-
const CHUNK_SIZE = 5000n;
|
|
1996
|
-
/**
|
|
1997
|
-
* Async version of {@link minePoolAddressLocal} that yields to the event loop
|
|
1998
|
-
* between chunks of iterations, preventing the browser UI from freezing.
|
|
1999
|
-
*
|
|
2000
|
-
* @param params - Mining parameters (versioned: 'v3' or 'v4'). No `client` required.
|
|
2001
|
-
* @returns The best salt found and its rarity (number of leading hex zeros).
|
|
2002
|
-
*/
|
|
2003
|
-
async function minePoolAddressLocalAsync(params) {
|
|
2004
|
-
const { factoryAddress, deployerAddress, riskEngine, salt, loops, minTargetRarity } = params;
|
|
2005
|
-
let bestSalt = salt;
|
|
2006
|
-
let highestRarity = 0n;
|
|
2007
|
-
const maxSalt = salt + loops;
|
|
2008
|
-
const saltPrefix = params.version === "v3" ? computeSaltPrefixV3(deployerAddress, params.v3Pool, riskEngine) : computeSaltPrefixV4(deployerAddress, params.poolKey, riskEngine);
|
|
2009
|
-
let currentSalt = salt;
|
|
2010
|
-
while (currentSalt < maxSalt) {
|
|
2011
|
-
const chunkEnd = currentSalt + CHUNK_SIZE < maxSalt ? currentSalt + CHUNK_SIZE : maxSalt;
|
|
2012
|
-
let done = false;
|
|
2013
|
-
for (; currentSalt < chunkEnd; currentSalt++) {
|
|
2014
|
-
const newSalt = saltPrefix | currentSalt & MASK_96;
|
|
2015
|
-
const addrInt = addressOfClone3(factoryAddress, newSalt);
|
|
2016
|
-
const rarity = BigInt(numberOfLeadingHexZeros(addrInt));
|
|
2017
|
-
if (rarity > highestRarity) {
|
|
2018
|
-
highestRarity = rarity;
|
|
2019
|
-
bestSalt = currentSalt;
|
|
2020
|
-
}
|
|
2021
|
-
if (rarity >= minTargetRarity) {
|
|
2022
|
-
highestRarity = rarity;
|
|
2023
|
-
bestSalt = currentSalt;
|
|
2024
|
-
done = true;
|
|
2025
|
-
break;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
if (done) break;
|
|
2029
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2030
|
-
}
|
|
2031
|
-
return {
|
|
2032
|
-
bestSalt,
|
|
2033
|
-
highestRarity
|
|
2034
|
-
};
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
//#endregion
|
|
2038
|
-
//#region src/panoptic/v2/reads/premia.ts
|
|
2039
|
-
/**
|
|
2040
|
-
* Get premia totals for an account.
|
|
2041
|
-
*
|
|
2042
|
-
* Returns the total short and long premium across all positions.
|
|
2043
|
-
* Short premium is owed TO the account (earned from selling options).
|
|
2044
|
-
* Long premium is owed BY the account (paid for buying options).
|
|
2045
|
-
*
|
|
2046
|
-
* @param params - The parameters
|
|
2047
|
-
* @returns Premia totals with block metadata
|
|
2048
|
-
*
|
|
2049
|
-
* @example
|
|
2050
|
-
* ```typescript
|
|
2051
|
-
* const premia = await getAccountPremia({
|
|
2052
|
-
* client,
|
|
2053
|
-
* poolAddress,
|
|
2054
|
-
* account,
|
|
2055
|
-
* tokenIds: [tokenId1, tokenId2],
|
|
2056
|
-
* includePendingPremium: true,
|
|
2057
|
-
* })
|
|
2058
|
-
*
|
|
2059
|
-
* console.log('Short premium earned:', premia.shortPremium0, premia.shortPremium1)
|
|
2060
|
-
* console.log('Long premium owed:', premia.longPremium0, premia.longPremium1)
|
|
2061
|
-
* ```
|
|
2062
|
-
*/
|
|
2063
|
-
async function getAccountPremia(params) {
|
|
2064
|
-
const { client, poolAddress, account, tokenIds, includePendingPremium = true, blockNumber } = params;
|
|
2065
|
-
const targetBlockNumber = blockNumber ?? params._meta?.blockNumber ?? await client.getBlockNumber();
|
|
2066
|
-
if (tokenIds.length === 0) {
|
|
2067
|
-
const _meta$1 = params._meta ?? await getBlockMeta({
|
|
2068
|
-
client,
|
|
2069
|
-
blockNumber: targetBlockNumber
|
|
2070
|
-
});
|
|
2071
|
-
return {
|
|
2072
|
-
shortPremium0: 0n,
|
|
2073
|
-
shortPremium1: 0n,
|
|
2074
|
-
longPremium0: 0n,
|
|
2075
|
-
longPremium1: 0n,
|
|
2076
|
-
includePendingPremium,
|
|
2077
|
-
_meta: _meta$1
|
|
2078
|
-
};
|
|
2079
|
-
}
|
|
2080
|
-
const [[shortPremiumPacked, longPremiumPacked], _meta] = await Promise.all([client.readContract({
|
|
2081
|
-
address: poolAddress,
|
|
2082
|
-
abi: panopticPoolV2Abi,
|
|
2083
|
-
functionName: "getFullPositionsData",
|
|
2084
|
-
args: [
|
|
2085
|
-
account,
|
|
2086
|
-
includePendingPremium,
|
|
2087
|
-
tokenIds
|
|
2088
|
-
],
|
|
2089
|
-
blockNumber: targetBlockNumber
|
|
2090
|
-
}), params._meta ?? getBlockMeta({
|
|
2091
|
-
client,
|
|
2092
|
-
blockNumber: targetBlockNumber
|
|
2093
|
-
})]);
|
|
2094
|
-
const shortPremium = decodeLeftRightUnsigned(shortPremiumPacked);
|
|
2095
|
-
const longPremium = decodeLeftRightUnsigned(longPremiumPacked);
|
|
2096
|
-
return {
|
|
2097
|
-
shortPremium0: shortPremium.right,
|
|
2098
|
-
shortPremium1: shortPremium.left,
|
|
2099
|
-
longPremium0: longPremium.right,
|
|
2100
|
-
longPremium1: longPremium.left,
|
|
2101
|
-
includePendingPremium,
|
|
2102
|
-
_meta
|
|
2103
|
-
};
|
|
2104
|
-
}
|
|
2105
|
-
/**
|
|
2106
|
-
* Get positions with per-position premia data.
|
|
2107
|
-
*
|
|
2108
|
-
* Uses multicall to batch individual getFullPositionsData calls
|
|
2109
|
-
* for each position, giving us per-position premia in a single RPC request.
|
|
2110
|
-
*
|
|
2111
|
-
* @param params - The parameters
|
|
2112
|
-
* @returns Positions with premia and totals with block metadata
|
|
2113
|
-
*
|
|
2114
|
-
* @example
|
|
2115
|
-
* ```typescript
|
|
2116
|
-
* const result = await getPositionsWithPremia({
|
|
2117
|
-
* client,
|
|
2118
|
-
* poolAddress,
|
|
2119
|
-
* account,
|
|
2120
|
-
* tokenIds: [tokenId1, tokenId2],
|
|
2121
|
-
* })
|
|
2122
|
-
*
|
|
2123
|
-
* for (const position of result.positions) {
|
|
2124
|
-
* console.log('Position:', position.tokenId)
|
|
2125
|
-
* console.log('Premia:', position.premiaOwed0, position.premiaOwed1)
|
|
2126
|
-
* }
|
|
2127
|
-
* console.log('Total short premium:', result.shortPremium0, result.shortPremium1)
|
|
2128
|
-
* ```
|
|
2129
|
-
*/
|
|
2130
|
-
async function getPositionsWithPremia(params) {
|
|
2131
|
-
const { client, poolAddress, account, tokenIds, includePendingPremium = true, blockNumber } = params;
|
|
2132
|
-
const targetBlockNumber = blockNumber ?? params._meta?.blockNumber ?? await client.getBlockNumber();
|
|
2133
|
-
if (tokenIds.length === 0) {
|
|
2134
|
-
const _meta$1 = params._meta ?? await getBlockMeta({
|
|
2135
|
-
client,
|
|
2136
|
-
blockNumber: targetBlockNumber
|
|
2137
|
-
});
|
|
2138
|
-
return {
|
|
2139
|
-
positions: [],
|
|
2140
|
-
shortPremium0: 0n,
|
|
2141
|
-
shortPremium1: 0n,
|
|
2142
|
-
longPremium0: 0n,
|
|
2143
|
-
longPremium1: 0n,
|
|
2144
|
-
includePendingPremium,
|
|
2145
|
-
_meta: _meta$1
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
|
-
const contracts = tokenIds.map((tokenId) => ({
|
|
2149
|
-
address: poolAddress,
|
|
2150
|
-
abi: panopticPoolV2Abi,
|
|
2151
|
-
functionName: "getFullPositionsData",
|
|
2152
|
-
args: [
|
|
2153
|
-
account,
|
|
2154
|
-
includePendingPremium,
|
|
2155
|
-
[tokenId]
|
|
2156
|
-
]
|
|
2157
|
-
}));
|
|
2158
|
-
const [multicallResults, _meta] = await Promise.all([client.multicall({
|
|
2159
|
-
contracts,
|
|
2160
|
-
blockNumber: targetBlockNumber,
|
|
2161
|
-
allowFailure: true
|
|
2162
|
-
}), params._meta ?? getBlockMeta({
|
|
2163
|
-
client,
|
|
2164
|
-
blockNumber: targetBlockNumber
|
|
2165
|
-
})]);
|
|
2166
|
-
const positions = [];
|
|
2167
|
-
let totalShortPremium0 = 0n;
|
|
2168
|
-
let totalShortPremium1 = 0n;
|
|
2169
|
-
let totalLongPremium0 = 0n;
|
|
2170
|
-
let totalLongPremium1 = 0n;
|
|
2171
|
-
for (let i = 0; i < tokenIds.length; i++) {
|
|
2172
|
-
const tokenId = tokenIds[i];
|
|
2173
|
-
const result = multicallResults[i];
|
|
2174
|
-
if (result.status !== "success") continue;
|
|
2175
|
-
const [shortPremiumPacked, longPremiumPacked, balances] = result.result;
|
|
2176
|
-
const shortPremium = decodeLeftRightUnsigned(shortPremiumPacked);
|
|
2177
|
-
const longPremium = decodeLeftRightUnsigned(longPremiumPacked);
|
|
2178
|
-
totalShortPremium0 += shortPremium.right;
|
|
2179
|
-
totalShortPremium1 += shortPremium.left;
|
|
2180
|
-
totalLongPremium0 += longPremium.right;
|
|
2181
|
-
totalLongPremium1 += longPremium.left;
|
|
2182
|
-
const balanceData = balances[0];
|
|
2183
|
-
if (balanceData === void 0) continue;
|
|
2184
|
-
const positionSize = balanceData & (1n << 128n) - 1n;
|
|
2185
|
-
if (positionSize === 0n) continue;
|
|
2186
|
-
const poolUtilization0 = balanceData >> 128n & 0xffffn;
|
|
2187
|
-
const poolUtilization1 = balanceData >> 144n & 0xffffn;
|
|
2188
|
-
let tickAtMint = balanceData >> 160n & 0xffffffn;
|
|
2189
|
-
if (tickAtMint > 0x7fffffn) tickAtMint = tickAtMint - 0x1000000n;
|
|
2190
|
-
const timestampAtMint = balanceData >> 184n & 0xffffffffn;
|
|
2191
|
-
const blockAtMint = balanceData >> 216n & (1n << 39n) - 1n;
|
|
2192
|
-
const swapAtMint = balanceData >> 255n === 1n;
|
|
2193
|
-
const decoded = decodePosition(tokenId);
|
|
2194
|
-
const tickSpacing = decodeTickSpacing(tokenId);
|
|
2195
|
-
const legs = decoded.legs.map((leg) => {
|
|
2196
|
-
const width = leg.width;
|
|
2197
|
-
const strike = leg.strike;
|
|
2198
|
-
const tickLower = strike - width * tickSpacing / 2n;
|
|
2199
|
-
const tickUpper = strike + width * tickSpacing / 2n;
|
|
2200
|
-
return {
|
|
2201
|
-
index: BigInt(leg.index),
|
|
2202
|
-
asset: leg.asset,
|
|
2203
|
-
optionRatio: leg.optionRatio,
|
|
2204
|
-
isLong: leg.isLong === 1n,
|
|
2205
|
-
tokenType: leg.tokenType,
|
|
2206
|
-
riskPartner: leg.riskPartner,
|
|
2207
|
-
strike,
|
|
2208
|
-
width,
|
|
2209
|
-
tickLower,
|
|
2210
|
-
tickUpper
|
|
2211
|
-
};
|
|
2212
|
-
});
|
|
2213
|
-
const premiaOwed0 = shortPremium.right - longPremium.right;
|
|
2214
|
-
const premiaOwed1 = shortPremium.left - longPremium.left;
|
|
2215
|
-
positions.push({
|
|
2216
|
-
tokenId,
|
|
2217
|
-
positionSize,
|
|
2218
|
-
owner: account,
|
|
2219
|
-
poolAddress,
|
|
2220
|
-
legs,
|
|
2221
|
-
poolUtilization0AtMint: poolUtilization0,
|
|
2222
|
-
poolUtilization1AtMint: poolUtilization1,
|
|
2223
|
-
tickAtMint,
|
|
2224
|
-
timestampAtMint,
|
|
2225
|
-
blockNumberAtMint: blockAtMint,
|
|
2226
|
-
swapAtMint,
|
|
2227
|
-
premiaOwed0,
|
|
2228
|
-
premiaOwed1,
|
|
2229
|
-
assetIndex: legs.length > 0 ? legs[0].asset : 0n,
|
|
2230
|
-
_meta
|
|
2231
|
-
});
|
|
2232
|
-
}
|
|
2233
|
-
return {
|
|
2234
|
-
positions,
|
|
2235
|
-
shortPremium0: totalShortPremium0,
|
|
2236
|
-
shortPremium1: totalShortPremium1,
|
|
2237
|
-
longPremium0: totalLongPremium0,
|
|
2238
|
-
longPremium1: totalLongPremium1,
|
|
2239
|
-
includePendingPremium,
|
|
2240
|
-
_meta
|
|
2241
|
-
};
|
|
959
|
+
{ type: "uint24" },
|
|
960
|
+
{ type: "int24" },
|
|
961
|
+
{ type: "address" }
|
|
962
|
+
], [
|
|
963
|
+
poolKey.currency0,
|
|
964
|
+
poolKey.currency1,
|
|
965
|
+
Number(poolKey.fee),
|
|
966
|
+
Number(poolKey.tickSpacing),
|
|
967
|
+
poolKey.hooks
|
|
968
|
+
]);
|
|
969
|
+
return BigInt(keccak256(encoded));
|
|
2242
970
|
}
|
|
2243
971
|
/**
|
|
2244
|
-
*
|
|
972
|
+
* Construct the bytes32 CREATE3 salt for PanopticFactoryV4.
|
|
2245
973
|
*
|
|
2246
|
-
*
|
|
2247
|
-
*
|
|
2248
|
-
*
|
|
2249
|
-
*
|
|
2250
|
-
*
|
|
2251
|
-
*
|
|
974
|
+
* Mirrors:
|
|
975
|
+
* bytes32(abi.encodePacked(
|
|
976
|
+
* uint80(uint160(deployerAddress) >> 80),
|
|
977
|
+
* uint40(uint256(PoolId.unwrap(key.toId())) >> 120), // bits [159:120] of poolId
|
|
978
|
+
* uint40(uint160(riskEngine) >> 120),
|
|
979
|
+
* salt
|
|
980
|
+
* ))
|
|
981
|
+
*/
|
|
982
|
+
function computeSaltPrefixV4(deployerAddress, poolKey, riskEngine) {
|
|
983
|
+
const deployer80 = BigInt(deployerAddress) >> 80n & MASK_80;
|
|
984
|
+
const poolId40 = computePoolIdV4(poolKey) >> 120n & MASK_40;
|
|
985
|
+
const risk40 = BigInt(riskEngine) >> 120n & MASK_40;
|
|
986
|
+
return deployer80 << 176n | poolId40 << 136n | risk40 << 96n;
|
|
987
|
+
}
|
|
988
|
+
/** Number of iterations per chunk before yielding back to the event loop. */
|
|
989
|
+
const CHUNK_SIZE = 5000n;
|
|
990
|
+
/**
|
|
991
|
+
* Async version of {@link minePoolAddressLocal} that yields to the event loop
|
|
992
|
+
* between chunks of iterations, preventing the browser UI from freezing.
|
|
2252
993
|
*
|
|
2253
|
-
* @param params -
|
|
2254
|
-
* @returns
|
|
994
|
+
* @param params - Mining parameters (versioned: 'v3' or 'v4'). No `client` required.
|
|
995
|
+
* @returns The best salt found and its rarity (number of leading hex zeros).
|
|
2255
996
|
*/
|
|
2256
|
-
async function
|
|
2257
|
-
const {
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
997
|
+
async function minePoolAddressLocalAsync(params) {
|
|
998
|
+
const { factoryAddress, deployerAddress, riskEngine, salt, loops, minTargetRarity } = params;
|
|
999
|
+
let bestSalt = salt;
|
|
1000
|
+
let highestRarity = 0n;
|
|
1001
|
+
const maxSalt = salt + loops;
|
|
1002
|
+
const saltPrefix = params.version === "v3" ? computeSaltPrefixV3(deployerAddress, params.v3Pool, riskEngine) : computeSaltPrefixV4(deployerAddress, params.poolKey, riskEngine);
|
|
1003
|
+
let currentSalt = salt;
|
|
1004
|
+
while (currentSalt < maxSalt) {
|
|
1005
|
+
const chunkEnd = currentSalt + CHUNK_SIZE < maxSalt ? currentSalt + CHUNK_SIZE : maxSalt;
|
|
1006
|
+
let done = false;
|
|
1007
|
+
for (; currentSalt < chunkEnd; currentSalt++) {
|
|
1008
|
+
const newSalt = saltPrefix | currentSalt & MASK_96;
|
|
1009
|
+
const addrInt = addressOfClone3(factoryAddress, newSalt);
|
|
1010
|
+
const rarity = BigInt(numberOfLeadingHexZeros(addrInt));
|
|
1011
|
+
if (rarity > highestRarity) {
|
|
1012
|
+
highestRarity = rarity;
|
|
1013
|
+
bestSalt = currentSalt;
|
|
1014
|
+
}
|
|
1015
|
+
if (rarity >= minTargetRarity) {
|
|
1016
|
+
highestRarity = rarity;
|
|
1017
|
+
bestSalt = currentSalt;
|
|
1018
|
+
done = true;
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (done) break;
|
|
1023
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1024
|
+
}
|
|
2281
1025
|
return {
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
available0: available.right,
|
|
2285
|
-
available1: available.left,
|
|
2286
|
-
forfeit0,
|
|
2287
|
-
forfeit1,
|
|
2288
|
-
_meta
|
|
1026
|
+
bestSalt,
|
|
1027
|
+
highestRarity
|
|
2289
1028
|
};
|
|
2290
1029
|
}
|
|
2291
1030
|
|
|
@@ -2606,7 +1345,7 @@ function maximumAmountIn(estimatedAmountIn, slippageBps) {
|
|
|
2606
1345
|
if (slippageBps < 0n) throw new PanopticError("slippageBps must be non-negative");
|
|
2607
1346
|
return (estimatedAmountIn * (BPS_DENOMINATOR$2 + slippageBps) + BPS_DENOMINATOR$2 - 1n) / BPS_DENOMINATOR$2;
|
|
2608
1347
|
}
|
|
2609
|
-
function ceilDiv(numerator, denominator) {
|
|
1348
|
+
function ceilDiv$1(numerator, denominator) {
|
|
2610
1349
|
return (numerator + denominator - 1n) / denominator;
|
|
2611
1350
|
}
|
|
2612
1351
|
function getInputAmount(tokenFlow, tokenInIndex) {
|
|
@@ -2721,7 +1460,7 @@ async function quoteTokenShortfallRecovery(params) {
|
|
|
2721
1460
|
error: new PanopticError("Insufficient source collateral for the recovery swap")
|
|
2722
1461
|
};
|
|
2723
1462
|
if (estimatedAmountOut < requiredOutput) {
|
|
2724
|
-
creditInput = estimatedAmountOut > 0n ? ceilDiv(creditInput * requiredOutput, estimatedAmountOut) + 1n : creditInput * 2n;
|
|
1463
|
+
creditInput = estimatedAmountOut > 0n ? ceilDiv$1(creditInput * requiredOutput, estimatedAmountOut) + 1n : creditInput * 2n;
|
|
2725
1464
|
continue;
|
|
2726
1465
|
}
|
|
2727
1466
|
const recoveredDispatch = buildPrefixedExactInputRecoveryDispatch({
|
|
@@ -2775,7 +1514,7 @@ async function quoteTokenShortfallRecovery(params) {
|
|
|
2775
1514
|
};
|
|
2776
1515
|
const residual = remainingShortfall.assetsRequested - remainingShortfall.assetBalance;
|
|
2777
1516
|
requiredOutput += residual > 0n ? residual : requiredOutput;
|
|
2778
|
-
creditInput = ceilDiv(creditInput * requiredOutput, estimatedAmountOut) + 1n;
|
|
1517
|
+
creditInput = ceilDiv$1(creditInput * requiredOutput, estimatedAmountOut) + 1n;
|
|
2779
1518
|
}
|
|
2780
1519
|
return {
|
|
2781
1520
|
available: false,
|
|
@@ -2831,7 +1570,7 @@ async function quoteTokenShortfallRecovery(params) {
|
|
|
2831
1570
|
const swapOutput = getOutputAmount(swapSimulation.tokenFlow, tokenOutIndex);
|
|
2832
1571
|
const sourceBalance = getBalanceBefore(swapSimulation.tokenFlow, tokenInIndex);
|
|
2833
1572
|
if (swapOutput < amountOut) {
|
|
2834
|
-
creditOutSize = swapOutput > 0n ? ceilDiv(creditOutSize * amountOut, swapOutput) + 1n : creditOutSize * 2n;
|
|
1573
|
+
creditOutSize = swapOutput > 0n ? ceilDiv$1(creditOutSize * amountOut, swapOutput) + 1n : creditOutSize * 2n;
|
|
2835
1574
|
continue;
|
|
2836
1575
|
}
|
|
2837
1576
|
if (sourceBalance < maxAmountIn) return {
|
|
@@ -2890,7 +1629,7 @@ async function quoteTokenShortfallRecovery(params) {
|
|
|
2890
1629
|
const decodedShortfall = remainingShortfall.assetsRequested - remainingShortfall.assetBalance;
|
|
2891
1630
|
const additionalAmountOut = decodedShortfall > 0n ? decodedShortfall : amountOut;
|
|
2892
1631
|
amountOut += additionalAmountOut;
|
|
2893
|
-
creditOutSize = swapOutput > 0n ? ceilDiv(creditOutSize * amountOut, swapOutput) + 1n : creditOutSize * 2n;
|
|
1632
|
+
creditOutSize = swapOutput > 0n ? ceilDiv$1(creditOutSize * amountOut, swapOutput) + 1n : creditOutSize * 2n;
|
|
2894
1633
|
}
|
|
2895
1634
|
return {
|
|
2896
1635
|
available: false,
|
|
@@ -6986,568 +5725,244 @@ async function getPositionChunkData(params) {
|
|
|
6986
5725
|
abi: panopticQueryAbi,
|
|
6987
5726
|
functionName: "getChunkData",
|
|
6988
5727
|
args: [poolAddress, tokenIds],
|
|
6989
|
-
blockNumber: targetBlockNumber
|
|
6990
|
-
}), getBlockMeta({
|
|
6991
|
-
client,
|
|
6992
|
-
blockNumber: targetBlockNumber
|
|
6993
|
-
})]);
|
|
6994
|
-
const positions = tokenIds.map((tokenId, posIndex) => {
|
|
6995
|
-
const positionData = chunkData[posIndex];
|
|
6996
|
-
const legs = [];
|
|
6997
|
-
for (let legIndex = 0; legIndex < 4; legIndex++) {
|
|
6998
|
-
const legData = positionData[legIndex];
|
|
6999
|
-
const netLiquidity = legData[0];
|
|
7000
|
-
const removedLiquidity = legData[1];
|
|
7001
|
-
if (netLiquidity > 0n || removedLiquidity > 0n) legs.push({
|
|
7002
|
-
legIndex,
|
|
7003
|
-
netLiquidity,
|
|
7004
|
-
removedLiquidity,
|
|
7005
|
-
spreadWad: calculateSpreadWad(netLiquidity, removedLiquidity, vegoid)
|
|
7006
|
-
});
|
|
7007
|
-
}
|
|
7008
|
-
return {
|
|
7009
|
-
tokenId,
|
|
7010
|
-
legs
|
|
7011
|
-
};
|
|
7012
|
-
});
|
|
7013
|
-
return {
|
|
7014
|
-
positions,
|
|
7015
|
-
_meta
|
|
7016
|
-
};
|
|
7017
|
-
}
|
|
7018
|
-
|
|
7019
|
-
//#endregion
|
|
7020
|
-
//#region src/panoptic/v2/sync/pendingPositions.ts
|
|
7021
|
-
/**
|
|
7022
|
-
* Add a pending position for optimistic updates.
|
|
7023
|
-
*
|
|
7024
|
-
* Called when openPosition() submits a transaction.
|
|
7025
|
-
*
|
|
7026
|
-
* @param params - Parameters
|
|
7027
|
-
*/
|
|
7028
|
-
async function addPendingPosition(params) {
|
|
7029
|
-
const { chainId, poolAddress, account, storage, position } = params;
|
|
7030
|
-
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
7031
|
-
const existingData = await storage.get(key);
|
|
7032
|
-
let pending;
|
|
7033
|
-
if (existingData) try {
|
|
7034
|
-
pending = jsonSerializer.parse(existingData);
|
|
7035
|
-
} catch {
|
|
7036
|
-
pending = [];
|
|
7037
|
-
}
|
|
7038
|
-
else pending = [];
|
|
7039
|
-
pending.push(position);
|
|
7040
|
-
await storage.set(key, jsonSerializer.stringify(pending));
|
|
7041
|
-
}
|
|
7042
|
-
/**
|
|
7043
|
-
* Get all pending positions for an account.
|
|
7044
|
-
*
|
|
7045
|
-
* @param params - Parameters
|
|
7046
|
-
* @returns Array of pending positions
|
|
7047
|
-
*/
|
|
7048
|
-
async function getPendingPositions(params) {
|
|
7049
|
-
const { chainId, poolAddress, account, storage } = params;
|
|
7050
|
-
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
7051
|
-
const data = await storage.get(key);
|
|
7052
|
-
if (!data) return [];
|
|
7053
|
-
try {
|
|
7054
|
-
const pending = jsonSerializer.parse(data);
|
|
7055
|
-
return pending.filter((p) => p.status === "pending");
|
|
7056
|
-
} catch {
|
|
7057
|
-
return [];
|
|
7058
|
-
}
|
|
7059
|
-
}
|
|
7060
|
-
/**
|
|
7061
|
-
* Mark a pending position as confirmed.
|
|
7062
|
-
*
|
|
7063
|
-
* Called when syncPositions() finds the OptionMinted event.
|
|
7064
|
-
*
|
|
7065
|
-
* @param params - Parameters
|
|
7066
|
-
*/
|
|
7067
|
-
async function confirmPendingPosition(params) {
|
|
7068
|
-
const { chainId, poolAddress, account, storage, tokenId } = params;
|
|
7069
|
-
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
7070
|
-
const existingData = await storage.get(key);
|
|
7071
|
-
if (!existingData) return;
|
|
7072
|
-
let pending;
|
|
7073
|
-
try {
|
|
7074
|
-
pending = jsonSerializer.parse(existingData);
|
|
7075
|
-
} catch {
|
|
7076
|
-
return;
|
|
7077
|
-
}
|
|
7078
|
-
const updated = pending.map((p) => p.tokenId === tokenId ? {
|
|
7079
|
-
...p,
|
|
7080
|
-
status: "confirmed"
|
|
7081
|
-
} : p);
|
|
7082
|
-
const remaining = updated.filter((p) => p.status === "pending");
|
|
7083
|
-
if (remaining.length === 0) await storage.delete(key);
|
|
7084
|
-
else await storage.set(key, jsonSerializer.stringify(remaining));
|
|
7085
|
-
}
|
|
7086
|
-
/**
|
|
7087
|
-
* Mark a pending position as failed (transaction reverted).
|
|
7088
|
-
*
|
|
7089
|
-
* @param params - Parameters
|
|
7090
|
-
*/
|
|
7091
|
-
async function failPendingPosition(params) {
|
|
7092
|
-
const { chainId, poolAddress, account, storage, txHash } = params;
|
|
7093
|
-
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
7094
|
-
const existingData = await storage.get(key);
|
|
7095
|
-
if (!existingData) return;
|
|
7096
|
-
let pending;
|
|
7097
|
-
try {
|
|
7098
|
-
pending = jsonSerializer.parse(existingData);
|
|
7099
|
-
} catch {
|
|
7100
|
-
return;
|
|
7101
|
-
}
|
|
7102
|
-
const remaining = pending.filter((p) => p.txHash !== txHash);
|
|
7103
|
-
if (remaining.length === 0) await storage.delete(key);
|
|
7104
|
-
else await storage.set(key, jsonSerializer.stringify(remaining));
|
|
7105
|
-
}
|
|
7106
|
-
/**
|
|
7107
|
-
* Clear all pending positions for an account.
|
|
7108
|
-
*
|
|
7109
|
-
* @param params - Parameters
|
|
7110
|
-
*/
|
|
7111
|
-
async function clearPendingPositions(params) {
|
|
7112
|
-
const { chainId, poolAddress, account, storage } = params;
|
|
7113
|
-
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
7114
|
-
await storage.delete(key);
|
|
7115
|
-
}
|
|
7116
|
-
/**
|
|
7117
|
-
* Clean up stale pending positions.
|
|
7118
|
-
*
|
|
7119
|
-
* Removes pending positions older than the specified block threshold.
|
|
7120
|
-
* This handles cases where transactions were dropped from the mempool.
|
|
7121
|
-
*
|
|
7122
|
-
* @param params - Parameters
|
|
7123
|
-
* @param maxAgeBlocks - Maximum age in blocks (default: 100)
|
|
7124
|
-
* @param currentBlock - Current block number
|
|
7125
|
-
*/
|
|
7126
|
-
async function cleanupStalePendingPositions(params, currentBlock, maxAgeBlocks = 100n) {
|
|
7127
|
-
const { chainId, poolAddress, account, storage } = params;
|
|
7128
|
-
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
7129
|
-
const existingData = await storage.get(key);
|
|
7130
|
-
if (!existingData) return;
|
|
7131
|
-
let pending;
|
|
7132
|
-
try {
|
|
7133
|
-
pending = jsonSerializer.parse(existingData);
|
|
7134
|
-
} catch {
|
|
7135
|
-
return;
|
|
7136
|
-
}
|
|
7137
|
-
const threshold = currentBlock - maxAgeBlocks;
|
|
7138
|
-
const remaining = pending.filter((p) => p.status === "pending" && p.submittedAtBlock > threshold);
|
|
7139
|
-
if (remaining.length === 0) await storage.delete(key);
|
|
7140
|
-
else if (remaining.length !== pending.length) await storage.set(key, jsonSerializer.stringify(remaining));
|
|
7141
|
-
}
|
|
7142
|
-
|
|
7143
|
-
//#endregion
|
|
7144
|
-
//#region src/panoptic/v2/simulations/simulateBatchDispatch.ts
|
|
7145
|
-
/**
|
|
7146
|
-
* Simulate a batch dispatch built from `items` + the current on-chain
|
|
7147
|
-
* positionIdList. Returns batch diagnostics OR a real simulation result.
|
|
7148
|
-
*/
|
|
7149
|
-
async function simulateBatchDispatch(params) {
|
|
7150
|
-
const { client, poolAddress, account, items, existingPositionIds, usePremiaAsCollateral = false, builderCode = 0n, blockNumber } = params;
|
|
7151
|
-
const { args, diagnostics } = buildBatchDispatchArgs({
|
|
7152
|
-
items,
|
|
7153
|
-
existingPositionIds,
|
|
7154
|
-
usePremiaAsCollateral,
|
|
7155
|
-
builderCode
|
|
7156
|
-
});
|
|
7157
|
-
if (args === null) {
|
|
7158
|
-
const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
|
|
7159
|
-
const meta = await getBlockMeta({
|
|
7160
|
-
client,
|
|
7161
|
-
blockNumber: targetBlockNumber
|
|
7162
|
-
});
|
|
5728
|
+
blockNumber: targetBlockNumber
|
|
5729
|
+
}), getBlockMeta({
|
|
5730
|
+
client,
|
|
5731
|
+
blockNumber: targetBlockNumber
|
|
5732
|
+
})]);
|
|
5733
|
+
const positions = tokenIds.map((tokenId, posIndex) => {
|
|
5734
|
+
const positionData = chunkData[posIndex];
|
|
5735
|
+
const legs = [];
|
|
5736
|
+
for (let legIndex = 0; legIndex < 4; legIndex++) {
|
|
5737
|
+
const legData = positionData[legIndex];
|
|
5738
|
+
const netLiquidity = legData[0];
|
|
5739
|
+
const removedLiquidity = legData[1];
|
|
5740
|
+
if (netLiquidity > 0n || removedLiquidity > 0n) legs.push({
|
|
5741
|
+
legIndex,
|
|
5742
|
+
netLiquidity,
|
|
5743
|
+
removedLiquidity,
|
|
5744
|
+
spreadWad: calculateSpreadWad(netLiquidity, removedLiquidity, vegoid)
|
|
5745
|
+
});
|
|
5746
|
+
}
|
|
7163
5747
|
return {
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
_meta: meta
|
|
5748
|
+
tokenId,
|
|
5749
|
+
legs
|
|
7167
5750
|
};
|
|
7168
|
-
}
|
|
7169
|
-
const sim = await simulateDispatch({
|
|
7170
|
-
client,
|
|
7171
|
-
poolAddress,
|
|
7172
|
-
account,
|
|
7173
|
-
positionIdList: args.positionIdList,
|
|
7174
|
-
finalPositionIdList: args.finalPositionIdList,
|
|
7175
|
-
existingPositionIdList: existingPositionIds,
|
|
7176
|
-
positionSizes: args.positionSizes,
|
|
7177
|
-
tickAndSpreadLimits: args.tickAndSpreadLimits,
|
|
7178
|
-
usePremiaAsCollateral: args.usePremiaAsCollateral,
|
|
7179
|
-
builderCode: args.builderCode,
|
|
7180
|
-
blockNumber
|
|
7181
5751
|
});
|
|
7182
5752
|
return {
|
|
7183
|
-
|
|
7184
|
-
|
|
5753
|
+
positions,
|
|
5754
|
+
_meta
|
|
7185
5755
|
};
|
|
7186
5756
|
}
|
|
7187
5757
|
|
|
7188
5758
|
//#endregion
|
|
7189
|
-
//#region src/panoptic/v2/
|
|
5759
|
+
//#region src/panoptic/v2/sync/pendingPositions.ts
|
|
7190
5760
|
/**
|
|
7191
|
-
*
|
|
5761
|
+
* Add a pending position for optimistic updates.
|
|
7192
5762
|
*
|
|
7193
|
-
*
|
|
7194
|
-
* to measure exact collateral asset movements from the burn.
|
|
5763
|
+
* Called when openPosition() submits a transaction.
|
|
7195
5764
|
*
|
|
7196
|
-
* @param params -
|
|
7197
|
-
* @returns Simulation result with close data or error
|
|
5765
|
+
* @param params - Parameters
|
|
7198
5766
|
*/
|
|
7199
|
-
async function
|
|
7200
|
-
const {
|
|
7201
|
-
const
|
|
7202
|
-
const
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
5767
|
+
async function addPendingPosition(params) {
|
|
5768
|
+
const { chainId, poolAddress, account, storage, position } = params;
|
|
5769
|
+
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
5770
|
+
const existingData = await storage.get(key);
|
|
5771
|
+
let pending;
|
|
5772
|
+
if (existingData) try {
|
|
5773
|
+
pending = jsonSerializer.parse(existingData);
|
|
5774
|
+
} catch {
|
|
5775
|
+
pending = [];
|
|
5776
|
+
}
|
|
5777
|
+
else pending = [];
|
|
5778
|
+
pending.push(position);
|
|
5779
|
+
await storage.set(key, jsonSerializer.stringify(pending));
|
|
5780
|
+
}
|
|
5781
|
+
/**
|
|
5782
|
+
* Get all pending positions for an account.
|
|
5783
|
+
*
|
|
5784
|
+
* @param params - Parameters
|
|
5785
|
+
* @returns Array of pending positions
|
|
5786
|
+
*/
|
|
5787
|
+
async function getPendingPositions(params) {
|
|
5788
|
+
const { chainId, poolAddress, account, storage } = params;
|
|
5789
|
+
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
5790
|
+
const data = await storage.get(key);
|
|
5791
|
+
if (!data) return [];
|
|
7215
5792
|
try {
|
|
7216
|
-
const
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
args: [
|
|
7221
|
-
[tokenId],
|
|
7222
|
-
finalPositionIdList,
|
|
7223
|
-
[0n],
|
|
7224
|
-
[tickLimits$1],
|
|
7225
|
-
usePremiaAsCollateral,
|
|
7226
|
-
builderCode
|
|
7227
|
-
]
|
|
7228
|
-
});
|
|
7229
|
-
const flowResult = await simulateWithTokenFlow({
|
|
7230
|
-
client,
|
|
7231
|
-
poolAddress,
|
|
7232
|
-
user: account,
|
|
7233
|
-
callData,
|
|
7234
|
-
blockNumber: targetBlockNumber
|
|
7235
|
-
});
|
|
7236
|
-
if (!flowResult.success || !flowResult.tokenFlow) throw flowResult.rawError ?? new PanopticError(flowResult.error || "Token flow simulation failed");
|
|
7237
|
-
const tokenFlow = flowResult.tokenFlow;
|
|
7238
|
-
const _meta = await metaPromise;
|
|
7239
|
-
const data = {
|
|
7240
|
-
amount0Received: tokenFlow.delta0,
|
|
7241
|
-
amount1Received: tokenFlow.delta1,
|
|
7242
|
-
premiaCollected0: null,
|
|
7243
|
-
premiaCollected1: null,
|
|
7244
|
-
postCollateral0: tokenFlow.balanceAfter0,
|
|
7245
|
-
postCollateral1: tokenFlow.balanceAfter1,
|
|
7246
|
-
realizedPnL0: null,
|
|
7247
|
-
realizedPnL1: null
|
|
7248
|
-
};
|
|
7249
|
-
return {
|
|
7250
|
-
success: true,
|
|
7251
|
-
data,
|
|
7252
|
-
gasEstimate: flowResult.gasEstimate,
|
|
7253
|
-
tokenFlow,
|
|
7254
|
-
_meta
|
|
7255
|
-
};
|
|
7256
|
-
} catch (error) {
|
|
7257
|
-
const _meta = await metaPromise;
|
|
7258
|
-
return {
|
|
7259
|
-
success: false,
|
|
7260
|
-
error: error instanceof PanopticError ? error : new PanopticError(error instanceof Error ? error.message : "Simulation failed", error instanceof Error ? error : void 0),
|
|
7261
|
-
_meta
|
|
7262
|
-
};
|
|
5793
|
+
const pending = jsonSerializer.parse(data);
|
|
5794
|
+
return pending.filter((p) => p.status === "pending");
|
|
5795
|
+
} catch {
|
|
5796
|
+
return [];
|
|
7263
5797
|
}
|
|
7264
5798
|
}
|
|
7265
|
-
|
|
7266
|
-
//#endregion
|
|
7267
|
-
//#region src/panoptic/v2/simulations/simulateForceExercise.ts
|
|
7268
5799
|
/**
|
|
7269
|
-
*
|
|
5800
|
+
* Mark a pending position as confirmed.
|
|
7270
5801
|
*
|
|
7271
|
-
*
|
|
7272
|
-
*
|
|
5802
|
+
* Called when syncPositions() finds the OptionMinted event.
|
|
5803
|
+
*
|
|
5804
|
+
* @param params - Parameters
|
|
7273
5805
|
*/
|
|
7274
|
-
async function
|
|
7275
|
-
const {
|
|
7276
|
-
const
|
|
7277
|
-
const
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
});
|
|
7281
|
-
const emptyTokenFlow = {
|
|
7282
|
-
delta0: 0n,
|
|
7283
|
-
delta1: 0n,
|
|
7284
|
-
balanceBefore0: 0n,
|
|
7285
|
-
balanceBefore1: 0n,
|
|
7286
|
-
balanceAfter0: 0n,
|
|
7287
|
-
balanceAfter1: 0n,
|
|
7288
|
-
tickBefore: null,
|
|
7289
|
-
tickAfter: null
|
|
7290
|
-
};
|
|
5806
|
+
async function confirmPendingPosition(params) {
|
|
5807
|
+
const { chainId, poolAddress, account, storage, tokenId } = params;
|
|
5808
|
+
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
5809
|
+
const existingData = await storage.get(key);
|
|
5810
|
+
if (!existingData) return;
|
|
5811
|
+
let pending;
|
|
7291
5812
|
try {
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
args: [
|
|
7296
|
-
positionIdListFrom,
|
|
7297
|
-
user,
|
|
7298
|
-
positionIdListTo,
|
|
7299
|
-
positionIdListToFinal,
|
|
7300
|
-
0n
|
|
7301
|
-
]
|
|
7302
|
-
});
|
|
7303
|
-
const flowResult = await simulateWithTokenFlow({
|
|
7304
|
-
client,
|
|
7305
|
-
poolAddress,
|
|
7306
|
-
user: account,
|
|
7307
|
-
callData,
|
|
7308
|
-
blockNumber: targetBlockNumber
|
|
7309
|
-
});
|
|
7310
|
-
if (!flowResult.success || !flowResult.tokenFlow) {
|
|
7311
|
-
const errorMessage = flowResult.error || "Simulation failed";
|
|
7312
|
-
const isNotExercisable = errorMessage.includes("NoLegsExercisable") || errorMessage.includes("NotALongLeg");
|
|
7313
|
-
if (isNotExercisable) {
|
|
7314
|
-
const _meta$1 = await metaPromise;
|
|
7315
|
-
const data$1 = {
|
|
7316
|
-
exerciseFee0: 0n,
|
|
7317
|
-
exerciseFee1: 0n,
|
|
7318
|
-
canExercise: false,
|
|
7319
|
-
reason: errorMessage.includes("NoLegsExercisable") ? "No legs are exercisable (not ITM)" : "Position does not have a long leg"
|
|
7320
|
-
};
|
|
7321
|
-
return {
|
|
7322
|
-
success: true,
|
|
7323
|
-
data: data$1,
|
|
7324
|
-
gasEstimate: 0n,
|
|
7325
|
-
tokenFlow: emptyTokenFlow,
|
|
7326
|
-
_meta: _meta$1
|
|
7327
|
-
};
|
|
7328
|
-
}
|
|
7329
|
-
throw new PanopticError(errorMessage);
|
|
7330
|
-
}
|
|
7331
|
-
const _meta = await metaPromise;
|
|
7332
|
-
const tokenFlow = flowResult.tokenFlow;
|
|
7333
|
-
const data = {
|
|
7334
|
-
exerciseFee0: tokenFlow.delta0,
|
|
7335
|
-
exerciseFee1: tokenFlow.delta1,
|
|
7336
|
-
canExercise: true
|
|
7337
|
-
};
|
|
7338
|
-
return {
|
|
7339
|
-
success: true,
|
|
7340
|
-
data,
|
|
7341
|
-
gasEstimate: flowResult.gasEstimate,
|
|
7342
|
-
tokenFlow,
|
|
7343
|
-
_meta
|
|
7344
|
-
};
|
|
7345
|
-
} catch (error) {
|
|
7346
|
-
const _meta = await metaPromise;
|
|
7347
|
-
const errorMessage = error instanceof Error ? error.message : "Simulation failed";
|
|
7348
|
-
const isNotExercisable = errorMessage.includes("NoLegsExercisable") || errorMessage.includes("NotALongLeg");
|
|
7349
|
-
if (isNotExercisable) {
|
|
7350
|
-
const data = {
|
|
7351
|
-
exerciseFee0: 0n,
|
|
7352
|
-
exerciseFee1: 0n,
|
|
7353
|
-
canExercise: false,
|
|
7354
|
-
reason: errorMessage.includes("NoLegsExercisable") ? "No legs are exercisable (not ITM)" : "Position does not have a long leg"
|
|
7355
|
-
};
|
|
7356
|
-
return {
|
|
7357
|
-
success: true,
|
|
7358
|
-
data,
|
|
7359
|
-
gasEstimate: 0n,
|
|
7360
|
-
tokenFlow: emptyTokenFlow,
|
|
7361
|
-
_meta
|
|
7362
|
-
};
|
|
7363
|
-
}
|
|
7364
|
-
return {
|
|
7365
|
-
success: false,
|
|
7366
|
-
error: error instanceof PanopticError ? error : new PanopticError(errorMessage, error instanceof Error ? error : void 0),
|
|
7367
|
-
_meta
|
|
7368
|
-
};
|
|
5813
|
+
pending = jsonSerializer.parse(existingData);
|
|
5814
|
+
} catch {
|
|
5815
|
+
return;
|
|
7369
5816
|
}
|
|
5817
|
+
const updated = pending.map((p) => p.tokenId === tokenId ? {
|
|
5818
|
+
...p,
|
|
5819
|
+
status: "confirmed"
|
|
5820
|
+
} : p);
|
|
5821
|
+
const remaining = updated.filter((p) => p.status === "pending");
|
|
5822
|
+
if (remaining.length === 0) await storage.delete(key);
|
|
5823
|
+
else await storage.set(key, jsonSerializer.stringify(remaining));
|
|
5824
|
+
}
|
|
5825
|
+
/**
|
|
5826
|
+
* Mark a pending position as failed (transaction reverted).
|
|
5827
|
+
*
|
|
5828
|
+
* @param params - Parameters
|
|
5829
|
+
*/
|
|
5830
|
+
async function failPendingPosition(params) {
|
|
5831
|
+
const { chainId, poolAddress, account, storage, txHash } = params;
|
|
5832
|
+
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
5833
|
+
const existingData = await storage.get(key);
|
|
5834
|
+
if (!existingData) return;
|
|
5835
|
+
let pending;
|
|
5836
|
+
try {
|
|
5837
|
+
pending = jsonSerializer.parse(existingData);
|
|
5838
|
+
} catch {
|
|
5839
|
+
return;
|
|
5840
|
+
}
|
|
5841
|
+
const remaining = pending.filter((p) => p.txHash !== txHash);
|
|
5842
|
+
if (remaining.length === 0) await storage.delete(key);
|
|
5843
|
+
else await storage.set(key, jsonSerializer.stringify(remaining));
|
|
5844
|
+
}
|
|
5845
|
+
/**
|
|
5846
|
+
* Clear all pending positions for an account.
|
|
5847
|
+
*
|
|
5848
|
+
* @param params - Parameters
|
|
5849
|
+
*/
|
|
5850
|
+
async function clearPendingPositions(params) {
|
|
5851
|
+
const { chainId, poolAddress, account, storage } = params;
|
|
5852
|
+
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
5853
|
+
await storage.delete(key);
|
|
5854
|
+
}
|
|
5855
|
+
/**
|
|
5856
|
+
* Clean up stale pending positions.
|
|
5857
|
+
*
|
|
5858
|
+
* Removes pending positions older than the specified block threshold.
|
|
5859
|
+
* This handles cases where transactions were dropped from the mempool.
|
|
5860
|
+
*
|
|
5861
|
+
* @param params - Parameters
|
|
5862
|
+
* @param maxAgeBlocks - Maximum age in blocks (default: 100)
|
|
5863
|
+
* @param currentBlock - Current block number
|
|
5864
|
+
*/
|
|
5865
|
+
async function cleanupStalePendingPositions(params, currentBlock, maxAgeBlocks = 100n) {
|
|
5866
|
+
const { chainId, poolAddress, account, storage } = params;
|
|
5867
|
+
const key = getPendingPositionsKey(chainId, poolAddress, account);
|
|
5868
|
+
const existingData = await storage.get(key);
|
|
5869
|
+
if (!existingData) return;
|
|
5870
|
+
let pending;
|
|
5871
|
+
try {
|
|
5872
|
+
pending = jsonSerializer.parse(existingData);
|
|
5873
|
+
} catch {
|
|
5874
|
+
return;
|
|
5875
|
+
}
|
|
5876
|
+
const threshold = currentBlock - maxAgeBlocks;
|
|
5877
|
+
const remaining = pending.filter((p) => p.status === "pending" && p.submittedAtBlock > threshold);
|
|
5878
|
+
if (remaining.length === 0) await storage.delete(key);
|
|
5879
|
+
else if (remaining.length !== pending.length) await storage.set(key, jsonSerializer.stringify(remaining));
|
|
7370
5880
|
}
|
|
7371
5881
|
|
|
7372
5882
|
//#endregion
|
|
7373
|
-
//#region src/panoptic/v2/simulations/
|
|
5883
|
+
//#region src/panoptic/v2/simulations/simulateBatchDispatch.ts
|
|
7374
5884
|
/**
|
|
7375
|
-
* Simulate a
|
|
7376
|
-
*
|
|
7377
|
-
* @param params - Simulation parameters
|
|
7378
|
-
* @returns Simulation result with liquidation data or error
|
|
5885
|
+
* Simulate a batch dispatch built from `items` + the current on-chain
|
|
5886
|
+
* positionIdList. Returns batch diagnostics OR a real simulation result.
|
|
7379
5887
|
*/
|
|
7380
|
-
async function
|
|
7381
|
-
const { client, poolAddress, account,
|
|
7382
|
-
const
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
5888
|
+
async function simulateBatchDispatch(params) {
|
|
5889
|
+
const { client, poolAddress, account, items, existingPositionIds, usePremiaAsCollateral = false, builderCode = 0n, blockNumber } = params;
|
|
5890
|
+
const { args, diagnostics } = buildBatchDispatchArgs({
|
|
5891
|
+
items,
|
|
5892
|
+
existingPositionIds,
|
|
5893
|
+
usePremiaAsCollateral,
|
|
5894
|
+
builderCode
|
|
7386
5895
|
});
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
balanceBefore0: 0n,
|
|
7391
|
-
balanceBefore1: 0n,
|
|
7392
|
-
balanceAfter0: 0n,
|
|
7393
|
-
balanceAfter1: 0n,
|
|
7394
|
-
tickBefore: null,
|
|
7395
|
-
tickAfter: null
|
|
7396
|
-
};
|
|
7397
|
-
try {
|
|
7398
|
-
const callData = encodeFunctionData({
|
|
7399
|
-
abi: panopticPoolV2Abi,
|
|
7400
|
-
functionName: "dispatchFrom",
|
|
7401
|
-
args: [
|
|
7402
|
-
positionIdListFrom,
|
|
7403
|
-
liquidatee,
|
|
7404
|
-
positionIdListTo,
|
|
7405
|
-
positionIdListToFinal,
|
|
7406
|
-
0n
|
|
7407
|
-
]
|
|
7408
|
-
});
|
|
7409
|
-
const flowResult = await simulateWithTokenFlow({
|
|
5896
|
+
if (args === null) {
|
|
5897
|
+
const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
|
|
5898
|
+
const meta = await getBlockMeta({
|
|
7410
5899
|
client,
|
|
7411
|
-
poolAddress,
|
|
7412
|
-
user: account,
|
|
7413
|
-
callData,
|
|
7414
5900
|
blockNumber: targetBlockNumber
|
|
7415
5901
|
});
|
|
7416
|
-
if (!flowResult.success || !flowResult.tokenFlow) {
|
|
7417
|
-
const errorMessage = flowResult.error || "Simulation failed";
|
|
7418
|
-
const isNotLiquidatable = errorMessage.includes("NotMarginCalled") || errorMessage.includes("AccountInsolvent");
|
|
7419
|
-
if (isNotLiquidatable) {
|
|
7420
|
-
const _meta$1 = await metaPromise;
|
|
7421
|
-
const data$1 = {
|
|
7422
|
-
bonus0: 0n,
|
|
7423
|
-
bonus1: 0n,
|
|
7424
|
-
positionsClosed: [],
|
|
7425
|
-
isLiquidatable: false,
|
|
7426
|
-
shortfall0: 0n,
|
|
7427
|
-
shortfall1: 0n
|
|
7428
|
-
};
|
|
7429
|
-
return {
|
|
7430
|
-
success: true,
|
|
7431
|
-
data: data$1,
|
|
7432
|
-
gasEstimate: 0n,
|
|
7433
|
-
tokenFlow: emptyTokenFlow,
|
|
7434
|
-
_meta: _meta$1
|
|
7435
|
-
};
|
|
7436
|
-
}
|
|
7437
|
-
throw new PanopticError(errorMessage);
|
|
7438
|
-
}
|
|
7439
|
-
const _meta = await metaPromise;
|
|
7440
|
-
const tokenFlow = flowResult.tokenFlow;
|
|
7441
|
-
const positionsClosed = positionIdListTo.filter((id) => !positionIdListToFinal.includes(id));
|
|
7442
|
-
const data = {
|
|
7443
|
-
bonus0: tokenFlow.delta0 > 0n ? tokenFlow.delta0 : 0n,
|
|
7444
|
-
bonus1: tokenFlow.delta1 > 0n ? tokenFlow.delta1 : 0n,
|
|
7445
|
-
positionsClosed,
|
|
7446
|
-
isLiquidatable: true,
|
|
7447
|
-
shortfall0: 0n,
|
|
7448
|
-
shortfall1: 0n
|
|
7449
|
-
};
|
|
7450
|
-
return {
|
|
7451
|
-
success: true,
|
|
7452
|
-
data,
|
|
7453
|
-
gasEstimate: flowResult.gasEstimate,
|
|
7454
|
-
tokenFlow,
|
|
7455
|
-
_meta
|
|
7456
|
-
};
|
|
7457
|
-
} catch (error) {
|
|
7458
|
-
const _meta = await metaPromise;
|
|
7459
|
-
const errorMessage = error instanceof Error ? error.message : "Simulation failed";
|
|
7460
|
-
const isNotLiquidatable = errorMessage.includes("NotMarginCalled") || errorMessage.includes("AccountInsolvent");
|
|
7461
|
-
if (isNotLiquidatable) {
|
|
7462
|
-
const data = {
|
|
7463
|
-
bonus0: 0n,
|
|
7464
|
-
bonus1: 0n,
|
|
7465
|
-
positionsClosed: [],
|
|
7466
|
-
isLiquidatable: false,
|
|
7467
|
-
shortfall0: 0n,
|
|
7468
|
-
shortfall1: 0n
|
|
7469
|
-
};
|
|
7470
|
-
return {
|
|
7471
|
-
success: true,
|
|
7472
|
-
data,
|
|
7473
|
-
gasEstimate: 0n,
|
|
7474
|
-
tokenFlow: emptyTokenFlow,
|
|
7475
|
-
_meta
|
|
7476
|
-
};
|
|
7477
|
-
}
|
|
7478
5902
|
return {
|
|
7479
5903
|
success: false,
|
|
7480
|
-
|
|
7481
|
-
_meta
|
|
5904
|
+
diagnostics,
|
|
5905
|
+
_meta: meta
|
|
7482
5906
|
};
|
|
7483
5907
|
}
|
|
5908
|
+
const sim = await simulateDispatch({
|
|
5909
|
+
client,
|
|
5910
|
+
poolAddress,
|
|
5911
|
+
account,
|
|
5912
|
+
positionIdList: args.positionIdList,
|
|
5913
|
+
finalPositionIdList: args.finalPositionIdList,
|
|
5914
|
+
existingPositionIdList: existingPositionIds,
|
|
5915
|
+
positionSizes: args.positionSizes,
|
|
5916
|
+
tickAndSpreadLimits: args.tickAndSpreadLimits,
|
|
5917
|
+
usePremiaAsCollateral: args.usePremiaAsCollateral,
|
|
5918
|
+
builderCode: args.builderCode,
|
|
5919
|
+
blockNumber
|
|
5920
|
+
});
|
|
5921
|
+
return {
|
|
5922
|
+
...sim,
|
|
5923
|
+
diagnostics: []
|
|
5924
|
+
};
|
|
7484
5925
|
}
|
|
7485
5926
|
|
|
7486
5927
|
//#endregion
|
|
7487
|
-
//#region src/panoptic/v2/simulations/
|
|
7488
|
-
/** BIT_MASK_128 = (1n << 128n) - 1n */
|
|
7489
|
-
const BIT_MASK_128 = (1n << 128n) - 1n;
|
|
7490
|
-
/**
|
|
7491
|
-
* PanopticPool multicall ABI (inherited from Uniswap).
|
|
7492
|
-
*/
|
|
7493
|
-
const multicallAbi = [{
|
|
7494
|
-
type: "function",
|
|
7495
|
-
name: "multicall",
|
|
7496
|
-
inputs: [{
|
|
7497
|
-
name: "data",
|
|
7498
|
-
type: "bytes[]"
|
|
7499
|
-
}],
|
|
7500
|
-
outputs: [{
|
|
7501
|
-
name: "results",
|
|
7502
|
-
type: "bytes[]"
|
|
7503
|
-
}],
|
|
7504
|
-
stateMutability: "nonpayable"
|
|
7505
|
-
}];
|
|
5928
|
+
//#region src/panoptic/v2/simulations/simulateClosePosition.ts
|
|
7506
5929
|
/**
|
|
7507
|
-
* Simulate
|
|
5930
|
+
* Simulate closing a position.
|
|
7508
5931
|
*
|
|
7509
|
-
*
|
|
7510
|
-
*
|
|
7511
|
-
* in a single multicall.
|
|
5932
|
+
* Uses PanopticPool.multicall with getAssetsOf-dispatch-getAssetsOf pattern
|
|
5933
|
+
* to measure exact collateral asset movements from the burn.
|
|
7512
5934
|
*
|
|
7513
5935
|
* @param params - Simulation parameters
|
|
7514
|
-
* @returns Simulation result with
|
|
5936
|
+
* @returns Simulation result with close data or error
|
|
7515
5937
|
*/
|
|
7516
|
-
async function
|
|
7517
|
-
const { client, poolAddress, account, positionIdList,
|
|
5938
|
+
async function simulateClosePosition(params) {
|
|
5939
|
+
const { client, poolAddress, account, positionIdList, tokenId, tickLimitLow, tickLimitHigh, spreadLimit = 0n, swapAtMint = false, usePremiaAsCollateral = false, builderCode = 0n, blockNumber } = params;
|
|
7518
5940
|
const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
|
|
7519
5941
|
const metaPromise = getBlockMeta({
|
|
7520
5942
|
client,
|
|
7521
5943
|
blockNumber: targetBlockNumber
|
|
7522
5944
|
});
|
|
5945
|
+
const tickLimits$1 = swapAtMint ? [
|
|
5946
|
+
Number(tickLimitHigh),
|
|
5947
|
+
Number(tickLimitLow),
|
|
5948
|
+
Number(spreadLimit)
|
|
5949
|
+
] : [
|
|
5950
|
+
Number(tickLimitLow),
|
|
5951
|
+
Number(tickLimitHigh),
|
|
5952
|
+
Number(spreadLimit)
|
|
5953
|
+
];
|
|
7523
5954
|
try {
|
|
7524
|
-
|
|
7525
|
-
const positionSizes = providedSizes ?? await getCurrentPositionSizes({
|
|
7526
|
-
client,
|
|
7527
|
-
poolAddress,
|
|
7528
|
-
account,
|
|
7529
|
-
positionIdList,
|
|
7530
|
-
blockNumber: targetBlockNumber
|
|
7531
|
-
});
|
|
7532
|
-
const tickAndSpreadLimits = positionIdList.map(() => [
|
|
7533
|
-
-887272n,
|
|
7534
|
-
887272n,
|
|
7535
|
-
0n
|
|
7536
|
-
]);
|
|
5955
|
+
const finalPositionIdList = positionIdList.filter((id) => id !== tokenId);
|
|
7537
5956
|
const callData = encodeFunctionData({
|
|
7538
5957
|
abi: panopticPoolV2Abi,
|
|
7539
5958
|
functionName: "dispatch",
|
|
7540
5959
|
args: [
|
|
7541
|
-
|
|
7542
|
-
finalPositionIdList
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
Number(t[2])
|
|
7548
|
-
]),
|
|
7549
|
-
false,
|
|
7550
|
-
0n
|
|
5960
|
+
[tokenId],
|
|
5961
|
+
finalPositionIdList,
|
|
5962
|
+
[0n],
|
|
5963
|
+
[tickLimits$1],
|
|
5964
|
+
usePremiaAsCollateral,
|
|
5965
|
+
builderCode
|
|
7551
5966
|
]
|
|
7552
5967
|
});
|
|
7553
5968
|
const flowResult = await simulateWithTokenFlow({
|
|
@@ -7559,23 +5974,16 @@ async function simulateSettle(params) {
|
|
|
7559
5974
|
});
|
|
7560
5975
|
if (!flowResult.success || !flowResult.tokenFlow) throw flowResult.rawError ?? new PanopticError(flowResult.error || "Token flow simulation failed");
|
|
7561
5976
|
const tokenFlow = flowResult.tokenFlow;
|
|
7562
|
-
let forfeitAmounts;
|
|
7563
|
-
if (tokenId !== void 0) forfeitAmounts = await computeForfeitAmounts({
|
|
7564
|
-
client,
|
|
7565
|
-
poolAddress,
|
|
7566
|
-
account,
|
|
7567
|
-
positionIdList,
|
|
7568
|
-
tokenId,
|
|
7569
|
-
dispatchCallData: callData,
|
|
7570
|
-
blockNumber: targetBlockNumber
|
|
7571
|
-
});
|
|
7572
5977
|
const _meta = await metaPromise;
|
|
7573
5978
|
const data = {
|
|
7574
|
-
|
|
7575
|
-
|
|
5979
|
+
amount0Received: tokenFlow.delta0,
|
|
5980
|
+
amount1Received: tokenFlow.delta1,
|
|
5981
|
+
premiaCollected0: null,
|
|
5982
|
+
premiaCollected1: null,
|
|
7576
5983
|
postCollateral0: tokenFlow.balanceAfter0,
|
|
7577
5984
|
postCollateral1: tokenFlow.balanceAfter1,
|
|
7578
|
-
|
|
5985
|
+
realizedPnL0: null,
|
|
5986
|
+
realizedPnL1: null
|
|
7579
5987
|
};
|
|
7580
5988
|
return {
|
|
7581
5989
|
success: true,
|
|
@@ -7593,95 +6001,17 @@ async function simulateSettle(params) {
|
|
|
7593
6001
|
};
|
|
7594
6002
|
}
|
|
7595
6003
|
}
|
|
7596
|
-
/**
|
|
7597
|
-
* Compute forfeit amounts by chaining dispatch + getFullPositionsData
|
|
7598
|
-
* in a single PanopticPool.multicall.
|
|
7599
|
-
*/
|
|
7600
|
-
async function computeForfeitAmounts(params) {
|
|
7601
|
-
const { client, poolAddress, account, tokenId, dispatchCallData, blockNumber } = params;
|
|
7602
|
-
const feesCallAvailable = encodeFunctionData({
|
|
7603
|
-
abi: panopticPoolV2Abi,
|
|
7604
|
-
functionName: "getFullPositionsData",
|
|
7605
|
-
args: [
|
|
7606
|
-
account,
|
|
7607
|
-
false,
|
|
7608
|
-
[tokenId]
|
|
7609
|
-
]
|
|
7610
|
-
});
|
|
7611
|
-
const feesCallTotal = encodeFunctionData({
|
|
7612
|
-
abi: panopticPoolV2Abi,
|
|
7613
|
-
functionName: "getFullPositionsData",
|
|
7614
|
-
args: [
|
|
7615
|
-
account,
|
|
7616
|
-
true,
|
|
7617
|
-
[tokenId]
|
|
7618
|
-
]
|
|
7619
|
-
});
|
|
7620
|
-
try {
|
|
7621
|
-
const { result } = await client.simulateContract({
|
|
7622
|
-
address: poolAddress,
|
|
7623
|
-
abi: multicallAbi,
|
|
7624
|
-
functionName: "multicall",
|
|
7625
|
-
args: [[
|
|
7626
|
-
dispatchCallData,
|
|
7627
|
-
feesCallAvailable,
|
|
7628
|
-
feesCallTotal
|
|
7629
|
-
]],
|
|
7630
|
-
account,
|
|
7631
|
-
blockNumber
|
|
7632
|
-
});
|
|
7633
|
-
const decodeFeesResult = (data) => {
|
|
7634
|
-
return decodeFunctionResult({
|
|
7635
|
-
abi: panopticPoolV2Abi,
|
|
7636
|
-
functionName: "getFullPositionsData",
|
|
7637
|
-
data
|
|
7638
|
-
})[0];
|
|
7639
|
-
};
|
|
7640
|
-
const availablePremium = decodeFeesResult(result[1]);
|
|
7641
|
-
const totalPremium = decodeFeesResult(result[2]);
|
|
7642
|
-
const available0 = availablePremium & BIT_MASK_128;
|
|
7643
|
-
const available1 = availablePremium >> 128n;
|
|
7644
|
-
const total0 = totalPremium & BIT_MASK_128;
|
|
7645
|
-
const total1 = totalPremium >> 128n;
|
|
7646
|
-
return [total0 - available0, total1 - available1];
|
|
7647
|
-
} catch (error) {
|
|
7648
|
-
throw new PanopticError("Forfeit amount computation failed", error instanceof Error ? error : void 0);
|
|
7649
|
-
}
|
|
7650
|
-
}
|
|
7651
6004
|
|
|
7652
6005
|
//#endregion
|
|
7653
|
-
//#region src/panoptic/v2/simulations/
|
|
7654
|
-
/** Soft-failure revert markers for settle premium (target-state issues, not caller errors). */
|
|
7655
|
-
const SOFT_FAILURES = [
|
|
7656
|
-
{
|
|
7657
|
-
marker: "AccountInsolvent",
|
|
7658
|
-
reason: "Target account is insolvent; premium cannot be settled"
|
|
7659
|
-
},
|
|
7660
|
-
{
|
|
7661
|
-
marker: "PositionNotOwned",
|
|
7662
|
-
reason: "Target account no longer owns the position"
|
|
7663
|
-
},
|
|
7664
|
-
{
|
|
7665
|
-
marker: "StaleOracle",
|
|
7666
|
-
reason: "Oracle price is stale; settlement temporarily unavailable"
|
|
7667
|
-
},
|
|
7668
|
-
{
|
|
7669
|
-
marker: "InputListFail",
|
|
7670
|
-
reason: "Position list is stale (target positions changed)"
|
|
7671
|
-
}
|
|
7672
|
-
];
|
|
6006
|
+
//#region src/panoptic/v2/simulations/simulateForceExercise.ts
|
|
7673
6007
|
/**
|
|
7674
|
-
* Simulate
|
|
7675
|
-
* (equal-length `positionIdListTo`/`positionIdListToFinal` selects the settle mode).
|
|
7676
|
-
*
|
|
7677
|
-
* The measured token flow is the CALLER's collateral delta — i.e. the premium
|
|
7678
|
-
* the caller receives from the settlement (for chunks they sold).
|
|
6008
|
+
* Simulate a force exercise operation.
|
|
7679
6009
|
*
|
|
7680
6010
|
* @param params - Simulation parameters
|
|
7681
|
-
* @returns Simulation result with
|
|
6011
|
+
* @returns Simulation result with exercise data or error
|
|
7682
6012
|
*/
|
|
7683
|
-
async function
|
|
7684
|
-
const { client, poolAddress, account, user, positionIdListFrom,
|
|
6013
|
+
async function simulateForceExercise(params) {
|
|
6014
|
+
const { client, poolAddress, account, user, positionIdListFrom, positionIdListTo, positionIdListToFinal, blockNumber } = params;
|
|
7685
6015
|
const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
|
|
7686
6016
|
const metaPromise = getBlockMeta({
|
|
7687
6017
|
client,
|
|
@@ -7697,65 +6027,39 @@ async function simulateSettlePremiumFrom(params) {
|
|
|
7697
6027
|
tickBefore: null,
|
|
7698
6028
|
tickAfter: null
|
|
7699
6029
|
};
|
|
7700
|
-
const softFailure = (errorMessage) => {
|
|
7701
|
-
const match = SOFT_FAILURES.find(({ marker }) => errorMessage.includes(marker));
|
|
7702
|
-
return match ? {
|
|
7703
|
-
premium0: 0n,
|
|
7704
|
-
premium1: 0n,
|
|
7705
|
-
settled0: 0n,
|
|
7706
|
-
settled1: 0n,
|
|
7707
|
-
canSettle: false,
|
|
7708
|
-
reason: match.reason
|
|
7709
|
-
} : null;
|
|
7710
|
-
};
|
|
7711
6030
|
try {
|
|
7712
|
-
const orderedList = tokenId !== void 0 ? orderListForSettle(positionIdList, tokenId) : positionIdList;
|
|
7713
6031
|
const callData = encodeFunctionData({
|
|
7714
6032
|
abi: panopticPoolV2Abi,
|
|
7715
6033
|
functionName: "dispatchFrom",
|
|
7716
6034
|
args: [
|
|
7717
6035
|
positionIdListFrom,
|
|
7718
6036
|
user,
|
|
7719
|
-
|
|
7720
|
-
|
|
6037
|
+
positionIdListTo,
|
|
6038
|
+
positionIdListToFinal,
|
|
7721
6039
|
0n
|
|
7722
6040
|
]
|
|
7723
6041
|
});
|
|
7724
|
-
const availablePremiumCallData = encodeFunctionData({
|
|
7725
|
-
abi: panopticPoolV2Abi,
|
|
7726
|
-
functionName: "getFullPositionsData",
|
|
7727
|
-
args: [
|
|
7728
|
-
account,
|
|
7729
|
-
false,
|
|
7730
|
-
positionIdListFrom
|
|
7731
|
-
]
|
|
7732
|
-
});
|
|
7733
|
-
const buyerOwedCallData = encodeFunctionData({
|
|
7734
|
-
abi: panopticPoolV2Abi,
|
|
7735
|
-
functionName: "getFullPositionsData",
|
|
7736
|
-
args: [
|
|
7737
|
-
user,
|
|
7738
|
-
true,
|
|
7739
|
-
orderedList
|
|
7740
|
-
]
|
|
7741
|
-
});
|
|
7742
6042
|
const flowResult = await simulateWithTokenFlow({
|
|
7743
6043
|
client,
|
|
7744
6044
|
poolAddress,
|
|
7745
6045
|
user: account,
|
|
7746
6046
|
callData,
|
|
7747
|
-
blockNumber: targetBlockNumber
|
|
7748
|
-
preCallData: [availablePremiumCallData, buyerOwedCallData],
|
|
7749
|
-
postCallData: [availablePremiumCallData, buyerOwedCallData]
|
|
6047
|
+
blockNumber: targetBlockNumber
|
|
7750
6048
|
});
|
|
7751
6049
|
if (!flowResult.success || !flowResult.tokenFlow) {
|
|
7752
6050
|
const errorMessage = flowResult.error || "Simulation failed";
|
|
7753
|
-
const
|
|
7754
|
-
if (
|
|
6051
|
+
const isNotExercisable = errorMessage.includes("NoLegsExercisable") || errorMessage.includes("NotALongLeg");
|
|
6052
|
+
if (isNotExercisable) {
|
|
7755
6053
|
const _meta$1 = await metaPromise;
|
|
6054
|
+
const data$1 = {
|
|
6055
|
+
exerciseFee0: 0n,
|
|
6056
|
+
exerciseFee1: 0n,
|
|
6057
|
+
canExercise: false,
|
|
6058
|
+
reason: errorMessage.includes("NoLegsExercisable") ? "No legs are exercisable (not ITM)" : "Position does not have a long leg"
|
|
6059
|
+
};
|
|
7756
6060
|
return {
|
|
7757
6061
|
success: true,
|
|
7758
|
-
data:
|
|
6062
|
+
data: data$1,
|
|
7759
6063
|
gasEstimate: 0n,
|
|
7760
6064
|
tokenFlow: emptyTokenFlow,
|
|
7761
6065
|
_meta: _meta$1
|
|
@@ -7765,47 +6069,10 @@ async function simulateSettlePremiumFrom(params) {
|
|
|
7765
6069
|
}
|
|
7766
6070
|
const _meta = await metaPromise;
|
|
7767
6071
|
const tokenFlow = flowResult.tokenFlow;
|
|
7768
|
-
const decodePremia = (raw) => {
|
|
7769
|
-
const [shortPremiumPacked, longPremiumPacked] = decodeFunctionResult({
|
|
7770
|
-
abi: panopticPoolV2Abi,
|
|
7771
|
-
functionName: "getFullPositionsData",
|
|
7772
|
-
data: raw
|
|
7773
|
-
});
|
|
7774
|
-
const short = decodeLeftRightUnsigned(shortPremiumPacked);
|
|
7775
|
-
const long = decodeLeftRightUnsigned(longPremiumPacked);
|
|
7776
|
-
return {
|
|
7777
|
-
short0: short.right,
|
|
7778
|
-
short1: short.left,
|
|
7779
|
-
long0: long.right,
|
|
7780
|
-
long1: long.left
|
|
7781
|
-
};
|
|
7782
|
-
};
|
|
7783
|
-
let premium0 = 0n;
|
|
7784
|
-
let premium1 = 0n;
|
|
7785
|
-
const preRaw = flowResult.preCallResults?.[0];
|
|
7786
|
-
const postRaw = flowResult.postCallResults?.[0];
|
|
7787
|
-
if (preRaw !== void 0 && postRaw !== void 0) {
|
|
7788
|
-
const pre = decodePremia(preRaw);
|
|
7789
|
-
const post = decodePremia(postRaw);
|
|
7790
|
-
premium0 = post.short0 > pre.short0 ? post.short0 - pre.short0 : 0n;
|
|
7791
|
-
premium1 = post.short1 > pre.short1 ? post.short1 - pre.short1 : 0n;
|
|
7792
|
-
}
|
|
7793
|
-
let settled0 = 0n;
|
|
7794
|
-
let settled1 = 0n;
|
|
7795
|
-
const preBuyerRaw = flowResult.preCallResults?.[1];
|
|
7796
|
-
const postBuyerRaw = flowResult.postCallResults?.[1];
|
|
7797
|
-
if (preBuyerRaw !== void 0 && postBuyerRaw !== void 0) {
|
|
7798
|
-
const pre = decodePremia(preBuyerRaw);
|
|
7799
|
-
const post = decodePremia(postBuyerRaw);
|
|
7800
|
-
settled0 = pre.long0 > post.long0 ? pre.long0 - post.long0 : 0n;
|
|
7801
|
-
settled1 = pre.long1 > post.long1 ? pre.long1 - post.long1 : 0n;
|
|
7802
|
-
}
|
|
7803
6072
|
const data = {
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
settled1,
|
|
7808
|
-
canSettle: true
|
|
6073
|
+
exerciseFee0: tokenFlow.delta0,
|
|
6074
|
+
exerciseFee1: tokenFlow.delta1,
|
|
6075
|
+
canExercise: true
|
|
7809
6076
|
};
|
|
7810
6077
|
return {
|
|
7811
6078
|
success: true,
|
|
@@ -7817,14 +6084,22 @@ async function simulateSettlePremiumFrom(params) {
|
|
|
7817
6084
|
} catch (error) {
|
|
7818
6085
|
const _meta = await metaPromise;
|
|
7819
6086
|
const errorMessage = error instanceof Error ? error.message : "Simulation failed";
|
|
7820
|
-
const
|
|
7821
|
-
if (
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
6087
|
+
const isNotExercisable = errorMessage.includes("NoLegsExercisable") || errorMessage.includes("NotALongLeg");
|
|
6088
|
+
if (isNotExercisable) {
|
|
6089
|
+
const data = {
|
|
6090
|
+
exerciseFee0: 0n,
|
|
6091
|
+
exerciseFee1: 0n,
|
|
6092
|
+
canExercise: false,
|
|
6093
|
+
reason: errorMessage.includes("NoLegsExercisable") ? "No legs are exercisable (not ITM)" : "Position does not have a long leg"
|
|
6094
|
+
};
|
|
6095
|
+
return {
|
|
6096
|
+
success: true,
|
|
6097
|
+
data,
|
|
6098
|
+
gasEstimate: 0n,
|
|
6099
|
+
tokenFlow: emptyTokenFlow,
|
|
6100
|
+
_meta
|
|
6101
|
+
};
|
|
6102
|
+
}
|
|
7828
6103
|
return {
|
|
7829
6104
|
success: false,
|
|
7830
6105
|
error: error instanceof PanopticError ? error : new PanopticError(errorMessage, error instanceof Error ? error : void 0),
|
|
@@ -7834,89 +6109,41 @@ async function simulateSettlePremiumFrom(params) {
|
|
|
7834
6109
|
}
|
|
7835
6110
|
|
|
7836
6111
|
//#endregion
|
|
7837
|
-
//#region src/panoptic/v2/simulations/
|
|
6112
|
+
//#region src/panoptic/v2/simulations/simulateLiquidate.ts
|
|
7838
6113
|
/**
|
|
7839
|
-
* Simulate
|
|
7840
|
-
*
|
|
7841
|
-
* Individual failures (insolvent buyer, stale list, …) are soft: the target
|
|
7842
|
-
* lands in the unsettleable partition instead of failing the batch. Only
|
|
7843
|
-
* unexpected errors reject.
|
|
6114
|
+
* Simulate a liquidation operation.
|
|
7844
6115
|
*
|
|
7845
6116
|
* @param params - Simulation parameters
|
|
7846
|
-
* @returns
|
|
6117
|
+
* @returns Simulation result with liquidation data or error
|
|
7847
6118
|
*/
|
|
7848
|
-
async function
|
|
7849
|
-
const { client, poolAddress, account, positionIdListFrom,
|
|
6119
|
+
async function simulateLiquidate(params) {
|
|
6120
|
+
const { client, poolAddress, account, liquidatee, positionIdListFrom, positionIdListTo, positionIdListToFinal, blockNumber } = params;
|
|
7850
6121
|
const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
|
|
7851
6122
|
const metaPromise = getBlockMeta({
|
|
7852
6123
|
client,
|
|
7853
6124
|
blockNumber: targetBlockNumber
|
|
7854
6125
|
});
|
|
7855
|
-
const
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
})));
|
|
7865
|
-
const results = [];
|
|
7866
|
-
const settleable = [];
|
|
7867
|
-
let unsettleableCount = 0;
|
|
7868
|
-
let premium0 = 0n;
|
|
7869
|
-
let premium1 = 0n;
|
|
7870
|
-
simulations.forEach((sim, i) => {
|
|
7871
|
-
const simulation = sim.success ? sim.data : {
|
|
7872
|
-
premium0: 0n,
|
|
7873
|
-
premium1: 0n,
|
|
7874
|
-
settled0: 0n,
|
|
7875
|
-
settled1: 0n,
|
|
7876
|
-
canSettle: false,
|
|
7877
|
-
reason: sim.error.message
|
|
7878
|
-
};
|
|
7879
|
-
results.push({
|
|
7880
|
-
target: targets[i],
|
|
7881
|
-
simulation
|
|
7882
|
-
});
|
|
7883
|
-
if (simulation.canSettle) {
|
|
7884
|
-
settleable.push(targets[i]);
|
|
7885
|
-
premium0 += simulation.premium0;
|
|
7886
|
-
premium1 += simulation.premium1;
|
|
7887
|
-
} else unsettleableCount += 1;
|
|
7888
|
-
});
|
|
7889
|
-
const _meta = await metaPromise;
|
|
7890
|
-
return {
|
|
7891
|
-
results,
|
|
7892
|
-
settleable,
|
|
7893
|
-
unsettleableCount,
|
|
7894
|
-
premium0,
|
|
7895
|
-
premium1,
|
|
7896
|
-
_meta
|
|
6126
|
+
const emptyTokenFlow = {
|
|
6127
|
+
delta0: 0n,
|
|
6128
|
+
delta1: 0n,
|
|
6129
|
+
balanceBefore0: 0n,
|
|
6130
|
+
balanceBefore1: 0n,
|
|
6131
|
+
balanceAfter0: 0n,
|
|
6132
|
+
balanceAfter1: 0n,
|
|
6133
|
+
tickBefore: null,
|
|
6134
|
+
tickAfter: null
|
|
7897
6135
|
};
|
|
7898
|
-
}
|
|
7899
|
-
/**
|
|
7900
|
-
* Simulate a full settle sequence (all settles + optional close) as the one
|
|
7901
|
-
* multicall that `executeSettleSequence` submits, measuring the caller's
|
|
7902
|
-
* total token flow and gas.
|
|
7903
|
-
*
|
|
7904
|
-
* @param params - Simulation parameters
|
|
7905
|
-
* @returns Simulation result with the caller's net flow, or error
|
|
7906
|
-
*/
|
|
7907
|
-
async function simulateSettleSequence(params) {
|
|
7908
|
-
const { client, poolAddress, account, blockNumber } = params;
|
|
7909
|
-
const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
|
|
7910
|
-
const metaPromise = getBlockMeta({
|
|
7911
|
-
client,
|
|
7912
|
-
blockNumber: targetBlockNumber
|
|
7913
|
-
});
|
|
7914
6136
|
try {
|
|
7915
|
-
const calls = buildSettleSequenceCalls(params);
|
|
7916
6137
|
const callData = encodeFunctionData({
|
|
7917
6138
|
abi: panopticPoolV2Abi,
|
|
7918
|
-
functionName: "
|
|
7919
|
-
args: [
|
|
6139
|
+
functionName: "dispatchFrom",
|
|
6140
|
+
args: [
|
|
6141
|
+
positionIdListFrom,
|
|
6142
|
+
liquidatee,
|
|
6143
|
+
positionIdListTo,
|
|
6144
|
+
positionIdListToFinal,
|
|
6145
|
+
0n
|
|
6146
|
+
]
|
|
7920
6147
|
});
|
|
7921
6148
|
const flowResult = await simulateWithTokenFlow({
|
|
7922
6149
|
client,
|
|
@@ -7925,15 +6152,43 @@ async function simulateSettleSequence(params) {
|
|
|
7925
6152
|
callData,
|
|
7926
6153
|
blockNumber: targetBlockNumber
|
|
7927
6154
|
});
|
|
7928
|
-
if (!flowResult.success || !flowResult.tokenFlow)
|
|
6155
|
+
if (!flowResult.success || !flowResult.tokenFlow) {
|
|
6156
|
+
const errorMessage = flowResult.error || "Simulation failed";
|
|
6157
|
+
const isNotLiquidatable = errorMessage.includes("NotMarginCalled") || errorMessage.includes("AccountInsolvent");
|
|
6158
|
+
if (isNotLiquidatable) {
|
|
6159
|
+
const _meta$1 = await metaPromise;
|
|
6160
|
+
const data$1 = {
|
|
6161
|
+
bonus0: 0n,
|
|
6162
|
+
bonus1: 0n,
|
|
6163
|
+
positionsClosed: [],
|
|
6164
|
+
isLiquidatable: false,
|
|
6165
|
+
shortfall0: 0n,
|
|
6166
|
+
shortfall1: 0n
|
|
6167
|
+
};
|
|
6168
|
+
return {
|
|
6169
|
+
success: true,
|
|
6170
|
+
data: data$1,
|
|
6171
|
+
gasEstimate: 0n,
|
|
6172
|
+
tokenFlow: emptyTokenFlow,
|
|
6173
|
+
_meta: _meta$1
|
|
6174
|
+
};
|
|
6175
|
+
}
|
|
6176
|
+
throw new PanopticError(errorMessage);
|
|
6177
|
+
}
|
|
7929
6178
|
const _meta = await metaPromise;
|
|
7930
6179
|
const tokenFlow = flowResult.tokenFlow;
|
|
6180
|
+
const positionsClosed = positionIdListTo.filter((id) => !positionIdListToFinal.includes(id));
|
|
6181
|
+
const data = {
|
|
6182
|
+
bonus0: tokenFlow.delta0 > 0n ? tokenFlow.delta0 : 0n,
|
|
6183
|
+
bonus1: tokenFlow.delta1 > 0n ? tokenFlow.delta1 : 0n,
|
|
6184
|
+
positionsClosed,
|
|
6185
|
+
isLiquidatable: true,
|
|
6186
|
+
shortfall0: 0n,
|
|
6187
|
+
shortfall1: 0n
|
|
6188
|
+
};
|
|
7931
6189
|
return {
|
|
7932
6190
|
success: true,
|
|
7933
|
-
data
|
|
7934
|
-
delta0: tokenFlow.delta0,
|
|
7935
|
-
delta1: tokenFlow.delta1
|
|
7936
|
-
},
|
|
6191
|
+
data,
|
|
7937
6192
|
gasEstimate: flowResult.gasEstimate,
|
|
7938
6193
|
tokenFlow,
|
|
7939
6194
|
_meta
|
|
@@ -7941,6 +6196,24 @@ async function simulateSettleSequence(params) {
|
|
|
7941
6196
|
} catch (error) {
|
|
7942
6197
|
const _meta = await metaPromise;
|
|
7943
6198
|
const errorMessage = error instanceof Error ? error.message : "Simulation failed";
|
|
6199
|
+
const isNotLiquidatable = errorMessage.includes("NotMarginCalled") || errorMessage.includes("AccountInsolvent");
|
|
6200
|
+
if (isNotLiquidatable) {
|
|
6201
|
+
const data = {
|
|
6202
|
+
bonus0: 0n,
|
|
6203
|
+
bonus1: 0n,
|
|
6204
|
+
positionsClosed: [],
|
|
6205
|
+
isLiquidatable: false,
|
|
6206
|
+
shortfall0: 0n,
|
|
6207
|
+
shortfall1: 0n
|
|
6208
|
+
};
|
|
6209
|
+
return {
|
|
6210
|
+
success: true,
|
|
6211
|
+
data,
|
|
6212
|
+
gasEstimate: 0n,
|
|
6213
|
+
tokenFlow: emptyTokenFlow,
|
|
6214
|
+
_meta
|
|
6215
|
+
};
|
|
6216
|
+
}
|
|
7944
6217
|
return {
|
|
7945
6218
|
success: false,
|
|
7946
6219
|
error: error instanceof PanopticError ? error : new PanopticError(errorMessage, error instanceof Error ? error : void 0),
|
|
@@ -9954,6 +8227,441 @@ function createEventPoller(params) {
|
|
|
9954
8227
|
};
|
|
9955
8228
|
}
|
|
9956
8229
|
|
|
8230
|
+
//#endregion
|
|
8231
|
+
//#region src/panoptic/v2/strike/ladder.ts
|
|
8232
|
+
/** Target number of rungs across a leg's full range (lower tick → upper tick). */
|
|
8233
|
+
const STRIKE_LADDER_TARGET_STRIKES = 16n;
|
|
8234
|
+
/** Nice increment mantissas, in tenths (1, 2.5, 5) × 10ⁿ. */
|
|
8235
|
+
const NICE_MANTISSAS_TENTHS = [
|
|
8236
|
+
10n,
|
|
8237
|
+
25n,
|
|
8238
|
+
50n
|
|
8239
|
+
];
|
|
8240
|
+
/** Upper bound on rung hops when searching for a tick-changing step. */
|
|
8241
|
+
const MAX_STEP_ITERATIONS = 512;
|
|
8242
|
+
/** Decimal precision used when rendering exact tick prices as strings. */
|
|
8243
|
+
const PRICE_PRECISION = 30n;
|
|
8244
|
+
function floorDiv(a, b) {
|
|
8245
|
+
const q = a / b;
|
|
8246
|
+
return a % b !== 0n && a < 0n !== b < 0n ? q - 1n : q;
|
|
8247
|
+
}
|
|
8248
|
+
function floorMod(a, b) {
|
|
8249
|
+
return a - floorDiv(a, b) * b;
|
|
8250
|
+
}
|
|
8251
|
+
function ceilDiv(a, b) {
|
|
8252
|
+
return -floorDiv(-a, b);
|
|
8253
|
+
}
|
|
8254
|
+
/** round(a / b) to nearest, ties up. */
|
|
8255
|
+
function roundDiv(a, b) {
|
|
8256
|
+
return floorDiv(2n * a + b, 2n * b);
|
|
8257
|
+
}
|
|
8258
|
+
function pow10(exp) {
|
|
8259
|
+
let result = 1n;
|
|
8260
|
+
for (let i = 0n; i < exp; i++) result *= 10n;
|
|
8261
|
+
return result;
|
|
8262
|
+
}
|
|
8263
|
+
function gcd(a, b) {
|
|
8264
|
+
a = a < 0n ? -a : a;
|
|
8265
|
+
b = b < 0n ? -b : b;
|
|
8266
|
+
while (b !== 0n) [a, b] = [b, a % b];
|
|
8267
|
+
return a;
|
|
8268
|
+
}
|
|
8269
|
+
function reduce(f) {
|
|
8270
|
+
const g = gcd(f.numerator, f.denominator);
|
|
8271
|
+
return g <= 1n ? f : {
|
|
8272
|
+
numerator: f.numerator / g,
|
|
8273
|
+
denominator: f.denominator / g
|
|
8274
|
+
};
|
|
8275
|
+
}
|
|
8276
|
+
function fromBigint(n) {
|
|
8277
|
+
return {
|
|
8278
|
+
numerator: n,
|
|
8279
|
+
denominator: 1n
|
|
8280
|
+
};
|
|
8281
|
+
}
|
|
8282
|
+
function mul(a, b) {
|
|
8283
|
+
return reduce({
|
|
8284
|
+
numerator: a.numerator * b.numerator,
|
|
8285
|
+
denominator: a.denominator * b.denominator
|
|
8286
|
+
});
|
|
8287
|
+
}
|
|
8288
|
+
function add(a, b) {
|
|
8289
|
+
return reduce({
|
|
8290
|
+
numerator: a.numerator * b.denominator + b.numerator * a.denominator,
|
|
8291
|
+
denominator: a.denominator * b.denominator
|
|
8292
|
+
});
|
|
8293
|
+
}
|
|
8294
|
+
function sub(a, b) {
|
|
8295
|
+
return add(a, {
|
|
8296
|
+
numerator: -b.numerator,
|
|
8297
|
+
denominator: b.denominator
|
|
8298
|
+
});
|
|
8299
|
+
}
|
|
8300
|
+
/** -1 | 0 | 1 comparing a to b. */
|
|
8301
|
+
function cmp(a, b) {
|
|
8302
|
+
const l = a.numerator * b.denominator;
|
|
8303
|
+
const r = b.numerator * a.denominator;
|
|
8304
|
+
return l === r ? 0 : l < r ? -1 : 1;
|
|
8305
|
+
}
|
|
8306
|
+
const LADDER_MIN = fromBigint(1n);
|
|
8307
|
+
/** Largest normalised price on the ladder. Prices above are "outside". */
|
|
8308
|
+
const LADDER_MAX = fromBigint(1000000n);
|
|
8309
|
+
function parseDecimal(value) {
|
|
8310
|
+
const trimmed = value.trim();
|
|
8311
|
+
const [basePart, exponentPart] = trimmed.toLowerCase().split("e");
|
|
8312
|
+
const [integerStr, fractionalStr = ""] = basePart.split(".");
|
|
8313
|
+
let numerator = BigInt(`${integerStr === "" ? "0" : integerStr}${fractionalStr}`);
|
|
8314
|
+
let denominator = pow10(BigInt(fractionalStr.length));
|
|
8315
|
+
if (exponentPart !== void 0 && exponentPart !== "") {
|
|
8316
|
+
const exponent = BigInt(exponentPart);
|
|
8317
|
+
if (exponent > 0n) numerator *= pow10(exponent);
|
|
8318
|
+
else if (exponent < 0n) denominator *= pow10(-exponent);
|
|
8319
|
+
}
|
|
8320
|
+
if (numerator <= 0n) throw new Error("Price must be positive");
|
|
8321
|
+
return reduce({
|
|
8322
|
+
numerator,
|
|
8323
|
+
denominator
|
|
8324
|
+
});
|
|
8325
|
+
}
|
|
8326
|
+
/** Render a positive fraction as a decimal string with trailing zeros trimmed. */
|
|
8327
|
+
function fractionToDecimal(f, precision) {
|
|
8328
|
+
const scaled = f.numerator * pow10(precision) / f.denominator;
|
|
8329
|
+
const digits = scaled.toString().padStart(Number(precision) + 1, "0");
|
|
8330
|
+
const intPart = digits.slice(0, digits.length - Number(precision));
|
|
8331
|
+
const fracPart = digits.slice(digits.length - Number(precision)).replace(/0+$/, "");
|
|
8332
|
+
return fracPart.length === 0 ? intPart : `${intPart}.${fracPart}`;
|
|
8333
|
+
}
|
|
8334
|
+
/** 10^k as an exact fraction, k may be negative. */
|
|
8335
|
+
function powerOfTen(k) {
|
|
8336
|
+
return k >= 0n ? fromBigint(pow10(k)) : {
|
|
8337
|
+
numerator: 1n,
|
|
8338
|
+
denominator: pow10(-k)
|
|
8339
|
+
};
|
|
8340
|
+
}
|
|
8341
|
+
/** floor(log10(f)) for a positive fraction. */
|
|
8342
|
+
function floorLog10(f) {
|
|
8343
|
+
let e = BigInt(f.numerator.toString().length - f.denominator.toString().length);
|
|
8344
|
+
while (cmp(f, powerOfTen(e)) < 0) e -= 1n;
|
|
8345
|
+
while (cmp(f, powerOfTen(e + 1n)) >= 0) e += 1n;
|
|
8346
|
+
return e;
|
|
8347
|
+
}
|
|
8348
|
+
function orientTick(tick, orient) {
|
|
8349
|
+
return orient.asset === 0n ? tick : -tick;
|
|
8350
|
+
}
|
|
8351
|
+
/** Exact-enough price of `tick` in the caller's orientation. */
|
|
8352
|
+
function tickToOrientedPrice(tick, orient) {
|
|
8353
|
+
const price = tickToPriceDecimalScaled(orientTick(tick, orient), orient.assetDecimals, orient.quoteDecimals, PRICE_PRECISION);
|
|
8354
|
+
return parseDecimal(price);
|
|
8355
|
+
}
|
|
8356
|
+
/** Nearest integer tick for a caller-oriented decimal price. */
|
|
8357
|
+
function orientedPriceToTick(price, orient) {
|
|
8358
|
+
const t = priceToTick(price, orient.assetDecimals, orient.quoteDecimals);
|
|
8359
|
+
return orientTick(t, orient);
|
|
8360
|
+
}
|
|
8361
|
+
/**
|
|
8362
|
+
* Relative rung step for a leg: `1.0001^(round(width·tickSpacing / TARGET)) − 1`.
|
|
8363
|
+
* Rungs scale with price (they are multiples of a price-proportional increment), so
|
|
8364
|
+
* spacing them by the range's TARGET-th root yields ≈TARGET rungs across the range
|
|
8365
|
+
* regardless of how wide it is.
|
|
8366
|
+
*/
|
|
8367
|
+
function relativeStep(width, tickSpacing) {
|
|
8368
|
+
validateGeometry(width, tickSpacing);
|
|
8369
|
+
const ticks = roundDiv(width * tickSpacing, STRIKE_LADDER_TARGET_STRIKES);
|
|
8370
|
+
const ratio = parseDecimal(tickToPriceDecimalScaled(ticks < 1n ? 1n : ticks, 0n, 0n, PRICE_PRECISION));
|
|
8371
|
+
return sub(ratio, fromBigint(1n));
|
|
8372
|
+
}
|
|
8373
|
+
/** Nice increment `m × 10ⁿ` (m ∈ {1, 2.5, 5}) with the smallest log-distance to `raw`. */
|
|
8374
|
+
function niceIncrement(raw) {
|
|
8375
|
+
const e = floorLog10(raw);
|
|
8376
|
+
const scale = (tenths) => mul(fromBigint(tenths), powerOfTen(e - 1n));
|
|
8377
|
+
const candidates = [...NICE_MANTISSAS_TENTHS.map(scale), scale(100n)];
|
|
8378
|
+
let lo = candidates[0] ?? powerOfTen(e);
|
|
8379
|
+
for (const hi of candidates.slice(1)) {
|
|
8380
|
+
if (cmp(raw, hi) < 0) return cmp(mul(raw, raw), mul(lo, hi)) <= 0 ? lo : hi;
|
|
8381
|
+
lo = hi;
|
|
8382
|
+
}
|
|
8383
|
+
return lo;
|
|
8384
|
+
}
|
|
8385
|
+
/** Rung increment at normalised price `p` for the given relative step. */
|
|
8386
|
+
function incrementAt(p, rf) {
|
|
8387
|
+
return niceIncrement(mul(p, rf));
|
|
8388
|
+
}
|
|
8389
|
+
/** Normalise a price to `max(p, 1/p)` and remember whether it was inverted. */
|
|
8390
|
+
function normalise(price) {
|
|
8391
|
+
if (price.numerator >= price.denominator) return {
|
|
8392
|
+
value: price,
|
|
8393
|
+
reciprocal: false
|
|
8394
|
+
};
|
|
8395
|
+
return {
|
|
8396
|
+
value: {
|
|
8397
|
+
numerator: price.denominator,
|
|
8398
|
+
denominator: price.numerator
|
|
8399
|
+
},
|
|
8400
|
+
reciprocal: true
|
|
8401
|
+
};
|
|
8402
|
+
}
|
|
8403
|
+
/** Is `r` a multiple of `inc`? */
|
|
8404
|
+
function isMultiple(r, inc) {
|
|
8405
|
+
return r.numerator * inc.denominator % (r.denominator * inc.numerator) === 0n;
|
|
8406
|
+
}
|
|
8407
|
+
/** Smallest multiple of `inc` strictly greater than `x`. */
|
|
8408
|
+
function ceilMultipleStrict(x, inc) {
|
|
8409
|
+
const k = floorDiv(x.numerator * inc.denominator, x.denominator * inc.numerator) + 1n;
|
|
8410
|
+
return mul(fromBigint(k), inc);
|
|
8411
|
+
}
|
|
8412
|
+
/** Largest multiple of `inc` strictly smaller than `x`. */
|
|
8413
|
+
function floorMultipleStrict(x, inc) {
|
|
8414
|
+
const k = ceilDiv(x.numerator * inc.denominator, x.denominator * inc.numerator) - 1n;
|
|
8415
|
+
return mul(fromBigint(k), inc);
|
|
8416
|
+
}
|
|
8417
|
+
/**
|
|
8418
|
+
* The rung set is `{ r : r is a multiple of incrementAt(r) }`. Because the
|
|
8419
|
+
* increment grows with price, a multiple of a finer increment can land inside a
|
|
8420
|
+
* coarser band where it is no longer a rung; these helpers push such a candidate
|
|
8421
|
+
* up/down until it is a multiple of its own band's increment.
|
|
8422
|
+
*/
|
|
8423
|
+
function isRung(r, rf) {
|
|
8424
|
+
return r.numerator > 0n && isMultiple(r, incrementAt(r, rf));
|
|
8425
|
+
}
|
|
8426
|
+
function fixUp(c, rf) {
|
|
8427
|
+
for (let i = 0; i < 64 && !isRung(c, rf); i++) c = ceilMultipleStrict(c, incrementAt(c, rf));
|
|
8428
|
+
return c;
|
|
8429
|
+
}
|
|
8430
|
+
function fixDown(c, rf) {
|
|
8431
|
+
for (let i = 0; i < 64 && c.numerator > 0n && !isRung(c, rf); i++) c = floorMultipleStrict(c, incrementAt(c, rf));
|
|
8432
|
+
return c;
|
|
8433
|
+
}
|
|
8434
|
+
/** Smallest rung strictly above `x` (may exceed the ladder top). */
|
|
8435
|
+
function nextRungAbove(x, rf) {
|
|
8436
|
+
const incHere = incrementAt(x, rf);
|
|
8437
|
+
const incs = [incHere, incrementAt(add(x, incHere), rf)];
|
|
8438
|
+
let best = null;
|
|
8439
|
+
for (const inc of incs) {
|
|
8440
|
+
const c = fixUp(ceilMultipleStrict(x, inc), rf);
|
|
8441
|
+
if (cmp(c, x) > 0 && (best === null || cmp(c, best) < 0)) best = c;
|
|
8442
|
+
}
|
|
8443
|
+
if (best === null) throw new PanopticValidationError("No ladder rung above price");
|
|
8444
|
+
return best;
|
|
8445
|
+
}
|
|
8446
|
+
/** Largest rung strictly below `x` (may fall below the ladder floor). */
|
|
8447
|
+
function prevRungBelow(x, rf) {
|
|
8448
|
+
const incHere = incrementAt(x, rf);
|
|
8449
|
+
const finer = incrementAt(sub(x, incHere), rf);
|
|
8450
|
+
let best = null;
|
|
8451
|
+
for (const inc of [incHere, finer]) {
|
|
8452
|
+
const c = fixDown(floorMultipleStrict(x, inc), rf);
|
|
8453
|
+
if (c.numerator > 0n && cmp(c, x) < 0 && (best === null || cmp(c, best) > 0)) best = c;
|
|
8454
|
+
}
|
|
8455
|
+
return best ?? fromBigint(0n);
|
|
8456
|
+
}
|
|
8457
|
+
/** Nearest rung price to a normalised price, or null when above the ladder. */
|
|
8458
|
+
function nearestRungPrice(normalised, rf) {
|
|
8459
|
+
let rung;
|
|
8460
|
+
if (isRung(normalised, rf)) rung = normalised;
|
|
8461
|
+
else {
|
|
8462
|
+
const up = nextRungAbove(normalised, rf);
|
|
8463
|
+
const down = prevRungBelow(normalised, rf);
|
|
8464
|
+
if (down.numerator <= 0n) rung = up;
|
|
8465
|
+
else rung = cmp(sub(normalised, down), sub(up, normalised)) <= 0 ? down : up;
|
|
8466
|
+
}
|
|
8467
|
+
if (cmp(rung, LADDER_MIN) < 0) rung = LADDER_MIN;
|
|
8468
|
+
if (cmp(rung, LADDER_MAX) > 0) return null;
|
|
8469
|
+
return rung;
|
|
8470
|
+
}
|
|
8471
|
+
/** Step one rung up (+1) or down (-1) on the normalised ladder. Null when leaving it. */
|
|
8472
|
+
function stepRungPrice(rung, direction, rf) {
|
|
8473
|
+
const next = direction > 0n ? nextRungAbove(rung, rf) : prevRungBelow(rung, rf);
|
|
8474
|
+
if (cmp(next, LADDER_MIN) < 0 || cmp(next, LADDER_MAX) > 0) return null;
|
|
8475
|
+
return next;
|
|
8476
|
+
}
|
|
8477
|
+
/** Nominal caller-oriented price of a rung as an exact fraction. */
|
|
8478
|
+
function rungToPrice(rung) {
|
|
8479
|
+
return rung.reciprocal ? {
|
|
8480
|
+
numerator: rung.price.denominator,
|
|
8481
|
+
denominator: rung.price.numerator
|
|
8482
|
+
} : rung.price;
|
|
8483
|
+
}
|
|
8484
|
+
function rungToPriceString(rung) {
|
|
8485
|
+
return fractionToDecimal(rungToPrice(rung), PRICE_PRECISION);
|
|
8486
|
+
}
|
|
8487
|
+
function nearestRung(price, rf) {
|
|
8488
|
+
const { value, reciprocal } = normalise(price);
|
|
8489
|
+
const p = nearestRungPrice(value, rf);
|
|
8490
|
+
return p === null ? null : {
|
|
8491
|
+
price: p,
|
|
8492
|
+
reciprocal
|
|
8493
|
+
};
|
|
8494
|
+
}
|
|
8495
|
+
/**
|
|
8496
|
+
* Step a rung in the caller's price direction. For reciprocal quotes a higher
|
|
8497
|
+
* caller price is a lower normalised price, so the direction flips.
|
|
8498
|
+
*/
|
|
8499
|
+
function stepRung(rung, direction, rf) {
|
|
8500
|
+
const normalisedDir = rung.reciprocal ? direction > 0n ? -1n : 1n : direction;
|
|
8501
|
+
const p = stepRungPrice(rung.price, normalisedDir, rf);
|
|
8502
|
+
return p === null ? null : {
|
|
8503
|
+
price: p,
|
|
8504
|
+
reciprocal: rung.reciprocal
|
|
8505
|
+
};
|
|
8506
|
+
}
|
|
8507
|
+
/** Reject geometry the ladder cannot place: non-positive inputs or a span wider than the tick domain. */
|
|
8508
|
+
function validateGeometry(width, tickSpacing) {
|
|
8509
|
+
if (width <= 0n) throw new PanopticValidationError("width must be positive");
|
|
8510
|
+
if (tickSpacing <= 0n) throw new PanopticValidationError("tickSpacing must be positive");
|
|
8511
|
+
if (width * tickSpacing > MAX_TICK - MIN_TICK) throw new PanopticValidationError("width * tickSpacing exceeds the tick domain");
|
|
8512
|
+
}
|
|
8513
|
+
/** `rangeDown`/`rangeUp` exactly as `PanopticMath.getRangesFromStrike`. */
|
|
8514
|
+
function rangesFromStrike(width, tickSpacing) {
|
|
8515
|
+
const span = width * tickSpacing;
|
|
8516
|
+
return {
|
|
8517
|
+
rangeDown: span / 2n,
|
|
8518
|
+
rangeUp: ceilDiv(span, 2n)
|
|
8519
|
+
};
|
|
8520
|
+
}
|
|
8521
|
+
/**
|
|
8522
|
+
* Nearest valid strike tick to `exactTick` for `width`/`tickSpacing`.
|
|
8523
|
+
* Valid strikes satisfy `strike ≡ rangeDown (mod tickSpacing)`; the result is
|
|
8524
|
+
* shifted in whole tick spacings so the leg's ticks stay within `[MIN_TICK, MAX_TICK]`.
|
|
8525
|
+
*/
|
|
8526
|
+
function canonicalStrikeForWidth(exactTick, width, tickSpacing) {
|
|
8527
|
+
validateGeometry(width, tickSpacing);
|
|
8528
|
+
const { rangeDown, rangeUp } = rangesFromStrike(width, tickSpacing);
|
|
8529
|
+
const offset = floorMod(rangeDown, tickSpacing);
|
|
8530
|
+
let strike = roundDiv(exactTick - offset, tickSpacing) * tickSpacing + offset;
|
|
8531
|
+
while (strike - rangeDown < MIN_TICK) strike += tickSpacing;
|
|
8532
|
+
while (strike + rangeUp > MAX_TICK) strike -= tickSpacing;
|
|
8533
|
+
return strike;
|
|
8534
|
+
}
|
|
8535
|
+
function rungToStrike(rung, geom) {
|
|
8536
|
+
const nominalPrice = rungToPriceString(rung);
|
|
8537
|
+
const exactTick = orientedPriceToTick(nominalPrice, geom.orient);
|
|
8538
|
+
return {
|
|
8539
|
+
tick: canonicalStrikeForWidth(exactTick, geom.width, geom.tickSpacing),
|
|
8540
|
+
nominalPrice
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
8543
|
+
function classifyTick(tick, geom, rf) {
|
|
8544
|
+
const rung = nearestRung(tickToOrientedPrice(tick, geom.orient), rf);
|
|
8545
|
+
if (rung === null) return { kind: "outside-ladder" };
|
|
8546
|
+
const canonical = rungToStrike(rung, geom);
|
|
8547
|
+
return canonical.tick === tick ? {
|
|
8548
|
+
kind: "ladder",
|
|
8549
|
+
nominalPrice: canonical.nominalPrice
|
|
8550
|
+
} : { kind: "off-ladder" };
|
|
8551
|
+
}
|
|
8552
|
+
/**
|
|
8553
|
+
* A rung's canonical tick is only usable when that tick classifies back to the
|
|
8554
|
+
* same rung (`classifyStrike` → 'ladder'). When rungs are finer than the tick
|
|
8555
|
+
* grid two rungs can share a tick and only one of them "owns" it; return the
|
|
8556
|
+
* owner's strike, or null when this rung does not own its tick.
|
|
8557
|
+
*/
|
|
8558
|
+
function ownedStrike(rung, geom, rf) {
|
|
8559
|
+
const strike = rungToStrike(rung, geom);
|
|
8560
|
+
const cls = classifyTick(strike.tick, geom, rf);
|
|
8561
|
+
return cls.kind === "ladder" ? {
|
|
8562
|
+
tick: strike.tick,
|
|
8563
|
+
nominalPrice: cls.nominalPrice
|
|
8564
|
+
} : null;
|
|
8565
|
+
}
|
|
8566
|
+
/**
|
|
8567
|
+
* Classify a strike tick against the ladder for its width.
|
|
8568
|
+
*
|
|
8569
|
+
* - `ladder`: the tick is the canonical tick of its nearest rung → show `nominalPrice`.
|
|
8570
|
+
* - `off-ladder`: an in-range tick that is not a rung's canonical tick (e.g. real AMM
|
|
8571
|
+
* liquidity at an arbitrary strike) → show the exact price.
|
|
8572
|
+
* - `outside-ladder`: the normalised price exceeds 1,000,000 → legacy behaviour.
|
|
8573
|
+
*/
|
|
8574
|
+
function classifyStrike(params) {
|
|
8575
|
+
return classifyTick(params.tick, params, relativeStep(params.width, params.tickSpacing));
|
|
8576
|
+
}
|
|
8577
|
+
/**
|
|
8578
|
+
* Resolve a tick to a ladder strike.
|
|
8579
|
+
*
|
|
8580
|
+
* - `step` 0 (default): the ladder tick nearest to `tick` (the tick's own rung when
|
|
8581
|
+
* it owns one, otherwise the closest owned neighbour).
|
|
8582
|
+
* - `step` ±1: the nearest ladder tick strictly beyond `tick` in that price direction
|
|
8583
|
+
* (rungs finer than the tick grid can collapse onto one tick, so a single rung hop
|
|
8584
|
+
* may not move the strike).
|
|
8585
|
+
*
|
|
8586
|
+
* Returns null when the target lies outside the ladder (normalised price > 1e6).
|
|
8587
|
+
*/
|
|
8588
|
+
function resolveLadderStrike(params) {
|
|
8589
|
+
const step = params.step ?? 0n;
|
|
8590
|
+
const rf = relativeStep(params.width, params.tickSpacing);
|
|
8591
|
+
const start = nearestRung(tickToOrientedPrice(params.tick, params.orient), rf);
|
|
8592
|
+
if (start === null) return null;
|
|
8593
|
+
const startOriented = orientTick(params.tick, params.orient);
|
|
8594
|
+
if (step === 0n) {
|
|
8595
|
+
const own = ownedStrike(start, params, rf);
|
|
8596
|
+
if (own !== null) return own;
|
|
8597
|
+
let lo = start;
|
|
8598
|
+
let hi = start;
|
|
8599
|
+
for (let i = 0; i < MAX_STEP_ITERATIONS; i++) {
|
|
8600
|
+
lo = lo === null ? null : stepRung(lo, -1n, rf);
|
|
8601
|
+
hi = hi === null ? null : stepRung(hi, 1n, rf);
|
|
8602
|
+
const a = lo === null ? null : ownedStrike(lo, params, rf);
|
|
8603
|
+
const b = hi === null ? null : ownedStrike(hi, params, rf);
|
|
8604
|
+
if (a !== null && b !== null) {
|
|
8605
|
+
const da = startOriented - orientTick(a.tick, params.orient);
|
|
8606
|
+
const db = orientTick(b.tick, params.orient) - startOriented;
|
|
8607
|
+
return da <= db ? a : b;
|
|
8608
|
+
}
|
|
8609
|
+
if (a !== null) return a;
|
|
8610
|
+
if (b !== null) return b;
|
|
8611
|
+
if (lo === null && hi === null) return null;
|
|
8612
|
+
}
|
|
8613
|
+
return null;
|
|
8614
|
+
}
|
|
8615
|
+
let rung = start;
|
|
8616
|
+
for (let i = 0; i < MAX_STEP_ITERATIONS && rung !== null; i++) {
|
|
8617
|
+
const own = ownedStrike(rung, params, rf);
|
|
8618
|
+
if (own !== null) {
|
|
8619
|
+
const oriented = orientTick(own.tick, params.orient);
|
|
8620
|
+
if (step > 0n ? oriented > startOriented : oriented < startOriented) return own;
|
|
8621
|
+
}
|
|
8622
|
+
rung = stepRung(rung, step, rf);
|
|
8623
|
+
}
|
|
8624
|
+
return null;
|
|
8625
|
+
}
|
|
8626
|
+
/**
|
|
8627
|
+
* Generate up to `count` ladder strikes centred on `centerTick`, sorted by
|
|
8628
|
+
* ascending caller price and deduplicated by tick. Rungs beyond the ladder are
|
|
8629
|
+
* omitted (the result may be shorter than `count`). Returns null when the
|
|
8630
|
+
* centre itself lies outside the ladder and `[]` when `count` is 0.
|
|
8631
|
+
*/
|
|
8632
|
+
function ladderStrikeSequence(params) {
|
|
8633
|
+
const rf = relativeStep(params.width, params.tickSpacing);
|
|
8634
|
+
if (params.count <= 0) return [];
|
|
8635
|
+
const center = nearestRung(tickToOrientedPrice(params.centerTick, params.orient), rf);
|
|
8636
|
+
if (center === null) return null;
|
|
8637
|
+
const half = Math.floor(params.count / 2);
|
|
8638
|
+
const below = [];
|
|
8639
|
+
const above = [];
|
|
8640
|
+
let r = center;
|
|
8641
|
+
for (let i = 0; i < half && r !== null; i++) {
|
|
8642
|
+
r = stepRung(r, -1n, rf);
|
|
8643
|
+
if (r !== null) below.push(r);
|
|
8644
|
+
}
|
|
8645
|
+
r = center;
|
|
8646
|
+
for (let i = 0; i < params.count - half - 1 && r !== null; i++) {
|
|
8647
|
+
r = stepRung(r, 1n, rf);
|
|
8648
|
+
if (r !== null) above.push(r);
|
|
8649
|
+
}
|
|
8650
|
+
const seen = new Set();
|
|
8651
|
+
const out = [];
|
|
8652
|
+
for (const rung of [
|
|
8653
|
+
...below.reverse(),
|
|
8654
|
+
center,
|
|
8655
|
+
...above
|
|
8656
|
+
]) {
|
|
8657
|
+
const strike = ownedStrike(rung, params, rf);
|
|
8658
|
+
if (strike === null || seen.has(strike.tick)) continue;
|
|
8659
|
+
seen.add(strike.tick);
|
|
8660
|
+
out.push(strike);
|
|
8661
|
+
}
|
|
8662
|
+
return out;
|
|
8663
|
+
}
|
|
8664
|
+
|
|
9957
8665
|
//#endregion
|
|
9958
8666
|
//#region src/panoptic/v2/bot/index.ts
|
|
9959
8667
|
/**
|
|
@@ -10183,4 +8891,4 @@ function isGasError(error) {
|
|
|
10183
8891
|
}
|
|
10184
8892
|
|
|
10185
8893
|
//#endregion
|
|
10186
|
-
export { DEFAULT_MIN_SWAP_RATIO_BPS, DEFAULT_RECONNECT_CONFIG, REQUIRED_BASE_ERROR_SENTINEL, addPendingPosition, addTrackedChunks, apportion, assertCanBurn, assertCanForceExercise, assertCanLiquidate, assertCanMint, assertFresh, assertHealthy, assertTradeable, buildCreditWrappedDispatch, buildSfpmSwapCalldata, buildSfpmSwapPlan, buildTemporaryLoanRecoveryDispatch, buildTokenShortfallRecoveryDispatch, calculateAccountGreeksPure, calculateSpreadWad, checkCollateralAcrossTicks, classifyStrategyGroups, cleanupStalePendingPositions, clearPendingPositions, clearTrackedChunks, clearTradeHistory, collateralRuleKindFor, computeV4PoolId, confirmPendingPosition, convertToAssets, convertToShares, createEventPoller, createEventSubscription, createFlowNeutralTokenId,
|
|
8894
|
+
export { DEFAULT_MIN_SWAP_RATIO_BPS, DEFAULT_RECONNECT_CONFIG, REQUIRED_BASE_ERROR_SENTINEL, STRIKE_LADDER_TARGET_STRIKES, addPendingPosition, addTrackedChunks, apportion, assertCanBurn, assertCanForceExercise, assertCanLiquidate, assertCanMint, assertFresh, assertHealthy, assertTradeable, buildCreditWrappedDispatch, buildSfpmSwapCalldata, buildSfpmSwapPlan, buildTemporaryLoanRecoveryDispatch, buildTokenShortfallRecoveryDispatch, calculateAccountGreeksPure, calculateSpreadWad, checkCollateralAcrossTicks, classifyStrategyGroups, classifyStrike, cleanupStalePendingPositions, clearPendingPositions, clearTrackedChunks, clearTradeHistory, collateralRuleKindFor, computeV4PoolId, confirmPendingPosition, convertToAssets, convertToShares, createEventPoller, createEventSubscription, createFlowNeutralTokenId, decodePanopticTokenURI, encodePoolKeyBytes, encodeV3PoolKeyBytes, ensureSfpmV3PoolInitialized, estimateBlockNumbers, estimateCollateralBreakdown, estimateCollateralRequired, failPendingPosition, feesFromFeeGrowthDelta, fetchSfpmV3PoolId, getAccountBuyingPower, getAccountGreeks, getAccountHistory, getChunkLiquidities, getChunkSpreads, getClosedPositions, getCollateralSharePrices, getCollateralTotalAssetsBatch, getDeltaHedgeParams, getEnforcedTickLimits, getFactoryConstructMetadata, getFactoryOwnerOf, getFactoryTokenURI, getGuardianUnlockState, getItmAmounts, getMarginBuffer, getMaxPositionSize, getMaxRedeem, getMaxWithdrawable, getNativeTokenPrice, getNotEnoughTokensError, getOpenPositionPreview, getPanopticPoolAddress, getPanopticPoolFromPoolId, getPendingPositions, getPoolLiquidities, getPortfolioValue, getPositionChunkData, getPositionEnrichmentData, getPriceHistory, getRealizedPnL, getRequiredCreditForITM, getSafeMode, getStreamiaHistory, getSyncStatus, getTrackedChunks, getTradeHistory, getUniswapFeeHistory, getUniswapV3LpPositionState, getUniswapV3PoolFromId, getUniswapV3PoolInfo, getUniswapV3PoolLiquidities, getUniswapV4LpPositionState, getUniswapV4PoolBasicState, getUniswapV4PoolInfo, getUniswapV4PoolKeyFromId, getUniswapV4PoolLiquidities, interpolateBlocks, isGasError, isNonceError, isRetryableRpcError, isolateGroupTokenId, ladderStrikeSequence, minePoolAddress, minePoolAddressLocalAsync, multicallRead, optimizeTokenIdRiskPartners, parseCollateralLog, parsePoolLog, previewDeposit, previewMint, previewRedeem, previewWithdraw, quoteOneTokenFlow, quoteSfpmSwap, quoteTemporaryLoanRecovery, quoteTokenShortfallRecovery, removeTrackedChunks, resolveBlockNumbers, resolveLadderStrike, resolvePanopticPoolFromPoolId, resolveUniswapV4PoolKey, saveClosedPosition, scanChunks, simulateBatchDispatch, simulateClosePosition, simulateDeployNewPool, simulateDeposit, simulateDispatch, simulateForceExercise, simulateLiquidate, simulateSFPMBurn, simulateSFPMMint, simulateSwapExactIn, simulateSwapExactOut, simulateWithdraw, slippageBpsToTickDistance, watchEvents };
|