@panoptic-eng/sdk 1.0.47 → 1.0.49

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.
@@ -1,7 +1,7 @@
1
- import { StateViewAbi, formatTokenAmount, getAccountCollateral, panopticQueryAbi$1 as panopticQueryAbi, parseTokenAmount, readBlockAndAggregate, requireReturnData } from "./irm-CvLqxTAo.js";
2
- import { AccountInsolventError, ChunkLimitError, DEFAULT_VEGOID, InvalidHistoryRangeError, LEG_LIMITS, NotEnoughTokensError, PanopticError, PanopticValidationError, STANDARD_TICK_WIDTHS, SafeModeError, StaleDataError, StorageDataNotFoundError, TOKEN_ID_BITS, UnhealthyPoolError, WrongUniswapPoolError, collateralTrackerV2Abi, decodeLeftRightSigned, decodeLeftRightUnsigned, decodePosition, decodePositionBalance, decodeTickSpacing, getBlockMeta, getClosedPositionsKey, getPendingPositionsKey, getPool, getPoolMetaKey, getPositionMetaKey, getPositionsKey, getTrackedChunksKey, getTrackedPositionIds, jsonSerializer, mintableAfterBuffer, panopticFactoryV3Abi, panopticFactoryV4Abi, panopticGuardianAbi, panopticPoolV2Abi, panopticQueryAbi as panopticQueryAbi$1, parsePanopticError, riskEngineAbi, semiFungiblePositionManagerV3Abi, semiFungiblePositionManagerV4Abi, stateViewAbi, uniswapV3PoolAbi } from "./getTrackedPositionIds-CwBHzOO2.js";
3
- import { MAX_TICK, MAX_TRACKED_CHUNKS, MIN_TICK, NO_LOWER_LIQUIDATION_TICK, NO_UPPER_LIQUIDATION_TICK, WAD, calculatePositionDelta, calculatePositionGamma, calculatePositionValue, priceToTick, roundToTickSpacing, sqrtPriceX96ToTick, tickLimits, tickToPrice, tickToPriceDecimalScaled, tickToSqrtPriceX96 } from "./greeks-9aTE-DwO.js";
4
- import { addLegToTokenId, buildBatchDispatchArgs, buildSettleSequenceCalls, buildUniqueCredit, buildUniqueLoan, countLegs, createTokenIdBuilder, decodeAllLegs, decodeTickSpacing$1, decodeTokenId, getCurrentPositionSizes, getPoolTokensForSimulation, loadCheckpoint, orderListForSettle, resolveTokenIndex, simulateOpenPosition, simulateWithTokenFlow } from "./writes-BZW7UA4N.js";
1
+ import { StateViewAbi, getAccountCollateral, panopticQueryAbi$1 as panopticQueryAbi, readBlockAndAggregate, requireReturnData } from "./irm-mySi_YMF.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 as panopticQueryAbi$1, parsePanopticError, riskEngineAbi, semiFungiblePositionManagerV3Abi, semiFungiblePositionManagerV4Abi, stateViewAbi, uniswapV3PoolAbi } from "./rates-CDLIUGDz.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-2reV_omF.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/tokenId/deriveUniqueTokenId.ts
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
- * Floor integer square root for bigints (Newton's method). Mirrors the isqrt
214
- * used in reads/collateralEstimate.ts (buildNeutralLeg).
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 isqrt$1(value) {
217
- if (value < 0n) throw new PanopticError("isqrt of negative number");
218
- if (value < 2n) return value;
219
- let x = value;
220
- let y = x + 1n >> 1n;
221
- while (y < x) {
222
- x = y;
223
- y = x + value / x >> 1n;
224
- }
225
- return x;
226
- }
227
- const DEFAULT_TICK_SPACING = 200n;
228
- /**
229
- * Target notional (in wei of the tokenType-side asset) for the appended
230
- * tiny credit leg. Chosen at 10 wei — small enough to be economically
231
- * meaningless against any real position size, large enough to safely
232
- * clear rounding on the SFPM's width-2 internal chunk math without any
233
- * risk of underflowing to zero (which would revert ChunkHasZeroLiquidity).
234
- */
235
- const TINY_CREDIT_TARGET_NOTIONAL_WEI = 10n;
236
- /**
237
- * Pick a signedStrike for the appended width=0 credit leg such that:
238
- * 1. positionSize · 1.0001^signedStrike ≈ TINY_CREDIT_TARGET_NOTIONAL_WEI
239
- * (~10 wei — economically meaningless but comfortably above any
240
- * SFPM width-2 chunk rounding);
241
- * 2. the leg's tick range (SFPM internally treats width=0 as width=2, so
242
- * the range is `strike ± tickSpacing`) stays strictly inside
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
- newTokenId: out,
329
- newPositionSize,
330
- effectivePositionSize: newPositionSize * N,
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/formatters/percentage.ts
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
- * Format a ratio as a percentage string.
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
- * @example
470
- * ```typescript
471
- * formatRatioPercent(1n, 4n, 1n) // "25.0%"
472
- * formatRatioPercent(3n, 4n, 2n) // "75.00%"
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 formatRatioPercent(numerator, denominator, precision) {
476
- const scaledNumerator = numerator * 100n;
477
- return `${formatRatio$2(scaledNumerator, denominator, precision)}%`;
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
- * Create pool-bound formatters that capture token decimals.
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
- * // Parse user input
508
- * const rawAmount0 = fmt.parseAmount0("1.5")
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 createPoolFormatters(config) {
513
- const { decimals0, decimals1 } = config;
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
- decimals0,
516
- decimals1,
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/formatters/tokenList.ts
546
- const TEN$1 = 10n;
547
- function formatRatio$1(numerator, denominator, precision) {
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
- * Generate a token list ID for external token list integration.
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
- * getTokenListId(11155111n, '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14')
572
- * // "11155111:0xfff9976782d46cc05630d1f6ebab18b2324d6b14"
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 getTokenListId(chainId, address) {
576
- return `${chainId}:${address.toLowerCase()}`;
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
- * Parse a token list ID back to chain ID and address.
358
+ * Build the swap plan.
580
359
  *
581
- * @param tokenListId - The token list ID string
582
- * @returns Object with chainId and address
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
- * @example
585
- * ```typescript
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 parseTokenListId(tokenListId) {
591
- const [chainIdStr, address] = tokenListId.split(":");
592
- if (!chainIdStr || !address) throw new Error(`Invalid token list ID: ${tokenListId}`);
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
- chainId: BigInt(chainIdStr),
595
- address
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
- * Generate a pool ID string for display purposes.
600
- *
601
- * @param token0Symbol - Symbol of token0
602
- * @param token1Symbol - Symbol of token1
603
- * @param feeBps - Fee in basis points (e.g., 500n for 0.05%)
604
- * @returns Pool ID string
605
- *
606
- * @example
607
- * ```typescript
608
- * getPoolDisplayId('WETH', 'USDC', 500n)
609
- * // "WETH/USDC 0.05%"
610
- *
611
- * getPoolDisplayId('WBTC', 'ETH', 3000n)
612
- * // "WBTC/ETH 0.30%"
613
- * ```
614
- */
615
- function getPoolDisplayId(token0Symbol, token1Symbol, feeBps) {
616
- return `${token0Symbol}/${token1Symbol} ${formatFeeTier(feeBps)}`;
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
- * Format a fee tier for display.
620
- *
621
- * @param feeBps - Fee in basis points (e.g., 500n for 0.05%)
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 formatFeeTier(feeBps) {
632
- const precision = feeBps < 10000n ? 2n : 1n;
633
- const feeStr = formatRatio$1(feeBps, 10000n, precision);
634
- return `${feeStr}%`;
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/formatters/display.ts
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
- * Truncate an address for display.
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
- * truncateAddress('0x1234567890abcdef1234567890abcdef12345678', 6)
662
- * // "0x123456...345678"
663
- * ```
482
+ * Calls `SemiFungiblePositionManagerV3.getUniswapV3PoolFromId(poolId)`.
664
483
  */
665
- function truncateAddress(address, chars = 4) {
666
- const charsBig = BigInt(chars);
667
- const minLength = charsBig * 2n + 4n;
668
- if (BigInt(address.length) <= minLength) return address;
669
- return `${address.slice(0, Number(charsBig + 2n))}...${address.slice(-Number(charsBig))}`;
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
- * Format a Unix timestamp as an ISO date string (YYYY-MM-DD).
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
- * @example
678
- * ```typescript
679
- * formatTimestamp(1700000000n) // "2023-11-14"
680
- * ```
496
+ * Calls `SemiFungiblePositionManagerV4.getUniswapV4PoolKeyFromId(poolId)`.
681
497
  */
682
- function formatTimestamp(timestamp) {
683
- const ms = timestamp * 1000n;
684
- return new Date(Number(ms)).toISOString().split("T")[0];
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
- * Format a Unix timestamp as an ISO datetime string.
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
- * @example
693
- * ```typescript
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 formatDatetime(timestamp) {
698
- const ms = timestamp * 1000n;
699
- return new Date(Number(ms)).toISOString();
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
- * Format a Unix timestamp as a locale-aware date string.
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
- * formatTimestampLocale(1700000000n, 'de-DE')
715
- * // "14.11.2023" (German locale)
716
- * ```
536
+ * Uses multicall for efficiency. Returns one result per input chunk,
537
+ * along with block metadata for freshness tracking.
717
538
  */
718
- function formatTimestampLocale(timestamp, locale, options) {
719
- const ms = timestamp * 1000n;
720
- return new Date(Number(ms)).toLocaleDateString(locale, options);
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
- * Format a duration in milliseconds as a human-readable string.
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 formatDuration(ms) {
737
- const isNegative = ms < 0n;
738
- const absMs = isNegative ? -ms : ms;
739
- const sign = isNegative ? "-" : "";
740
- if (absMs < 1000n) return `${sign}${absMs}ms`;
741
- if (absMs < 60000n) {
742
- const tenthsTotal = (absMs + 50n) / 100n;
743
- const seconds = tenthsTotal / 10n;
744
- const tenths = tenthsTotal % 10n;
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
- * Format a duration in seconds as a human-readable string.
758
- *
759
- * @param seconds - Duration in seconds
760
- * @returns Formatted duration string
604
+ * Get the owner of a PanopticFactory NFT.
761
605
  */
762
- function formatDurationSeconds(seconds) {
763
- return formatDuration(seconds * 1000n);
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
- * Format a block number for display.
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 formatBlockNumber(blockNumber) {
777
- return blockNumber.toLocaleString();
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
- * Format a gas amount for display.
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 formatGas(gas) {
786
- return gas.toLocaleString();
787
- }
788
- /**
789
- * Format a transaction hash for display (truncated).
790
- *
791
- * @param hash - Full transaction hash
792
- * @param chars - Characters to show on each side (default: 6)
793
- * @returns Truncated hash like "0x123456...abcdef"
794
- *
795
- * @example
796
- * ```typescript
797
- * formatTxHash('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef')
798
- * // "0x123456...abcdef"
799
- * ```
800
- */
801
- function formatTxHash(hash, chars = 6) {
802
- const charsBig = BigInt(chars);
803
- const minLength = charsBig * 2n + 4n;
804
- if (BigInt(hash.length) <= minLength) return hash;
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
- * Format a TokenId as a hex string.
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 formatTokenIdHex(tokenId) {
817
- return formatHex(tokenId);
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
- * Format a PoolId as a hex string.
702
+ * Simulate a pool deployment to get the predicted pool address.
821
703
  *
822
- * @param poolId - PoolId value
823
- * @returns Hex string representation
704
+ * Uses `simulateContract` on `deployNewPool` — the return value is the new pool address
705
+ * without actually executing the transaction.
824
706
  */
825
- function formatPoolIdHex(poolId) {
826
- return formatHex(poolId);
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
- * Format a TokenId as a shortened hex string.
745
+ * Resolve an SFPM poolId to its PanopticPool address.
830
746
  *
831
- * @param tokenId - TokenId value
832
- * @param chars - Characters to show on each side (default: 4)
833
- * @returns Truncated hex string like "0x1234...abcd"
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 formatTokenIdShort(tokenId, chars = 4) {
836
- const hex = formatHex(tokenId);
837
- const charsBig = BigInt(chars);
838
- const minLength = charsBig * 2n + 4n;
839
- if (BigInt(hex.length) <= minLength) return hex;
840
- return `${hex.slice(0, Number(charsBig + 2n))}...${hex.slice(-Number(charsBig))}`;
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
- * Format a large number with K/M/B suffixes.
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
- * @example
850
- * ```typescript
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
- * @param wei - Amount in wei
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
- * @param wei - Amount in wei
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 formatGwei(wei, precision) {
884
- return `${formatTokenAmount(wei, 9n, precision)} gwei`;
885
- }
886
-
887
- //#endregion
888
- //#region src/panoptic/v2/tokenId/generateOverlapping.ts
889
- const MAX_UINT64$2 = (1n << 64n) - 1n;
890
- const MAX_UINT128$3 = (1n << 128n) - 1n;
891
- const Q192$2 = 1n << 192n;
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
- function formatGridPrice(multiple, spacing) {
926
- const scaledPrice = multiple * spacing.units;
927
- if (spacing.scale === 0n) return scaledPrice.toString();
928
- const scale = Number(spacing.scale);
929
- const digits = scaledPrice.toString().padStart(scale + 1, "0");
930
- const decimalIndex = digits.length - scale;
931
- return `${digits.slice(0, decimalIndex)}.${digits.slice(decimalIndex)}`;
932
- }
933
- function quotePriceAtTick(tick, asset, assetDecimals, quoteDecimals) {
934
- const orientedTick = asset === 0n ? tick : -tick;
935
- const sqrtPriceX96 = tickToSqrtPriceX96(orientedTick);
936
- let numerator = sqrtPriceX96 * sqrtPriceX96;
937
- let denominator = Q192$2;
938
- const decimalDifference = assetDecimals - quoteDecimals;
939
- if (decimalDifference > 0n) numerator *= pow10(decimalDifference);
940
- else if (decimalDifference < 0n) denominator *= pow10(-decimalDifference);
941
- return {
942
- numerator,
943
- denominator
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
- function isPriceGridStrike(strike, params, spacing, tickSpacing) {
947
- const price = quotePriceAtTick(strike, params.asset, params.assetDecimals, params.quoteDecimals);
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/tokenId/split.ts
1052
- const POOL_ID_MASK$2 = (1n << TOKEN_ID_BITS.POOL_ID_SIZE) - 1n;
1053
- const MAX_UINT128$2 = (1n << 128n) - 1n;
1054
- const MAX_UINT256$1 = (1n << 256n) - 1n;
1055
- function invalid(message) {
1056
- throw new PanopticValidationError(`splitTokenIdByTimescale: ${message}`);
1057
- }
1058
- function validateInput(tokenId, positionSize) {
1059
- if (tokenId < 0n || tokenId > MAX_UINT256$1) invalid("tokenId must fit in uint256");
1060
- if (positionSize <= 0n || positionSize > MAX_UINT128$2) invalid("positionSize must be between 1 and uint128.max");
1061
- const legs = decodeAllLegs(tokenId);
1062
- if (legs.length === 0) invalid("tokenId must contain at least one active leg");
1063
- for (const [index, leg] of legs.entries()) if (leg.index !== BigInt(index)) invalid("active legs must be contiguous from index 0");
1064
- const firstUnusedBit = TOKEN_ID_BITS.POOL_ID_SIZE + BigInt(legs.length) * TOKEN_ID_BITS.LEG_SIZE;
1065
- if (tokenId >> firstUnusedBit !== 0n) invalid("inactive leg slots must be empty");
1066
- const byIndex = new Map(legs.map((leg) => [leg.index, leg]));
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 legs;
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
- /** Preserve mutual pairs, then pair self-partnered economic straddles in source order. */
1081
- function groupOptionLegs(optionLegs) {
1082
- const byIndex = new Map(optionLegs.map((leg) => [leg.index, leg]));
1083
- const consumed = new Set();
1084
- const groups = [];
1085
- for (const leg of optionLegs) {
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
- function remapRiskPartner(leg, indexBySource) {
1107
- const riskPartner = indexBySource.get(leg.riskPartner);
1108
- if (riskPartner === void 0) invalid(`risk partner for leg ${leg.index} is missing from its output group`);
1109
- return riskPartner;
1110
- }
1111
- function buildTokenId(poolId, legs, transform) {
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
- * Get the unsettled short premium an account would forfeit by closing now.
972
+ * Construct the bytes32 CREATE3 salt for PanopticFactoryV4.
2245
973
  *
2246
- * Calls `getFullPositionsData` twice in one multicall — once with
2247
- * `includePendingPremium = true` (everything owed to the short legs) and once
2248
- * with `false` (only what is available to collect). The difference is the
2249
- * premium still owed by buyers that has not been settled; closing before it
2250
- * settles forfeits it. Settling buyers first (see `settlePremiumFrom`) moves
2251
- * that premium into the available bucket.
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 - The parameters
2254
- * @returns Owed, available, and forfeitable premium with block metadata
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 getForfeitablePremium(params) {
2257
- const { client, poolAddress, account, tokenIds, blockNumber } = params;
2258
- const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
2259
- const contracts = [true, false].map((includePendingPremium) => ({
2260
- address: poolAddress,
2261
- abi: panopticPoolV2Abi,
2262
- functionName: "getFullPositionsData",
2263
- args: [
2264
- account,
2265
- includePendingPremium,
2266
- tokenIds
2267
- ]
2268
- }));
2269
- const [multicallResults, _meta] = await Promise.all([client.multicall({
2270
- contracts,
2271
- blockNumber: targetBlockNumber,
2272
- allowFailure: false
2273
- }), getBlockMeta({
2274
- client,
2275
- blockNumber: targetBlockNumber
2276
- })]);
2277
- const owed = decodeLeftRightUnsigned(multicallResults[0][0]);
2278
- const available = decodeLeftRightUnsigned(multicallResults[1][0]);
2279
- const forfeit0 = owed.right > available.right ? owed.right - available.right : 0n;
2280
- const forfeit1 = owed.left > available.left ? owed.left - available.left : 0n;
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
- owed0: owed.right,
2283
- owed1: owed.left,
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
- success: false,
7165
- diagnostics,
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
- ...sim,
7184
- diagnostics: []
5753
+ positions,
5754
+ _meta
7185
5755
  };
7186
5756
  }
7187
5757
 
7188
5758
  //#endregion
7189
- //#region src/panoptic/v2/simulations/simulateClosePosition.ts
5759
+ //#region src/panoptic/v2/sync/pendingPositions.ts
7190
5760
  /**
7191
- * Simulate closing a position.
5761
+ * Add a pending position for optimistic updates.
7192
5762
  *
7193
- * Uses PanopticPool.multicall with getAssetsOf-dispatch-getAssetsOf pattern
7194
- * to measure exact collateral asset movements from the burn.
5763
+ * Called when openPosition() submits a transaction.
7195
5764
  *
7196
- * @param params - Simulation parameters
7197
- * @returns Simulation result with close data or error
5765
+ * @param params - Parameters
7198
5766
  */
7199
- async function simulateClosePosition(params) {
7200
- const { client, poolAddress, account, positionIdList, tokenId, tickLimitLow, tickLimitHigh, spreadLimit = 0n, swapAtMint = false, usePremiaAsCollateral = false, builderCode = 0n, blockNumber } = params;
7201
- const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
7202
- const metaPromise = getBlockMeta({
7203
- client,
7204
- blockNumber: targetBlockNumber
7205
- });
7206
- const tickLimits$1 = swapAtMint ? [
7207
- Number(tickLimitHigh),
7208
- Number(tickLimitLow),
7209
- Number(spreadLimit)
7210
- ] : [
7211
- Number(tickLimitLow),
7212
- Number(tickLimitHigh),
7213
- Number(spreadLimit)
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 finalPositionIdList = positionIdList.filter((id) => id !== tokenId);
7217
- const callData = encodeFunctionData({
7218
- abi: panopticPoolV2Abi,
7219
- functionName: "dispatch",
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
- * Simulate a force exercise operation.
5800
+ * Mark a pending position as confirmed.
7270
5801
  *
7271
- * @param params - Simulation parameters
7272
- * @returns Simulation result with exercise data or error
5802
+ * Called when syncPositions() finds the OptionMinted event.
5803
+ *
5804
+ * @param params - Parameters
7273
5805
  */
7274
- async function simulateForceExercise(params) {
7275
- const { client, poolAddress, account, user, positionIdListFrom, positionIdListTo, positionIdListToFinal, blockNumber } = params;
7276
- const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
7277
- const metaPromise = getBlockMeta({
7278
- client,
7279
- blockNumber: targetBlockNumber
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
- const callData = encodeFunctionData({
7293
- abi: panopticPoolV2Abi,
7294
- functionName: "dispatchFrom",
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/simulateLiquidate.ts
5883
+ //#region src/panoptic/v2/simulations/simulateBatchDispatch.ts
7374
5884
  /**
7375
- * Simulate a liquidation operation.
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 simulateLiquidate(params) {
7381
- const { client, poolAddress, account, liquidatee, positionIdListFrom, positionIdListTo, positionIdListToFinal, blockNumber } = params;
7382
- const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
7383
- const metaPromise = getBlockMeta({
7384
- client,
7385
- blockNumber: targetBlockNumber
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
- const emptyTokenFlow = {
7388
- delta0: 0n,
7389
- delta1: 0n,
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
- error: error instanceof PanopticError ? error : new PanopticError(errorMessage, error instanceof Error ? error : void 0),
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/simulateSettle.ts
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 premium settlement.
5930
+ * Simulate closing a position.
7508
5931
  *
7509
- * When `tokenId` is provided, the simulation also computes forfeit amounts
7510
- * by chaining the dispatch with `getFullPositionsData` reads
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 settlement data or error
5936
+ * @returns Simulation result with close data or error
7515
5937
  */
7516
- async function simulateSettle(params) {
7517
- const { client, poolAddress, account, positionIdList, finalPositionIdList, positionSizes: providedSizes, tokenId, blockNumber } = params;
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
- if (providedSizes && providedSizes.length !== positionIdList.length) throw new PanopticError("simulateSettle: positionSizes length must match positionIdList");
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
- positionIdList,
7542
- finalPositionIdList ?? positionIdList,
7543
- positionSizes.map((s) => BigInt(s)),
7544
- tickAndSpreadLimits.map((t) => [
7545
- Number(t[0]),
7546
- Number(t[1]),
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
- premiaReceived0: tokenFlow.delta0,
7575
- premiaReceived1: tokenFlow.delta1,
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
- forfeitAmounts
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/simulateSettlePremiumFrom.ts
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 settling another account's accumulated long premium via `dispatchFrom`
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 settled premium data or error
6011
+ * @returns Simulation result with exercise data or error
7682
6012
  */
7683
- async function simulateSettlePremiumFrom(params) {
7684
- const { client, poolAddress, account, user, positionIdListFrom, positionIdList, tokenId, blockNumber } = params;
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
- orderedList,
7720
- orderedList,
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 soft = softFailure(errorMessage);
7754
- if (soft) {
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: soft,
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
- premium0,
7805
- premium1,
7806
- settled0,
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 soft = softFailure(errorMessage);
7821
- if (soft) return {
7822
- success: true,
7823
- data: soft,
7824
- gasEstimate: 0n,
7825
- tokenFlow: emptyTokenFlow,
7826
- _meta
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/simulateSettlePremiumBatch.ts
6112
+ //#region src/panoptic/v2/simulations/simulateLiquidate.ts
7838
6113
  /**
7839
- * Simulate settling each target buyer's owed long premium, all at one block.
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 Partitioned targets with per-target premium and totals
6117
+ * @returns Simulation result with liquidation data or error
7847
6118
  */
7848
- async function simulateSettlePremiumBatch(params) {
7849
- const { client, poolAddress, account, positionIdListFrom, targets, blockNumber } = params;
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 simulations = await Promise.all(targets.map((target) => simulateSettlePremiumFrom({
7856
- client,
7857
- poolAddress,
7858
- account,
7859
- user: target.user,
7860
- positionIdListFrom,
7861
- positionIdList: target.positionIdList,
7862
- tokenId: target.tokenId,
7863
- blockNumber: targetBlockNumber
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: "multicall",
7919
- args: [calls]
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) throw new PanopticError(flowResult.error || "Simulation failed");
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, createPoolFormatters, decodePanopticTokenURI, deriveUniqueTokenId, encodePoolKeyBytes, encodeV3PoolKeyBytes, ensureSfpmV3PoolInitialized, estimateBlockNumbers, estimateCollateralBreakdown, estimateCollateralRequired, failPendingPosition, feesFromFeeGrowthDelta, fetchSfpmV3PoolId, formatBlockNumber, formatBps, formatCompact, formatDatetime, formatDuration, formatDurationSeconds, formatFeeTier, formatGas, formatGwei, formatPoolIdHex, formatRatioPercent, formatTimestamp, formatTimestampLocale, formatTokenIdHex, formatTokenIdShort, formatTxHash, formatUtilization, formatWei, generateOverlappingTokenIds, getAccountBuyingPower, getAccountGreeks, getAccountHistory, getAccountPremia, getChunkLiquidities, getChunkSpreads, getClosedPositions, getCollateralSharePrices, getCollateralTotalAssetsBatch, getDeltaHedgeParams, getEnforcedTickLimits, getFactoryConstructMetadata, getFactoryOwnerOf, getFactoryTokenURI, getForfeitablePremium, getGuardianUnlockState, getItmAmounts, getMarginBuffer, getMaxPositionSize, getMaxRedeem, getMaxWithdrawable, getNativeTokenPrice, getNotEnoughTokensError, getOpenPositionPreview, getPanopticPoolAddress, getPanopticPoolFromPoolId, getPendingPositions, getPoolDisplayId, getPoolLiquidities, getPortfolioValue, getPositionChunkData, getPositionEnrichmentData, getPositionsWithPremia, getPriceHistory, getRealizedPnL, getRequiredCreditForITM, getSafeMode, getStreamiaHistory, getSyncStatus, getTokenListId, getTrackedChunks, getTradeHistory, getUniswapFeeHistory, getUniswapV3LpPositionState, getUniswapV3PoolFromId, getUniswapV3PoolInfo, getUniswapV3PoolLiquidities, getUniswapV4LpPositionState, getUniswapV4PoolBasicState, getUniswapV4PoolInfo, getUniswapV4PoolKeyFromId, getUniswapV4PoolLiquidities, interpolateBlocks, isGasError, isNonceError, isRetryableRpcError, isolateGroupTokenId, minePoolAddress, minePoolAddressLocalAsync, multicallRead, optimizeTokenIdRiskPartners, parseBps, parseCollateralLog, parsePoolLog, parseTokenListId, planDeriveStrategy, previewDeposit, previewMint, previewRedeem, previewWithdraw, quoteOneTokenFlow, quoteSfpmSwap, quoteTemporaryLoanRecovery, quoteTokenShortfallRecovery, removeTrackedChunks, resolveBlockNumbers, resolvePanopticPoolFromPoolId, resolveUniswapV4PoolKey, saveClosedPosition, scanChunks, simulateBatchDispatch, simulateClosePosition, simulateDeployNewPool, simulateDeposit, simulateDispatch, simulateForceExercise, simulateLiquidate, simulateSFPMBurn, simulateSFPMMint, simulateSettle, simulateSettlePremiumBatch, simulateSettlePremiumFrom, simulateSettleSequence, simulateSwapExactIn, simulateSwapExactOut, simulateWithdraw, slippageBpsToTickDistance, splitTokenIdByTimescale, truncateAddress, watchEvents };
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 };