@panoptic-eng/sdk 1.0.49 → 1.0.50

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.
Files changed (39) hide show
  1. package/dist/cow/index.js.map +1 -1
  2. package/dist/{cow-DMX9ebPk.js → cow-CSSKPknb.js} +1 -1
  3. package/dist/deployments.d.ts +12 -4
  4. package/dist/deployments.d.ts.map +1 -1
  5. package/dist/deployments.js +78 -10
  6. package/dist/deployments.js.map +1 -1
  7. package/dist/{greeks-9aTE-DwO.js → greeks-CNi1-cnp.js} +39 -16
  8. package/dist/index.d.ts +518 -76
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +701 -84
  11. package/dist/index.js.map +1 -1
  12. package/dist/{irm-mySi_YMF.js → irm-BJaJSUjF.js} +80 -12
  13. package/dist/panoptic/v2/greeks/index.d.ts +12 -1
  14. package/dist/panoptic/v2/greeks/index.d.ts.map +1 -1
  15. package/dist/panoptic/v2/greeks/index.js +39 -16
  16. package/dist/panoptic/v2/greeks/index.js.map +1 -1
  17. package/dist/panoptic/v2/index.d.ts +1336 -74
  18. package/dist/panoptic/v2/index.d.ts.map +1 -1
  19. package/dist/panoptic/v2/index.js +20322 -18660
  20. package/dist/panoptic/v2/index.js.map +1 -1
  21. package/dist/panoptic/v2/react-public.d.ts +1518 -240
  22. package/dist/panoptic/v2/react-public.d.ts.map +1 -1
  23. package/dist/panoptic/v2/react-public.js +11518 -9319
  24. package/dist/panoptic/v2/react-public.js.map +1 -1
  25. package/dist/{rates-CDLIUGDz.js → rates-COAJpKRd.js} +612 -73
  26. package/dist/{router-45ulPnWe.js → router-B-I1eACz.js} +5 -43
  27. package/dist/router-BSS7HKd2.js +837 -0
  28. package/dist/uniswap/index.d.ts +465 -215
  29. package/dist/uniswap/index.d.ts.map +1 -1
  30. package/dist/uniswap/index.js +1799 -1076
  31. package/dist/uniswap/index.js.map +1 -1
  32. package/dist/{v2-DshssxX4.js → v2-zF6b01PI.js} +2488 -1596
  33. package/dist/{writes-2reV_omF.js → writes-D1zN0v9x.js} +4 -3
  34. package/dist/zodiac/index.d.ts +51 -7
  35. package/dist/zodiac/index.d.ts.map +1 -1
  36. package/dist/zodiac/index.js +154 -2
  37. package/dist/zodiac/index.js.map +1 -1
  38. package/package.json +2 -1
  39. package/dist/router-B2jY9Nyb.js +0 -312
@@ -1,9 +1,11 @@
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";
1
+ import { StateViewAbi, getAccountCollateral, panopticQueryAbi, readBlockAndAggregate, requireReturnData } from "./irm-BJaJSUjF.js";
2
+ import { AccountInsolventError, ChunkLimitError, DEFAULT_VEGOID, InvalidHistoryRangeError, MulticallResultMissingError, NotEnoughTokensError, PanopticError, PanopticValidationError, SafeModeError, StaleDataError, StorageDataNotFoundError, UnhealthyPoolError, WrongUniswapPoolError, applyMintBufferPerToken, availableToBorrow, collateralTrackerV2Abi, decodeLeftRightSigned, decodeLeftRightUnsigned, decodePositionBalance, getBlockMeta, getClosedPositionsKey, getPendingPositionsKey, getPool, getPoolMetaKey, getPoolMetadata, getPositionMetaKey, getPositionsKey, getTrackedChunksKey, getTrackedPositionIds, isPanopticErrorType, jsonSerializer, mintableAfterBuffer, panopticFactoryV3Abi, panopticFactoryV4Abi, panopticGuardianAbi, panopticLiquidatorAbi, panopticPoolV2Abi, panopticQueryAbi$1, parsePanopticError, riskEngineAbi, semiFungiblePositionManagerV3Abi, semiFungiblePositionManagerV4Abi, stateViewAbi, uniswapV3PoolAbi } from "./rates-COAJpKRd.js";
3
+ import { MAX_TICK, MAX_TRACKED_CHUNKS, MIN_TICK, NO_LOWER_LIQUIDATION_TICK, NO_UPPER_LIQUIDATION_TICK, WAD, calculatePositionDelta, calculatePositionDeltaDebtOnly, calculatePositionGamma, calculatePositionValue, priceToTick, sqrtPriceX96ToTick, tickLimits, tickToPriceDecimalScaled, tickToSqrtPriceX96, toVaultFrameAtTick } from "./greeks-CNi1-cnp.js";
4
+ import { addLegToTokenId, buildBatchDispatchArgs, buildSettleSequenceCalls, buildUniqueCredit, buildUniqueLoan, countLegs, createTokenIdBuilder, decodeAllLegs, decodeTokenId, getPoolTokensForSimulation, loadCheckpoint, resolveTokenIndex, simulateOpenPosition, simulateWithTokenFlow } from "./writes-D1zN0v9x.js";
5
+ import { getLpPositionFunding, getMaxLpPositionSize } from "./router-BSS7HKd2.js";
5
6
  import { BaseError, ContractFunctionExecutionError, ContractFunctionRevertedError, ExecutionRevertedError, decodeFunctionResult, encodeAbiParameters, encodeFunctionData, getAddress, keccak256, toHex, zeroAddress } from "viem";
6
7
  import { multicall } from "viem/actions";
8
+ import Decimal from "decimal.js";
7
9
 
8
10
  //#region src/panoptic/v2/clients/blocksByTimestamp.ts
9
11
  /**
@@ -162,1480 +164,1880 @@ async function multicallRead(params) {
162
164
  }
163
165
 
164
166
  //#endregion
165
- //#region src/panoptic/v2/utils/factory.ts
167
+ //#region src/panoptic/v2/utils/priceConvert.ts
168
+ const Q128$3 = 1n << 128n;
166
169
  /**
167
- * Decode a base64-encoded Panoptic NFT token URI into metadata.
168
- *
169
- * @param tokenURI - The base64-encoded data URI string
170
- * @returns Parsed NFT metadata object, or undefined if input is invalid
170
+ * A non-positive price has no meaningful conversion, and `convert1to0` would
171
+ * divide by zero — surface that as a typed SDK error rather than a RangeError
172
+ * thrown from inside the arithmetic.
171
173
  */
172
- function decodePanopticTokenURI(tokenURI) {
173
- if (tokenURI === void 0 || tokenURI === "") return void 0;
174
- const parts = tokenURI.split("data:application/json;base64,");
175
- if (parts.length < 2 || parts[1] === "") return void 0;
176
- try {
177
- const decoded = atob(parts[1]);
178
- return JSON.parse(decoded);
179
- } catch {
180
- return void 0;
174
+ function assertPositivePrice(sqrtPriceX96) {
175
+ if (sqrtPriceX96 <= 0n) throw new PanopticError(`sqrtPriceX96 must be positive, got ${sqrtPriceX96}`);
176
+ }
177
+ /** Convert a token0 amount to its token1-equivalent at the given sqrtPriceX96. */
178
+ function convert0to1$2(amount, sqrtPriceX96) {
179
+ assertPositivePrice(sqrtPriceX96);
180
+ if (sqrtPriceX96 < Q128$3) return amount * sqrtPriceX96 * sqrtPriceX96 >> 192n;
181
+ const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
182
+ return amount * sp2Hi >> 128n;
183
+ }
184
+ /** Convert a token1 amount to its token0-equivalent at the given sqrtPriceX96. */
185
+ function convert1to0$2(amount, sqrtPriceX96) {
186
+ assertPositivePrice(sqrtPriceX96);
187
+ if (sqrtPriceX96 < Q128$3) {
188
+ const denom = sqrtPriceX96 * sqrtPriceX96;
189
+ return amount * (1n << 192n) / denom;
181
190
  }
191
+ const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
192
+ return amount * (1n << 128n) / sp2Hi;
182
193
  }
183
-
184
- //#endregion
185
- //#region src/panoptic/v2/utils/interpolateBlocks.ts
186
194
  /**
187
- * Generate evenly-spaced block numbers between start and end (pure math, no RPC).
188
- *
189
- * @param startBlock - First block in the range
190
- * @param endBlock - Last block in the range
191
- * @param points - Number of evenly-spaced data points to generate
192
- * @returns Array of interpolated block numbers
193
- *
194
- * @throws {InvalidHistoryRangeError} if startBlock > endBlock or points < 0
195
+ * Convert an amount denominated in `fromTokenIndex` into the other token's
196
+ * terms, so the two sides of a pool flow can be compared on one scale.
195
197
  */
196
- function interpolateBlocks(startBlock, endBlock, points) {
197
- if (!Number.isFinite(points) || !Number.isSafeInteger(points)) throw new InvalidHistoryRangeError(`points must be a finite safe integer, got ${points}`);
198
- if (points < 0) throw new InvalidHistoryRangeError(`points must be >= 0, got ${points}`);
199
- if (startBlock > endBlock) throw new InvalidHistoryRangeError(`startBlock (${startBlock}) must be <= endBlock (${endBlock})`);
200
- if (points === 0) return [];
201
- if (points === 1) return [endBlock];
202
- const range = endBlock - startBlock;
203
- return Array.from({ length: points }, (_, i) => startBlock + range * BigInt(i) / BigInt(points - 1));
198
+ function convertToTokenIndex(amount, fromTokenIndex, toTokenIndex, sqrtPriceX96) {
199
+ if (fromTokenIndex === toTokenIndex) return amount;
200
+ return fromTokenIndex === 0n ? convert0to1$2(amount, sqrtPriceX96) : convert1to0$2(amount, sqrtPriceX96);
204
201
  }
205
202
 
206
203
  //#endregion
207
- //#region src/panoptic/v2/sfpmSwap/calldata.ts
204
+ //#region src/panoptic/v2/reads/margin.ts
205
+ const FP96$1 = 1n << 96n;
206
+ const Q128$2 = 1n << 128n;
207
+ /** Cap for a usage ratio with no collateral behind it. */
208
+ const MAX_USAGE_BPS = 1000000n;
209
+ const bigintMax = (a, b) => a > b ? a : b;
210
+ const bigintMin = (a, b) => a < b ? a : b;
208
211
  /**
209
- * Encode the `multicall([mint, burn])` for a swap plan.
212
+ * Convert a token0 amount to its token1-equivalent at the given sqrtPriceX96.
210
213
  *
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.
214
+ * Matches the on-chain `PanopticMath.convert0to1` truncation, with an
215
+ * overflow-safe branch when `sqrtPriceX96^2` would not fit in uint256.
213
216
  */
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]]
241
- });
242
- return {
243
- multicallData,
244
- mintData,
245
- burnData
246
- };
217
+ function convert0to1$1(amount, sqrtPriceX96) {
218
+ if (sqrtPriceX96 < Q128$2) return amount * sqrtPriceX96 * sqrtPriceX96 >> 192n;
219
+ const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
220
+ return amount * sp2Hi >> 128n;
247
221
  }
248
-
249
- //#endregion
250
- //#region src/panoptic/v2/sfpmSwap/init.ts
251
222
  /**
252
- * Resolve the `uint64` SFPM poolId for a Uniswap v3 pool, initializing it if needed.
253
- *
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.
223
+ * Convert a token1 amount to its token0-equivalent at the given sqrtPriceX96.
258
224
  */
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);
225
+ function convert1to0$1(amount, sqrtPriceX96) {
226
+ if (sqrtPriceX96 < Q128$2) {
227
+ const denom = sqrtPriceX96 * sqrtPriceX96;
228
+ return amount * (1n << 192n) / denom;
229
+ }
230
+ const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
231
+ return amount * (1n << 128n) / sp2Hi;
274
232
  }
275
233
  /**
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.
234
+ * Get margin buffer and distance-to-liquidation for an account.
278
235
  *
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.
236
+ * Reads, all pinned to the same block:
237
+ * - `getCurrentTick` (sequencing dependency for sqrtPrice conversion)
238
+ * - `getFullPositionsData(account, true, tokenIds)` → collateralRequirements
239
+ * - `CollateralTracker.assetsOf(account)` on both trackers → gross collateral
240
+ * - `PanopticQuery.getLiquidationPrices(...)` → liquidation boundaries
241
+ *
242
+ * @param params - The parameters
243
+ * @returns Margin buffer with liquidation distance and block metadata
281
244
  */
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({
245
+ async function getMarginBuffer(params) {
246
+ const { client, poolAddress, account, tokenIds, queryAddress, blockNumber, mintBuffer } = params;
247
+ const targetBlockNumber = blockNumber ?? params._meta?.blockNumber ?? await client.getBlockNumber();
248
+ let collateralToken0;
249
+ let collateralToken1;
250
+ if (params.collateralAddresses) {
251
+ collateralToken0 = params.collateralAddresses.collateralToken0;
252
+ collateralToken1 = params.collateralAddresses.collateralToken1;
253
+ } else {
254
+ const addrs = await client.multicall({
255
+ contracts: [{
256
+ address: poolAddress,
257
+ abi: panopticPoolV2Abi,
258
+ functionName: "collateralToken0"
259
+ }, {
260
+ address: poolAddress,
261
+ abi: panopticPoolV2Abi,
262
+ functionName: "collateralToken1"
263
+ }],
264
+ blockNumber: targetBlockNumber,
265
+ allowFailure: false
266
+ });
267
+ collateralToken0 = addrs[0];
268
+ collateralToken1 = addrs[1];
269
+ }
270
+ const plan = prepareMarginBufferRead({
271
+ poolAddress,
272
+ account,
273
+ tokenIds,
274
+ queryAddress,
275
+ mintBuffer,
276
+ collateralAddresses: {
277
+ collateralToken0,
278
+ collateralToken1
279
+ }
280
+ });
281
+ const { _meta, results } = await readBlockAndAggregate({
286
282
  client,
287
- sfpmAddress,
288
- token0,
289
- token1,
290
- fee,
291
- vegoid
283
+ calls: plan.contracts.map((contract) => ({
284
+ target: contract.address,
285
+ callData: encodeFunctionData({
286
+ abi: contract.abi,
287
+ functionName: contract.functionName,
288
+ args: contract.args
289
+ })
290
+ })),
291
+ blockNumber: targetBlockNumber
292
292
  });
293
- const registered = await client.readContract({
294
- address: sfpmAddress,
295
- abi: semiFungiblePositionManagerV3Abi,
296
- functionName: "getUniswapV3PoolFromId",
297
- args: [poolId]
293
+ const decodedResults = results.map((result, index) => {
294
+ const contract = plan.contracts[index];
295
+ if (!contract) return {
296
+ status: "failure",
297
+ error: new MulticallResultMissingError("margin read contract", index)
298
+ };
299
+ if (!result.success) return {
300
+ status: "failure",
301
+ error: new PanopticError(`Margin read ${index} failed`)
302
+ };
303
+ return {
304
+ status: "success",
305
+ result: decodeFunctionResult({
306
+ abi: contract.abi,
307
+ functionName: contract.functionName,
308
+ data: result.returnData
309
+ })
310
+ };
298
311
  });
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}`));
327
- return {
328
- poolId,
329
- initialized
330
- };
312
+ return plan.decode(decodedResults, _meta);
331
313
  }
332
-
333
- //#endregion
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];
337
- /**
338
- * Convert a slippage tolerance in bps to a conservative Uniswap tick distance.
339
- *
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.
343
- */
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;
314
+ function requireMulticallResult(results, index, label) {
315
+ const result = results[index];
316
+ if (result === void 0) throw new MulticallResultMissingError(label, index);
317
+ if (result.status === "failure") throw result.error;
318
+ return result.result;
356
319
  }
357
- /**
358
- * Build the swap plan.
359
- *
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.
367
- *
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.
370
- */
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";
320
+ function requireBigIntResult(results, index, label) {
321
+ const result = requireMulticallResult(results, index, label);
322
+ if (typeof result !== "bigint" && typeof result !== "number") throw new PanopticError(`${label} returned an invalid value`);
323
+ return BigInt(result);
324
+ }
325
+ function prepareMarginBufferRead({ poolAddress, account, tokenIds, queryAddress, collateralAddresses, mintBuffer }) {
326
+ const hasPositions = tokenIds.length > 0;
327
+ const contracts = [
328
+ {
329
+ address: poolAddress,
330
+ abi: panopticPoolV2Abi,
331
+ functionName: "getCurrentTick"
332
+ },
333
+ {
334
+ address: collateralAddresses.collateralToken0,
335
+ abi: collateralTrackerV2Abi,
336
+ functionName: "assetsOf",
337
+ args: [account]
338
+ },
339
+ {
340
+ address: collateralAddresses.collateralToken1,
341
+ abi: collateralTrackerV2Abi,
342
+ functionName: "assetsOf",
343
+ args: [account]
344
+ }
345
+ ];
346
+ const positionDataIndex = hasPositions ? contracts.length : null;
347
+ if (positionDataIndex !== null) contracts.push({
348
+ address: poolAddress,
349
+ abi: panopticPoolV2Abi,
350
+ functionName: "getFullPositionsData",
351
+ args: [
352
+ account,
353
+ true,
354
+ tokenIds
355
+ ]
356
+ });
357
+ const liqPricesIndex = hasPositions ? contracts.length : null;
358
+ if (liqPricesIndex !== null) contracts.push({
359
+ address: queryAddress,
360
+ abi: panopticQueryAbi,
361
+ functionName: "getLiquidationPrices",
362
+ args: [
363
+ poolAddress,
364
+ account,
365
+ tokenIds
366
+ ]
367
+ });
368
+ const checkCollateralIndex = hasPositions ? contracts.length : null;
369
+ if (checkCollateralIndex !== null) contracts.push({
370
+ address: queryAddress,
371
+ abi: panopticQueryAbi,
372
+ functionName: "checkCollateral",
373
+ args: [
374
+ poolAddress,
375
+ account,
376
+ tokenIds
377
+ ]
378
+ });
388
379
  return {
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
380
+ contracts,
381
+ decode: (results, _meta) => decodeMarginBufferResults({
382
+ results,
383
+ _meta,
384
+ positionDataIndex,
385
+ liqPricesIndex,
386
+ checkCollateralIndex,
387
+ mintBuffer
388
+ })
389
+ };
390
+ }
391
+ function decodeMarginBufferResults({ results, _meta, positionDataIndex, liqPricesIndex, checkCollateralIndex, mintBuffer }) {
392
+ const currentTick = requireBigIntResult(results, 0, "PanopticPool.getCurrentTick");
393
+ const assets0 = requireBigIntResult(results, 1, "CollateralTracker.assetsOf token0");
394
+ const assets1 = requireBigIntResult(results, 2, "CollateralTracker.assetsOf token1");
395
+ const positionDataResult = positionDataIndex === null ? null : requireMulticallResult(results, positionDataIndex, "PanopticPool.getFullPositionsData");
396
+ const liqPricesResult = liqPricesIndex === null ? null : requireMulticallResult(results, liqPricesIndex, "PanopticQuery.getLiquidationPrices");
397
+ const checkCollateralResult = checkCollateralIndex === null ? null : requireMulticallResult(results, checkCollateralIndex, "PanopticQuery.checkCollateral");
398
+ let usageBps0 = null;
399
+ let usageBps1 = null;
400
+ let crossMarginUsageBps = null;
401
+ let mintableMarginBinding = null;
402
+ if (checkCollateralResult) {
403
+ const [balances0, requireds0, balances1, requireds1] = checkCollateralResult;
404
+ const ratio = (required, balance) => {
405
+ if (required === void 0 || balance === void 0) return null;
406
+ if (balance === 0n) return required > 0n ? MAX_USAGE_BPS : 0n;
407
+ return required * 10000n / balance;
408
+ };
409
+ usageBps0 = ratio(requireds0[0], balances0[0]);
410
+ usageBps1 = ratio(requireds1[0], balances1[0]);
411
+ if (usageBps0 !== null || usageBps1 !== null) crossMarginUsageBps = usageBps0 === null ? usageBps1 : usageBps1 === null ? usageBps0 : bigintMax(usageBps0, usageBps1);
412
+ const mintable = (balance, required) => balance === void 0 || required === void 0 ? null : mintableAfterBuffer(balance, required, mintBuffer);
413
+ const mintable0 = mintable(balances0[0], requireds0[0]);
414
+ const mintable1 = mintable(balances1[0], requireds1[0]);
415
+ if (mintable0 !== null || mintable1 !== null) mintableMarginBinding = mintable0 === null ? mintable1 : mintable1 === null ? mintable0 : bigintMin(mintable0, mintable1);
416
+ }
417
+ let required0Native = 0n;
418
+ let required1Native = 0n;
419
+ if (positionDataResult) {
420
+ const collateralRequirements = positionDataResult[3];
421
+ for (const packed of collateralRequirements) {
422
+ const decoded = decodeLeftRightUnsigned(packed);
423
+ required0Native += decoded.right;
424
+ required1Native += decoded.left;
425
+ }
426
+ }
427
+ const sqrtPriceX96 = tickToSqrtPriceX96(currentTick);
428
+ const denominatedInToken = sqrtPriceX96 < FP96$1 ? 0 : 1;
429
+ const currentMargin0 = assets0 + convert1to0$1(assets1, sqrtPriceX96);
430
+ const requiredMargin0 = required0Native + convert1to0$1(required1Native, sqrtPriceX96);
431
+ const currentMargin1 = assets1 + convert0to1$1(assets0, sqrtPriceX96);
432
+ const requiredMargin1 = required1Native + convert0to1$1(required0Native, sqrtPriceX96);
433
+ const buffer0 = currentMargin0 - requiredMargin0;
434
+ const buffer1 = currentMargin1 - requiredMargin1;
435
+ const bufferPercent0 = requiredMargin0 === 0n ? null : buffer0 * 10000n / requiredMargin0;
436
+ const bufferPercent1 = requiredMargin1 === 0n ? null : buffer1 * 10000n / requiredMargin1;
437
+ let lowerLiquidationTick = null;
438
+ let upperLiquidationTick = null;
439
+ let liquidationDistance = null;
440
+ if (liqPricesResult) {
441
+ const liqPriceDown = BigInt(liqPricesResult[0]);
442
+ const liqPriceUp = BigInt(liqPricesResult[1]);
443
+ lowerLiquidationTick = liqPriceDown === NO_LOWER_LIQUIDATION_TICK ? null : liqPriceDown;
444
+ upperLiquidationTick = liqPriceUp === NO_UPPER_LIQUIDATION_TICK ? null : liqPriceUp;
445
+ if (lowerLiquidationTick !== null && upperLiquidationTick !== null) {
446
+ const distLower = currentTick - lowerLiquidationTick;
447
+ const distUpper = upperLiquidationTick - currentTick;
448
+ liquidationDistance = distLower < distUpper ? distLower : distUpper;
449
+ } else if (lowerLiquidationTick !== null) liquidationDistance = currentTick - lowerLiquidationTick;
450
+ else if (upperLiquidationTick !== null) liquidationDistance = upperLiquidationTick - currentTick;
451
+ }
452
+ return {
453
+ buffer0,
454
+ buffer1,
455
+ bufferPercent0,
456
+ bufferPercent1,
457
+ currentMargin0,
458
+ currentMargin1,
459
+ requiredMargin0,
460
+ requiredMargin1,
461
+ denominatedInToken,
462
+ liquidationDistance,
463
+ lowerLiquidationTick,
464
+ upperLiquidationTick,
465
+ crossMarginUsageBps,
466
+ usageBps0,
467
+ usageBps1,
468
+ mintableMarginBinding,
469
+ currentTick,
470
+ _meta
398
471
  };
399
472
  }
400
473
 
401
474
  //#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;
475
+ //#region src/panoptic/v2/reads/lpFunding.ts
476
+ /** Recheck full LP backing against fresh pool and account state before signing. */
477
+ async function readLpFundingSnapshot(params) {
478
+ const { client, poolAddress, account, existingPositionIds, queryAddress, quoteTokenIndex } = params;
479
+ const metadata = await getPoolMetadata({
480
+ client,
481
+ poolAddress
482
+ });
483
+ if (metadata.isV4 && !params.stateViewAddress) throw new PanopticError("Missing V4 StateView for LP funding check");
484
+ const priceContract = metadata.isV4 && params.stateViewAddress ? {
485
+ address: params.stateViewAddress,
486
+ abi: stateViewAbi,
487
+ functionName: "getSlot0",
488
+ args: [metadata.underlyingPoolId]
489
+ } : {
490
+ address: metadata.underlyingPoolId,
491
+ abi: uniswapV3PoolAbi,
492
+ functionName: "slot0"
493
+ };
494
+ const marginRead = prepareMarginBufferRead({
495
+ poolAddress,
496
+ account,
497
+ tokenIds: existingPositionIds,
498
+ queryAddress,
499
+ collateralAddresses: {
500
+ collateralToken0: metadata.collateralToken0Address,
501
+ collateralToken1: metadata.collateralToken1Address
502
+ }
503
+ });
504
+ const { results, _meta } = await multicallRead({
505
+ client,
506
+ contracts: [priceContract, ...marginRead.contracts],
507
+ blockNumber: params.blockNumber
508
+ });
509
+ const priceResult = results[0];
510
+ if (!priceResult) throw new PanopticError("Missing pool price result for LP funding check");
511
+ if (priceResult.status === "failure") throw priceResult.error;
512
+ const slot0 = priceResult.result;
513
+ const margin = marginRead.decode(results.slice(1), _meta);
514
+ const valuationSqrtPriceX96 = tickToSqrtPriceX96(margin.currentTick);
515
+ const fundingParams = {
516
+ tokenId: params.tokenId,
517
+ positionSize: params.positionSize,
518
+ tickSpacing: metadata.tickSpacing,
519
+ sqrtPriceX96: slot0[0],
520
+ valuationSqrtPriceX96,
521
+ quoteTokenIndex
522
+ };
523
+ const funding = getLpPositionFunding(fundingParams);
524
+ const available = margin.mintableMarginBinding ?? (margin.denominatedInToken === 0 ? margin.currentMargin0 : margin.currentMargin1);
525
+ const availableInQuote = convertToTokenIndex(available, BigInt(margin.denominatedInToken), BigInt(quoteTokenIndex), valuationSqrtPriceX96);
409
526
  return {
410
- right: toInt128(u & UINT128 - 1n),
411
- left: toInt128(u >> 128n & UINT128 - 1n)
527
+ funding,
528
+ fundingParams,
529
+ availableInQuote,
530
+ blockNumber: _meta.blockNumber,
531
+ currentTick: margin.currentTick,
532
+ _meta
412
533
  };
413
534
  }
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);
535
+ /** Recheck the same funding policy used by executable LP sizing before signing. */
536
+ async function assertLpPositionFunded(params) {
537
+ const { funding, availableInQuote } = await readLpFundingSnapshot(params);
538
+ if (availableInQuote < funding.totalInQuote) throw new PanopticError("Insufficient collateral for the full LP principal and 5% funding buffer. Refresh the position and deposit collateral or reduce its size.");
539
+ return funding;
420
540
  }
541
+
542
+ //#endregion
543
+ //#region src/panoptic/v2/simulations/simulateDispatch.ts
421
544
  /**
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.
545
+ * Simulate a raw dispatch operation.
546
+ *
547
+ * Uses PanopticPool.multicall with getAssetsOf-dispatch-getAssetsOf pattern
548
+ * to measure exact collateral asset movements.
549
+ *
550
+ * @param params - Simulation parameters
551
+ * @returns Simulation result with dispatch data or error
425
552
  */
426
- async function quoteSfpmSwap(params) {
427
- const { client, plan, account, stateOverride, blockNumber } = params;
428
- const { mintData, burnData } = buildSfpmSwapCalldata(plan);
429
- const _meta = await getBlockMeta({
553
+ async function simulateDispatch(params) {
554
+ const { client, poolAddress, account, positionIdList, finalPositionIdList, positionSizes, tickAndSpreadLimits, usePremiaAsCollateral = false, builderCode = 0n, blockNumber, existingPositionIdList, measurePremia = false } = params;
555
+ const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
556
+ const metaPromise = getBlockMeta({
430
557
  client,
431
- blockNumber
558
+ blockNumber: targetBlockNumber
432
559
  });
433
560
  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]
561
+ const callData = encodeFunctionData({
562
+ abi: panopticPoolV2Abi,
563
+ functionName: "dispatch",
564
+ args: [
565
+ positionIdList,
566
+ finalPositionIdList,
567
+ positionSizes,
568
+ tickAndSpreadLimits.map((t) => [
569
+ Number(t[0]),
570
+ Number(t[1]),
571
+ Number(t[2])
572
+ ]),
573
+ usePremiaAsCollateral,
574
+ builderCode
575
+ ]
448
576
  });
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
577
+ const preFullPositionsCallData = existingPositionIdList !== void 0 ? encodeFunctionData({
578
+ abi: panopticPoolV2Abi,
579
+ functionName: "getFullPositionsData",
580
+ args: [
581
+ account,
582
+ false,
583
+ existingPositionIdList
584
+ ]
585
+ }) : void 0;
586
+ const prePremiaCallData = measurePremia && existingPositionIdList !== void 0 ? encodeFunctionData({
587
+ abi: panopticPoolV2Abi,
588
+ functionName: "getFullPositionsData",
589
+ args: [
590
+ account,
591
+ true,
592
+ existingPositionIdList
593
+ ]
594
+ }) : void 0;
595
+ const postFullPositionsCallData = encodeFunctionData({
596
+ abi: panopticPoolV2Abi,
597
+ functionName: "getFullPositionsData",
598
+ args: [
599
+ account,
600
+ false,
601
+ finalPositionIdList
602
+ ]
603
+ });
604
+ const postPremiaCallData = measurePremia ? encodeFunctionData({
605
+ abi: panopticPoolV2Abi,
606
+ functionName: "getFullPositionsData",
607
+ args: [
608
+ account,
609
+ true,
610
+ finalPositionIdList
611
+ ]
612
+ }) : void 0;
613
+ const flowResult = await simulateWithTokenFlow({
614
+ client,
615
+ poolAddress,
616
+ user: account,
617
+ callData: params.settleSequence?.targets.length ? encodeFunctionData({
618
+ abi: panopticPoolV2Abi,
619
+ functionName: "multicall",
620
+ args: [[...buildSettleSequenceCalls(params.settleSequence), callData]]
621
+ }) : callData,
622
+ blockNumber: targetBlockNumber,
623
+ preCallData: preFullPositionsCallData ? [preFullPositionsCallData, ...prePremiaCallData ? [prePremiaCallData] : []] : void 0,
624
+ postCallData: [postFullPositionsCallData, ...postPremiaCallData ? [postPremiaCallData] : []]
625
+ });
626
+ if (!flowResult.success || !flowResult.tokenFlow) throw flowResult.rawError ?? new PanopticError(flowResult.error || "Token flow simulation failed");
627
+ const tokenFlow = flowResult.tokenFlow;
628
+ const preSnapshot = existingPositionIdList ?? [];
629
+ const positionsCreated = finalPositionIdList.filter((id) => !preSnapshot.includes(id));
630
+ const positionsClosed = preSnapshot.filter((id) => !finalPositionIdList.includes(id));
631
+ const decodeFullPositions = (data$1) => {
632
+ if (!data$1) return null;
633
+ try {
634
+ const decoded = decodeFunctionResult({
635
+ abi: panopticPoolV2Abi,
636
+ functionName: "getFullPositionsData",
637
+ data: data$1
638
+ });
639
+ const shortPremium = decodeLeftRightUnsigned(decoded[0]);
640
+ const longPremium = decodeLeftRightUnsigned(decoded[1]);
641
+ const reqs = decoded[3];
642
+ let collateralRequirements0 = 0n;
643
+ let collateralRequirements1 = 0n;
644
+ for (const packed of reqs) {
645
+ const r = decodeLeftRightUnsigned(packed);
646
+ collateralRequirements0 += r.right;
647
+ collateralRequirements1 += r.left;
648
+ }
649
+ return {
650
+ collateralRequirements0,
651
+ collateralRequirements1,
652
+ netPremia0: shortPremium.right - longPremium.right,
653
+ netPremia1: shortPremium.left - longPremium.left
654
+ };
655
+ } catch {
656
+ return null;
657
+ }
658
+ };
659
+ const prePositions = decodeFullPositions(flowResult.preCallResults?.[0]);
660
+ const postPositions = decodeFullPositions(flowResult.postCallResults?.[0]);
661
+ const prePremia = decodeFullPositions(flowResult.preCallResults?.[1]);
662
+ const postPremia = decodeFullPositions(flowResult.postCallResults?.[1]);
663
+ const _meta = await metaPromise;
664
+ const data = {
665
+ netAmount0: tokenFlow.delta0,
666
+ netAmount1: tokenFlow.delta1,
667
+ premiaReceived0: prePremia !== null && postPremia !== null ? prePremia.netPremia0 - postPremia.netPremia0 : null,
668
+ premiaReceived1: prePremia !== null && postPremia !== null ? prePremia.netPremia1 - postPremia.netPremia1 : null,
669
+ positionsCreated,
670
+ positionsClosed,
671
+ postCollateral0: tokenFlow.balanceAfter0,
672
+ postCollateral1: tokenFlow.balanceAfter1,
673
+ preMarginExcess0: prePositions === null ? null : tokenFlow.balanceBefore0 - prePositions.collateralRequirements0,
674
+ preMarginExcess1: prePositions === null ? null : tokenFlow.balanceBefore1 - prePositions.collateralRequirements1,
675
+ postMarginExcess0: postPositions === null ? null : tokenFlow.balanceAfter0 - postPositions.collateralRequirements0,
676
+ postMarginExcess1: postPositions === null ? null : tokenFlow.balanceAfter1 - postPositions.collateralRequirements1
457
677
  };
458
678
  return {
459
679
  success: true,
460
- data: {
461
- amountIn: inSlot,
462
- amountOut: -outSlot,
463
- finalTick
464
- },
465
- gasEstimate: 0n,
680
+ data,
681
+ gasEstimate: flowResult.gasEstimate,
682
+ tokenFlow,
466
683
  _meta
467
684
  };
468
- } catch (err) {
685
+ } catch (error) {
686
+ const _meta = await metaPromise;
469
687
  return {
470
688
  success: false,
471
- error: toPanopticError(err),
689
+ error: error instanceof PanopticError ? error : new PanopticError(error instanceof Error ? error.message : "Simulation failed", error instanceof Error ? error : void 0),
472
690
  _meta
473
691
  };
474
692
  }
475
693
  }
476
694
 
477
695
  //#endregion
478
- //#region src/panoptic/v2/reads/sfpm.ts
696
+ //#region src/panoptic/v2/simulations/creditWrap.ts
479
697
  /**
480
- * Resolve an SFPM poolId to its corresponding Uniswap V3 pool address.
698
+ * Wrap a dispatch with a temporary credit leg that is opened and closed in the
699
+ * same transaction, netting to a swap.
481
700
  *
482
- * Calls `SemiFungiblePositionManagerV3.getUniswapV3PoolFromId(poolId)`.
701
+ * `swapAtMint` is not a calldata flag — it is the ORDER of the tick-limit pair:
702
+ * descending `[high, low]` turns the swap on, ascending `[low, high]` leaves it
703
+ * off. Exactly one of the two legs carries it, and which one is what makes the
704
+ * swap exact-in vs exact-out.
483
705
  */
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
- });
706
+ function buildCreditWrappedDispatch(params) {
707
+ const { dispatch, creditTokenId, creditPositionSize, direction, placement } = params;
708
+ const low = params.tickLimitLow <= params.tickLimitHigh ? params.tickLimitLow : params.tickLimitHigh;
709
+ const high = params.tickLimitLow <= params.tickLimitHigh ? params.tickLimitHigh : params.tickLimitLow;
710
+ const swapping = [
711
+ high,
712
+ low,
713
+ 0n
714
+ ];
715
+ const notSwapping = [
716
+ low,
717
+ high,
718
+ 0n
719
+ ];
720
+ const mintLimits = direction === "exact-out" ? swapping : notSwapping;
721
+ const burnLimits = direction === "exact-out" ? notSwapping : swapping;
722
+ const positionIdList = placement === "straddle" ? [
723
+ creditTokenId,
724
+ ...dispatch.positionIdList,
725
+ creditTokenId
726
+ ] : placement === "prepend" ? [
727
+ creditTokenId,
728
+ creditTokenId,
729
+ ...dispatch.positionIdList
730
+ ] : [
731
+ ...dispatch.positionIdList,
732
+ creditTokenId,
733
+ creditTokenId
734
+ ];
735
+ const positionSizes = placement === "straddle" ? [
736
+ creditPositionSize,
737
+ ...dispatch.positionSizes,
738
+ 0n
739
+ ] : placement === "prepend" ? [
740
+ creditPositionSize,
741
+ 0n,
742
+ ...dispatch.positionSizes
743
+ ] : [
744
+ ...dispatch.positionSizes,
745
+ creditPositionSize,
746
+ 0n
747
+ ];
748
+ const tickAndSpreadLimits = placement === "straddle" ? [
749
+ mintLimits,
750
+ ...dispatch.tickAndSpreadLimits,
751
+ burnLimits
752
+ ] : placement === "prepend" ? [
753
+ mintLimits,
754
+ burnLimits,
755
+ ...dispatch.tickAndSpreadLimits
756
+ ] : [
757
+ ...dispatch.tickAndSpreadLimits,
758
+ mintLimits,
759
+ burnLimits
760
+ ];
761
+ const wrapped = {
762
+ positionIdList,
763
+ finalPositionIdList: [...dispatch.finalPositionIdList],
764
+ positionSizes,
765
+ tickAndSpreadLimits,
766
+ usePremiaAsCollateral: dispatch.usePremiaAsCollateral,
767
+ builderCode: dispatch.builderCode
768
+ };
769
+ return wrapped;
492
770
  }
771
+
772
+ //#endregion
773
+ //#region src/panoptic/v2/simulations/tokenShortfallRecovery.ts
774
+ const BPS_DENOMINATOR$2 = 10000n;
775
+ const MAX_RECOVERY_QUOTE_ATTEMPTS = 8;
493
776
  /**
494
- * Resolve an SFPM poolId to its corresponding Uniswap V4 pool key.
777
+ * Wrap a dispatch with a temporary credit leg that sources the shortfall.
495
778
  *
496
- * Calls `SemiFungiblePositionManagerV4.getUniswapV4PoolKeyFromId(poolId)`.
779
+ * Exact-output construction: mint the credit with `swapAtMint=true` (paying a
780
+ * swapped amount of the token the account has), run the user's operations, then
781
+ * burn it with `swapAtMint=false` to receive exactly the missing token.
782
+ *
783
+ * A credit rather than a loan so the recovery is never capped by the shortfall
784
+ * token's utilization — the case that fails today on a >94% utilized tracker.
497
785
  */
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]
786
+ function buildTokenShortfallRecoveryDispatch(params) {
787
+ return buildCreditWrappedDispatch({
788
+ ...params,
789
+ direction: "exact-out",
790
+ placement: "straddle"
505
791
  });
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
- };
513
792
  }
514
- /**
515
- * Get the enforced tick limits for a pool from the SFPM.
516
- *
517
- * @param params - The parameters
518
- * @returns The min and max enforced ticks
519
- */
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]
793
+ function buildPrefixedExactInputRecoveryDispatch(params) {
794
+ return buildCreditWrappedDispatch({
795
+ ...params,
796
+ direction: "exact-in",
797
+ placement: "prepend"
527
798
  });
528
- return {
529
- minEnforcedTick: minTick,
530
- maxEnforcedTick: maxTick
531
- };
532
799
  }
533
800
  /**
534
- * Fetch liquidity breakdown for a batch of chunks via SFPM.getAccountLiquidity().
801
+ * Extract a fully-decoded `NotEnoughTokens` revert from an arbitrary error.
535
802
  *
536
- * Uses multicall for efficiency. Returns one result per input chunk,
537
- * along with block metadata for freshness tracking.
803
+ * Returns `null` when the error is something else, or when only the 4-byte
804
+ * selector could be matched (the parser's fallback path constructs the error
805
+ * with undefined args, which is not actionable).
538
806
  */
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
807
+ function getNotEnoughTokensError(error) {
808
+ const visited = new Set();
809
+ let current = error;
810
+ while (current !== void 0 && current !== null && !visited.has(current)) {
811
+ visited.add(current);
812
+ const parsed = current instanceof NotEnoughTokensError ? current : parsePanopticError(current)?.error;
813
+ const candidate = parsed instanceof NotEnoughTokensError ? parsed : null;
814
+ if (candidate !== null) {
815
+ const args = candidate;
816
+ if (args.tokenAddress !== void 0 && args.assetsRequested !== void 0 && args.assetBalance !== void 0) return candidate;
817
+ }
818
+ current = current instanceof Error && "cause" in current ? current.cause : void 0;
819
+ }
820
+ return null;
821
+ }
822
+ function maximumAmountIn(estimatedAmountIn, slippageBps) {
823
+ if (slippageBps < 0n) throw new PanopticError("slippageBps must be non-negative");
824
+ return (estimatedAmountIn * (BPS_DENOMINATOR$2 + slippageBps) + BPS_DENOMINATOR$2 - 1n) / BPS_DENOMINATOR$2;
825
+ }
826
+ function ceilDiv$2(numerator, denominator) {
827
+ return (numerator + denominator - 1n) / denominator;
828
+ }
829
+ function getInputAmount(tokenFlow, tokenInIndex) {
830
+ const delta = tokenInIndex === 0n ? tokenFlow.delta0 : tokenFlow.delta1;
831
+ return delta < 0n ? -delta : delta;
832
+ }
833
+ function getOutputAmount(tokenFlow, tokenOutIndex) {
834
+ const delta = tokenOutIndex === 0n ? tokenFlow.delta0 : tokenFlow.delta1;
835
+ return delta > 0n ? delta : 0n;
836
+ }
837
+ async function quoteTokenShortfallRecovery(params) {
838
+ const shortfallError = getNotEnoughTokensError(params.error);
839
+ if (shortfallError === null) return {
840
+ available: false,
841
+ reason: "not-token-shortfall"
546
842
  };
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
843
+ let amountOut = shortfallError.assetsRequested - shortfallError.assetBalance;
844
+ if (amountOut <= 0n) return {
845
+ available: false,
846
+ reason: "invalid-shortfall",
847
+ detail: `requested=${shortfallError.assetsRequested} <= balance=${shortfallError.assetBalance}`
848
+ };
849
+ if (params.slippageBps < 0n || params.slippageBps > BPS_DENOMINATOR$2) return {
850
+ available: false,
851
+ reason: "invalid-slippage",
852
+ detail: `slippageBps=${params.slippageBps} is outside [0, ${BPS_DENOMINATOR$2}]`
853
+ };
854
+ const targetBlockNumber = params.blockNumber ?? await params.client.getBlockNumber();
855
+ const pool = await getPool({
856
+ client: params.client,
857
+ poolAddress: params.poolAddress,
858
+ chainId: params.chainId,
859
+ blockNumber: targetBlockNumber
561
860
  });
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;
861
+ const defaultTickLimits = tickLimits(pool.currentTick, params.slippageBps);
862
+ const tickLimitLow = params.tickLimitLow ?? defaultTickLimits.low;
863
+ const tickLimitHigh = params.tickLimitHigh ?? defaultTickLimits.high;
864
+ if (tickLimitLow >= tickLimitHigh) return {
865
+ available: false,
866
+ reason: "invalid-tick-limits",
867
+ detail: `tickLimitLow=${tickLimitLow} >= tickLimitHigh=${tickLimitHigh}`
868
+ };
869
+ const token0 = pool.collateralTracker0.token;
870
+ const token1 = pool.collateralTracker1.token;
871
+ const shortfallTokenIndex = (error) => {
872
+ const address = error.tokenAddress.toLowerCase();
873
+ if (address === token0.toLowerCase() || address === pool.collateralTracker0.address.toLowerCase()) return 0n;
874
+ if (address === token1.toLowerCase() || address === pool.collateralTracker1.address.toLowerCase()) return 1n;
875
+ return null;
876
+ };
877
+ const tokenOutIndex = shortfallTokenIndex(shortfallError);
878
+ if (tokenOutIndex === null) return {
879
+ available: false,
880
+ reason: "unsupported-token",
881
+ detail: `${shortfallError.tokenAddress} is neither collateral token of ${params.poolAddress}`
882
+ };
883
+ const tokenInIndex = tokenOutIndex === 0n ? 1n : 0n;
884
+ const tokenOut = tokenOutIndex === 0n ? token0 : token1;
885
+ const tokenIn = tokenInIndex === 0n ? token0 : token1;
886
+ const collisionIds = Array.from(new Set([
887
+ ...params.existingPositionIds,
888
+ ...params.dispatch.positionIdList,
889
+ ...params.dispatch.finalPositionIdList
890
+ ]));
891
+ const quotePrefixedExactInput = async (initialRequiredOutput) => {
892
+ let requiredOutput = initialRequiredOutput;
893
+ const spotInput = convertToTokenIndex(requiredOutput, tokenOutIndex, tokenInIndex, pool.sqrtPriceX96);
894
+ let creditInput = maximumAmountIn(spotInput > 0n ? spotInput : 1n, params.slippageBps);
895
+ for (let attempt = 0; attempt < MAX_RECOVERY_QUOTE_ATTEMPTS; attempt++) {
896
+ const credit = buildUniqueCredit(pool.poolId, tokenInIndex, tokenInIndex, pool.currentTick, pool.tickSpacing, collisionIds, creditInput);
897
+ const wrapArgs = {
898
+ creditTokenId: credit.tokenId,
899
+ creditPositionSize: credit.adjustedSize,
900
+ tickLimitLow,
901
+ tickLimitHigh
902
+ };
903
+ const swapDispatch = buildPrefixedExactInputRecoveryDispatch({
904
+ ...wrapArgs,
905
+ dispatch: {
906
+ positionIdList: [],
907
+ finalPositionIdList: [...params.existingPositionIds],
908
+ positionSizes: [],
909
+ tickAndSpreadLimits: [],
910
+ usePremiaAsCollateral: false,
911
+ builderCode: 0n
912
+ }
913
+ });
914
+ const swapSimulation = await simulateDispatch({
915
+ client: params.client,
916
+ poolAddress: params.poolAddress,
917
+ account: params.account,
918
+ existingPositionIdList: params.existingPositionIds,
919
+ ...swapDispatch,
920
+ blockNumber: targetBlockNumber
921
+ });
922
+ if (!swapSimulation.success || swapSimulation.tokenFlow === void 0) return {
923
+ available: false,
924
+ reason: "swap-unavailable",
925
+ detail: swapSimulation.success ? "prefixed swap simulation returned no token flow" : `prefixed swap simulation reverted: ${swapSimulation.error.message}`,
926
+ error: swapSimulation.success ? void 0 : swapSimulation.error
927
+ };
928
+ const estimatedAmountIn = getInputAmount(swapSimulation.tokenFlow, tokenInIndex);
929
+ const estimatedAmountOut = getOutputAmount(swapSimulation.tokenFlow, tokenOutIndex);
930
+ if (estimatedAmountOut < requiredOutput) {
931
+ creditInput = estimatedAmountOut > 0n ? ceilDiv$2(creditInput * requiredOutput, estimatedAmountOut) + 1n : creditInput * 2n;
932
+ continue;
933
+ }
934
+ const recoveredDispatch = buildPrefixedExactInputRecoveryDispatch({
935
+ ...wrapArgs,
936
+ dispatch: params.dispatch
937
+ });
938
+ const recoverySimulation = await simulateDispatch({
939
+ client: params.client,
940
+ poolAddress: params.poolAddress,
941
+ account: params.account,
942
+ existingPositionIdList: params.existingPositionIds,
943
+ ...recoveredDispatch,
944
+ settleSequence: params.settleSequence,
945
+ measurePremia: true,
946
+ blockNumber: targetBlockNumber
947
+ });
948
+ if (recoverySimulation.success && recoverySimulation.tokenFlow !== void 0) return {
949
+ available: true,
950
+ quote: {
951
+ tokenIn,
952
+ tokenOut,
953
+ direction: "exact-in",
954
+ amountOut: requiredOutput,
955
+ estimatedAmountOut,
956
+ estimatedAmountIn,
957
+ maximumAmountIn: estimatedAmountIn,
958
+ slippageBps: params.slippageBps,
959
+ netTokenInChange: tokenInIndex === 0n ? recoverySimulation.tokenFlow.delta0 : recoverySimulation.tokenFlow.delta1,
960
+ netTokenOutChange: tokenOutIndex === 0n ? recoverySimulation.tokenFlow.delta0 : recoverySimulation.tokenFlow.delta1,
961
+ creditTokenId: credit.tokenId,
962
+ dispatch: recoveredDispatch,
963
+ simulation: {
964
+ ...recoverySimulation,
965
+ tokenFlow: recoverySimulation.tokenFlow
966
+ },
967
+ tokenFlow: recoverySimulation.tokenFlow,
968
+ _meta: recoverySimulation._meta
969
+ }
970
+ };
971
+ if (recoverySimulation.success) return {
972
+ available: false,
973
+ reason: "recovery-unavailable",
974
+ detail: "prefixed recovery simulation returned no token flow",
975
+ error: new PanopticError("Recovery simulation did not return token flow")
976
+ };
977
+ const remainingShortfall = getNotEnoughTokensError(recoverySimulation.error);
978
+ if (remainingShortfall === null || shortfallTokenIndex(remainingShortfall) !== tokenOutIndex) return {
979
+ available: false,
980
+ reason: "recovery-unavailable",
981
+ detail: `prefixed recovery reverted with a non-output shortfall: ${recoverySimulation.error.message}`,
982
+ error: recoverySimulation.error
983
+ };
984
+ const residual = remainingShortfall.assetsRequested - remainingShortfall.assetBalance;
985
+ requiredOutput += residual > 0n ? residual : requiredOutput;
986
+ creditInput = ceilDiv$2(creditInput * requiredOutput, estimatedAmountOut) + 1n;
987
+ }
574
988
  return {
575
- netLiquidity,
576
- removedLiquidity,
577
- totalLiquidity,
578
- shortLiquidity: totalLiquidity,
579
- longLiquidity: removedLiquidity
989
+ available: false,
990
+ reason: "recovery-unavailable",
991
+ detail: `prefixed recovery remained short after ${MAX_RECOVERY_QUOTE_ATTEMPTS} attempts`,
992
+ error: new PanopticError("Could not size the prefixed recovery swap")
580
993
  };
581
- });
582
- return {
583
- results,
584
- _meta
585
994
  };
995
+ let creditOutSize = amountOut;
996
+ for (let attempt = 0; attempt < MAX_RECOVERY_QUOTE_ATTEMPTS; attempt++) {
997
+ const credit = buildUniqueCredit(pool.poolId, tokenOutIndex, tokenOutIndex, pool.currentTick, pool.tickSpacing, collisionIds, creditOutSize);
998
+ const recoveredDispatch = buildTokenShortfallRecoveryDispatch({
999
+ dispatch: params.dispatch,
1000
+ creditTokenId: credit.tokenId,
1001
+ creditPositionSize: credit.adjustedSize,
1002
+ tickLimitLow,
1003
+ tickLimitHigh
1004
+ });
1005
+ const swapDispatch = buildTokenShortfallRecoveryDispatch({
1006
+ dispatch: {
1007
+ positionIdList: [],
1008
+ finalPositionIdList: [...params.existingPositionIds],
1009
+ positionSizes: [],
1010
+ tickAndSpreadLimits: [],
1011
+ usePremiaAsCollateral: false,
1012
+ builderCode: 0n
1013
+ },
1014
+ creditTokenId: credit.tokenId,
1015
+ creditPositionSize: credit.adjustedSize,
1016
+ tickLimitLow,
1017
+ tickLimitHigh
1018
+ });
1019
+ const swapSimulation = await simulateDispatch({
1020
+ client: params.client,
1021
+ poolAddress: params.poolAddress,
1022
+ account: params.account,
1023
+ existingPositionIdList: params.existingPositionIds,
1024
+ ...swapDispatch,
1025
+ blockNumber: targetBlockNumber
1026
+ });
1027
+ if (!swapSimulation.success || swapSimulation.tokenFlow === void 0) {
1028
+ const bootstrapShortfall = swapSimulation.success ? null : getNotEnoughTokensError(swapSimulation.error);
1029
+ if (bootstrapShortfall !== null && shortfallTokenIndex(bootstrapShortfall) === tokenOutIndex) return quotePrefixedExactInput(amountOut);
1030
+ return {
1031
+ available: false,
1032
+ reason: "swap-unavailable",
1033
+ detail: swapSimulation.success ? "swap-only simulation returned no token flow" : `swap-only simulation reverted: ${swapSimulation.error.message}`,
1034
+ error: swapSimulation.success ? void 0 : swapSimulation.error
1035
+ };
1036
+ }
1037
+ const estimatedAmountIn = getInputAmount(swapSimulation.tokenFlow, tokenInIndex);
1038
+ const maxAmountIn = maximumAmountIn(estimatedAmountIn, params.slippageBps);
1039
+ const swapOutput = getOutputAmount(swapSimulation.tokenFlow, tokenOutIndex);
1040
+ if (swapOutput < amountOut) {
1041
+ creditOutSize = swapOutput > 0n ? ceilDiv$2(creditOutSize * amountOut, swapOutput) + 1n : creditOutSize * 2n;
1042
+ continue;
1043
+ }
1044
+ const recoverySimulation = await simulateDispatch({
1045
+ client: params.client,
1046
+ poolAddress: params.poolAddress,
1047
+ account: params.account,
1048
+ existingPositionIdList: params.existingPositionIds,
1049
+ ...recoveredDispatch,
1050
+ settleSequence: params.settleSequence,
1051
+ measurePremia: true,
1052
+ blockNumber: targetBlockNumber
1053
+ });
1054
+ if (recoverySimulation.success) {
1055
+ if (recoverySimulation.tokenFlow !== void 0) return {
1056
+ available: true,
1057
+ quote: {
1058
+ tokenIn,
1059
+ tokenOut,
1060
+ direction: "exact-out",
1061
+ amountOut,
1062
+ estimatedAmountOut: swapOutput,
1063
+ estimatedAmountIn,
1064
+ maximumAmountIn: maxAmountIn,
1065
+ slippageBps: params.slippageBps,
1066
+ netTokenInChange: tokenInIndex === 0n ? recoverySimulation.tokenFlow.delta0 : recoverySimulation.tokenFlow.delta1,
1067
+ netTokenOutChange: tokenOutIndex === 0n ? recoverySimulation.tokenFlow.delta0 : recoverySimulation.tokenFlow.delta1,
1068
+ creditTokenId: credit.tokenId,
1069
+ dispatch: recoveredDispatch,
1070
+ simulation: {
1071
+ ...recoverySimulation,
1072
+ tokenFlow: recoverySimulation.tokenFlow
1073
+ },
1074
+ tokenFlow: recoverySimulation.tokenFlow,
1075
+ _meta: recoverySimulation._meta
1076
+ }
1077
+ };
1078
+ return {
1079
+ available: false,
1080
+ reason: "recovery-unavailable",
1081
+ detail: "wrapped dispatch simulation returned no token flow",
1082
+ error: new PanopticError("Recovery simulation did not return token flow")
1083
+ };
1084
+ }
1085
+ const remainingShortfall = getNotEnoughTokensError(recoverySimulation.error);
1086
+ if (remainingShortfall === null || shortfallTokenIndex(remainingShortfall) !== tokenOutIndex) return {
1087
+ available: false,
1088
+ reason: "recovery-unavailable",
1089
+ detail: `wrapped dispatch reverted with a non-shortfall error: ${recoverySimulation.error.message}`,
1090
+ error: recoverySimulation.error
1091
+ };
1092
+ const decodedShortfall = remainingShortfall.assetsRequested - remainingShortfall.assetBalance;
1093
+ if (decodedShortfall >= shortfallError.assetsRequested - shortfallError.assetBalance) return quotePrefixedExactInput(amountOut);
1094
+ const additionalAmountOut = decodedShortfall > 0n ? decodedShortfall : amountOut;
1095
+ amountOut += additionalAmountOut;
1096
+ creditOutSize = swapOutput > 0n ? ceilDiv$2(creditOutSize * amountOut, swapOutput) + 1n : creditOutSize * 2n;
1097
+ }
1098
+ return quotePrefixedExactInput(shortfallError.assetsRequested - shortfallError.assetBalance);
586
1099
  }
587
1100
 
588
1101
  //#endregion
589
- //#region src/panoptic/v2/reads/factory.ts
590
- /**
591
- * Get the token URI from a PanopticFactory NFT.
1102
+ //#region src/panoptic/v2/reads/lpMaxSize.ts
1103
+ /** Largest verified LP mint within 0.1% of the funding bound, at one block.
1104
+ * Never returns an untested size. Transport errors abort instead of shrinking MAX.
592
1105
  */
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]
1106
+ async function getExecutableLpMaxSize(params) {
1107
+ const snapshot = await readLpFundingSnapshot({
1108
+ ...params,
1109
+ positionSize: 0n
601
1110
  });
602
- }
603
- /**
604
- * Get the owner of a PanopticFactory NFT.
605
- */
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]
1111
+ const upper = getMaxLpPositionSize({
1112
+ ...snapshot.fundingParams,
1113
+ availableInQuote: snapshot.availableInQuote
614
1114
  });
1115
+ const limits = tickLimits(
1116
+ // Funding valuation is derived from the account's current tick.
1117
+ // Read the bounds from that same snapshot, not an independently cached oracle.
1118
+ snapshot.currentTick,
1119
+ params.slippageBps
1120
+ );
1121
+ const executable = async (size) => {
1122
+ const intent = {
1123
+ positionIdList: [params.tokenId],
1124
+ finalPositionIdList: [...params.existingPositionIds, params.tokenId],
1125
+ positionSizes: [size],
1126
+ tickAndSpreadLimits: [[
1127
+ limits.low,
1128
+ limits.high,
1129
+ 0n
1130
+ ]],
1131
+ usePremiaAsCollateral: false,
1132
+ builderCode: 0n
1133
+ };
1134
+ const simulation = await simulateDispatch({
1135
+ ...params,
1136
+ ...intent,
1137
+ existingPositionIdList: params.existingPositionIds,
1138
+ blockNumber: snapshot.blockNumber
1139
+ });
1140
+ if (simulation.success) return true;
1141
+ const parsedSimulationError = parsePanopticError(simulation.error);
1142
+ if (parsedSimulationError && isPanopticErrorType(parsedSimulationError.error, NotEnoughTokensError) && getNotEnoughTokensError(simulation.error) !== null) {
1143
+ const recovery = await quoteTokenShortfallRecovery({
1144
+ ...params,
1145
+ dispatch: intent,
1146
+ error: simulation.error,
1147
+ blockNumber: snapshot.blockNumber
1148
+ });
1149
+ if (recovery.available) return true;
1150
+ if (recovery.error) {
1151
+ const parsedRecoveryError = parsePanopticError(recovery.error);
1152
+ if (!parsedRecoveryError) throw recovery.error;
1153
+ }
1154
+ return false;
1155
+ }
1156
+ if (!parsedSimulationError) throw simulation.error;
1157
+ return false;
1158
+ };
1159
+ if (upper === 0n || await executable(upper)) return {
1160
+ maxSize: upper,
1161
+ ...snapshot
1162
+ };
1163
+ let low = 0n;
1164
+ let high = upper;
1165
+ const tolerance = upper / 1000n > 0n ? upper / 1000n : 1n;
1166
+ while (high - low > tolerance) {
1167
+ const mid = (low + high) / 2n;
1168
+ if (await executable(mid)) low = mid;
1169
+ else high = mid;
1170
+ }
1171
+ return {
1172
+ maxSize: low,
1173
+ ...snapshot
1174
+ };
615
1175
  }
1176
+
1177
+ //#endregion
1178
+ //#region src/panoptic/v2/utils/factory.ts
616
1179
  /**
617
- * Construct NFT metadata for a pool via the factory contract.
1180
+ * Decode a base64-encoded Panoptic NFT token URI into metadata.
1181
+ *
1182
+ * @param tokenURI - The base64-encoded data URI string
1183
+ * @returns Parsed NFT metadata object, or undefined if input is invalid
618
1184
  */
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
- });
1185
+ function decodePanopticTokenURI(tokenURI) {
1186
+ if (tokenURI === void 0 || tokenURI === "") return void 0;
1187
+ const parts = tokenURI.split("data:application/json;base64,");
1188
+ if (parts.length < 2 || parts[1] === "") return void 0;
1189
+ try {
1190
+ const decoded = atob(parts[1]);
1191
+ return JSON.parse(decoded);
1192
+ } catch {
1193
+ return void 0;
1194
+ }
633
1195
  }
1196
+
1197
+ //#endregion
1198
+ //#region src/panoptic/v2/utils/interpolateBlocks.ts
634
1199
  /**
635
- * Get the PanopticPool address for a given pool and risk engine.
1200
+ * Generate evenly-spaced block numbers between start and end (pure math, no RPC).
1201
+ *
1202
+ * @param startBlock - First block in the range
1203
+ * @param endBlock - Last block in the range
1204
+ * @param points - Number of evenly-spaced data points to generate
1205
+ * @returns Array of interpolated block numbers
1206
+ *
1207
+ * @throws {InvalidHistoryRangeError} if startBlock > endBlock or points < 0
636
1208
  */
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
- });
1209
+ function interpolateBlocks(startBlock, endBlock, points) {
1210
+ if (!Number.isFinite(points) || !Number.isSafeInteger(points)) throw new InvalidHistoryRangeError(`points must be a finite safe integer, got ${points}`);
1211
+ if (points < 0) throw new InvalidHistoryRangeError(`points must be >= 0, got ${points}`);
1212
+ if (startBlock > endBlock) throw new InvalidHistoryRangeError(`startBlock (${startBlock}) must be <= endBlock (${endBlock})`);
1213
+ if (points === 0) return [];
1214
+ if (points === 1) return [endBlock];
1215
+ const range = endBlock - startBlock;
1216
+ return Array.from({ length: points }, (_, i) => startBlock + range * BigInt(i) / BigInt(points - 1));
657
1217
  }
1218
+
1219
+ //#endregion
1220
+ //#region src/panoptic/v2/sfpmSwap/calldata.ts
658
1221
  /**
659
- * Mine for an optimal pool address salt with high rarity.
1222
+ * Encode the `multicall([mint, burn])` for a swap plan.
1223
+ *
1224
+ * The order is always `[mint, burn]` — the ERC1155 must be minted before it is
1225
+ * burned. Which call carries the inverted (swap) limits is decided in the plan.
660
1226
  */
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",
1227
+ function buildSfpmSwapCalldata(plan) {
1228
+ const mintData = encodeFunctionData({
1229
+ abi: semiFungiblePositionManagerV3Abi,
1230
+ functionName: "mintTokenizedPosition",
668
1231
  args: [
669
- deployerAddress,
670
- params.v3Pool,
671
- riskEngine,
672
- salt,
673
- loops,
674
- minTargetRarity
1232
+ plan.poolKey,
1233
+ plan.tokenId,
1234
+ plan.positionSize,
1235
+ plan.mintTickLimits[0],
1236
+ plan.mintTickLimits[1]
675
1237
  ]
676
1238
  });
677
- else result = await client.readContract({
678
- address: factoryAddress,
679
- abi: panopticFactoryV4Abi,
680
- functionName: "minePoolAddress",
1239
+ const burnData = encodeFunctionData({
1240
+ abi: semiFungiblePositionManagerV3Abi,
1241
+ functionName: "burnTokenizedPosition",
681
1242
  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
1243
+ plan.poolKey,
1244
+ plan.tokenId,
1245
+ plan.positionSize,
1246
+ plan.burnTickLimits[0],
1247
+ plan.burnTickLimits[1]
694
1248
  ]
695
1249
  });
1250
+ const multicallData = encodeFunctionData({
1251
+ abi: semiFungiblePositionManagerV3Abi,
1252
+ functionName: "multicall",
1253
+ args: [[mintData, burnData]]
1254
+ });
696
1255
  return {
697
- bestSalt: BigInt(result[0]),
698
- highestRarity: result[1]
1256
+ multicallData,
1257
+ mintData,
1258
+ burnData
699
1259
  };
700
1260
  }
1261
+
1262
+ //#endregion
1263
+ //#region src/panoptic/v2/sfpmSwap/init.ts
701
1264
  /**
702
- * Simulate a pool deployment to get the predicted pool address.
1265
+ * Resolve the `uint64` SFPM poolId for a Uniswap v3 pool, initializing it if needed.
703
1266
  *
704
- * Uses `simulateContract` on `deployNewPool` — the return value is the new pool address
705
- * without actually executing the transaction.
1267
+ * `initializeAMMPool` is permissionless and idempotent and **returns the poolId**
1268
+ * whether or not the pool was already registered, so a `simulateContract` call is
1269
+ * enough to learn the id without sending a transaction. Always resolve the id this
1270
+ * way rather than encoding it offline — the SFPM can collision-increment ids.
706
1271
  */
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
- }
1272
+ async function fetchSfpmV3PoolId(params) {
1273
+ const { client, sfpmAddress, token0, token1, fee } = params;
1274
+ const vegoid = params.vegoid ?? Number(DEFAULT_VEGOID);
725
1275
  const { result } = await client.simulateContract({
726
- address: factoryAddress,
727
- abi: panopticFactoryV4Abi,
728
- functionName: "deployNewPool",
1276
+ address: sfpmAddress,
1277
+ abi: semiFungiblePositionManagerV3Abi,
1278
+ functionName: "initializeAMMPool",
729
1279
  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
1280
+ token0,
1281
+ token1,
1282
+ fee,
1283
+ vegoid
1284
+ ]
741
1285
  });
742
- return result;
1286
+ return BigInt(result);
743
1287
  }
744
1288
  /**
745
- * Resolve an SFPM poolId to its PanopticPool address.
1289
+ * Ensure a Uniswap v3 pool is registered on the SFPM, sending `initializeAMMPool`
1290
+ * only if it is not already registered. Returns the resolved poolId.
746
1291
  *
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
1292
+ * When `expectedPool` is given, the resolved id is checked to map back to it via
1293
+ * `getUniswapV3PoolFromId` — guards against a wrong token/fee triple.
750
1294
  */
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({
1295
+ async function ensureSfpmV3PoolInitialized(params) {
1296
+ const { client, wallet, sfpmAddress, token0, token1, fee, expectedPool } = params;
1297
+ const vegoid = params.vegoid ?? Number(DEFAULT_VEGOID);
1298
+ const poolId = await fetchSfpmV3PoolId({
768
1299
  client,
769
1300
  sfpmAddress,
770
- poolId
1301
+ token0,
1302
+ token1,
1303
+ fee,
1304
+ vegoid
771
1305
  });
772
- return getPanopticPoolAddress({
773
- version: "v4",
774
- client,
775
- factoryAddress,
776
- poolKey,
777
- riskEngine
1306
+ const registered = await client.readContract({
1307
+ address: sfpmAddress,
1308
+ abi: semiFungiblePositionManagerV3Abi,
1309
+ functionName: "getUniswapV3PoolFromId",
1310
+ args: [poolId]
778
1311
  });
779
- }
780
- /**
781
- * Resolve an SFPM poolId to its PanopticPool address without knowing the version.
782
- *
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.
785
- *
786
- * At least one of `v3` or `v4` must be provided.
787
- *
788
- * @throws {PanopticValidationError} If no version config is provided or neither resolves.
789
- */
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;
799
- };
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"
825
- };
826
- if (v4Result !== zeroAddress) return {
827
- panopticPoolAddress: v4Result,
828
- version: "v4"
1312
+ let initialized = false;
1313
+ let resolved = registered;
1314
+ if (getAddress(registered) === zeroAddress) {
1315
+ const account = wallet.account;
1316
+ if (account === void 0) throw new PanopticError("wallet client has no account");
1317
+ const hash = await wallet.writeContract({
1318
+ account,
1319
+ chain: wallet.chain ?? null,
1320
+ address: sfpmAddress,
1321
+ abi: semiFungiblePositionManagerV3Abi,
1322
+ functionName: "initializeAMMPool",
1323
+ args: [
1324
+ token0,
1325
+ token1,
1326
+ fee,
1327
+ vegoid
1328
+ ]
1329
+ });
1330
+ await client.waitForTransactionReceipt({ hash });
1331
+ initialized = true;
1332
+ resolved = await client.readContract({
1333
+ address: sfpmAddress,
1334
+ abi: semiFungiblePositionManagerV3Abi,
1335
+ functionName: "getUniswapV3PoolFromId",
1336
+ args: [poolId]
1337
+ });
1338
+ }
1339
+ if (expectedPool !== void 0 && getAddress(resolved) !== getAddress(expectedPool)) throw new WrongUniswapPoolError(new PanopticError(`SFPM poolId ${poolId} resolves to ${resolved}, expected ${expectedPool}`));
1340
+ return {
1341
+ poolId,
1342
+ initialized
829
1343
  };
830
- throw new Error(`No PanopticPool found for poolId ${poolId}`);
831
1344
  }
832
1345
 
833
1346
  //#endregion
834
- //#region src/panoptic/v2/reads/minePoolAddressLocal.ts
1347
+ //#region src/panoptic/v2/sfpmSwap/plan.ts
1348
+ /** Wide, non-inverted band for the paired (non-swapping) call — never triggers a swap. */
1349
+ const WIDE_LIMITS = [Number(MIN_TICK) + 1, Number(MAX_TICK) - 1];
835
1350
  /**
836
- * keccak256 of the CREATE3 proxy initcode used by ClonesWithImmutableArgs.
837
- * Source: packages/panoptic-v2-core/lib/clones-with-immutable-args/src/ClonesWithImmutableArgs.sol
1351
+ * Convert a slippage tolerance in bps to a conservative Uniswap tick distance.
1352
+ *
1353
+ * Ticks are 1.0001^tick, so each tick ≈ 1 bps. This walks up powers of 1.0001
1354
+ * until the cumulative price move covers `slippageBps`, matching the hedger-bot's
1355
+ * dispatch-path helper so both swap paths agree on band width.
838
1356
  */
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;
1357
+ function slippageBpsToTickDistance(slippageBps) {
1358
+ if (slippageBps < 0n || slippageBps > 1000n) throw new PanopticError(`slippage bps ${slippageBps} out of bounds (0..1000)`);
1359
+ if (slippageBps === 0n) return 0n;
1360
+ let numerator = 1n;
1361
+ let denominator = 1n;
1362
+ let ticks = 0n;
1363
+ while (numerator * 10000n < denominator * (10000n + slippageBps)) {
1364
+ numerator *= 10001n;
1365
+ denominator *= 10000n;
1366
+ ticks += 1n;
850
1367
  }
851
- return bytes;
1368
+ return ticks;
852
1369
  }
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;
1370
+ /**
1371
+ * Build the swap plan.
1372
+ *
1373
+ * Mechanism (verified in the Phase 0 fork test):
1374
+ * - A single-leg **loan** tokenId (width=0, isLong=false, `asset == tokenType`) moves
1375
+ * exactly `positionSize` of the `tokenType` token when its call carries inverted
1376
+ * tick limits (`low > high`); the paired call uses a wide band and moves nothing.
1377
+ * - `exactIn`: swap on the **mint**; `tokenType` = the **input** token index.
1378
+ * - `exactOut`: swap on the **burn** (isLong flips → exact-output); `tokenType` = the
1379
+ * **output** token index.
1380
+ *
1381
+ * The inverted band is centered on `currentTick` at ±`slippageBpsToTickDistance`,
1382
+ * which the SFPM re-sorts and enforces as an open interval on the post-swap tick.
1383
+ */
1384
+ function buildSfpmSwapPlan(params) {
1385
+ const { sfpmAddress, poolAddress, poolId, kind, zeroForOne, amount, currentTick, slippageBps } = params;
1386
+ if (amount <= 0n) throw new PanopticError(`swap amount must be positive (got ${amount})`);
1387
+ const distance = slippageBpsToTickDistance(slippageBps);
1388
+ if (distance < 1n) throw new PanopticError(`slippageBps ${slippageBps} yields a zero-width tick band; use a larger tolerance`);
1389
+ const tokenType = kind === "exactIn" ? zeroForOne ? 0n : 1n : zeroForOne ? 1n : 0n;
1390
+ const tokenId = createTokenIdBuilder(poolId).addLoan({
1391
+ asset: tokenType,
1392
+ tokenType,
1393
+ strike: 0n
1394
+ }).build();
1395
+ const d = Number(distance);
1396
+ const low = Math.min(Number(MAX_TICK), currentTick + d);
1397
+ const high = Math.max(Number(MIN_TICK), currentTick - d);
1398
+ if (low <= high) throw new PanopticError(`slippageBps ${slippageBps} at tick ${currentTick} yields a non-inverted band after clamping`);
1399
+ const invertedLimits = [low, high];
1400
+ const swapOn = kind === "exactIn" ? "mint" : "burn";
1401
+ return {
1402
+ sfpmAddress,
1403
+ poolAddress,
1404
+ poolKey: encodeAbiParameters([{ type: "address" }], [poolAddress]),
1405
+ tokenId,
1406
+ positionSize: amount,
1407
+ mintTickLimits: swapOn === "mint" ? invertedLimits : WIDE_LIMITS,
1408
+ burnTickLimits: swapOn === "burn" ? invertedLimits : WIDE_LIMITS,
1409
+ swapOn,
1410
+ kind
1411
+ };
859
1412
  }
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;
1413
+
1414
+ //#endregion
1415
+ //#region src/panoptic/v2/sfpmSwap/quote.ts
1416
+ const UINT128 = 1n << 128n;
1417
+ const INT128_MAX = (1n << 127n) - 1n;
1418
+ /** Extract the two signed 128-bit slots from a packed `LeftRightSigned` int256. */
1419
+ function unpackLeftRightSigned(packed) {
1420
+ const u = packed < 0n ? packed + (1n << 256n) : packed;
1421
+ const toInt128 = (half) => half > INT128_MAX ? half - UINT128 : half;
1422
+ return {
1423
+ right: toInt128(u & UINT128 - 1n),
1424
+ left: toInt128(u >> 128n & UINT128 - 1n)
1425
+ };
866
1426
  }
867
- /**
868
- * Compute the CREATE3 deployed address for a given factory and packed salt.
869
- *
870
- * Mirrors `ClonesWithImmutableArgs.addressOfClone3(salt)` (with `address(this)` = factory):
871
- * proxy = CREATE2(factory, salt, PROXY_BYTECODE_HASH)
872
- * deployed = CREATE1(proxy, nonce=1)
873
- *
874
- * Returns the deployed address as a uint160 BigInt.
875
- */
876
- function addressOfClone3(factory, salt) {
877
- const saltBytes = bigintToBytes(salt, 32);
878
- const create2Input = new Uint8Array(85);
879
- create2Input[0] = 255;
880
- create2Input.set(addressToBytes(factory), 1);
881
- create2Input.set(saltBytes, 21);
882
- create2Input.set(hex32ToBytes(CREATE3_PROXY_BYTECODE_HASH), 53);
883
- const proxyHash = keccak256(create2Input, "bytes");
884
- const proxyAddress = proxyHash.slice(12);
885
- const create1Input = new Uint8Array(23);
886
- create1Input[0] = 214;
887
- create1Input[1] = 148;
888
- create1Input.set(proxyAddress, 2);
889
- create1Input[22] = 1;
890
- const deployedHash = keccak256(create1Input, "bytes");
891
- let addr = 0n;
892
- for (let i = 12; i < 32; i++) addr = addr << 8n | BigInt(deployedHash[i]);
893
- return addr;
1427
+ /** Wrap an unknown thrown value as a PanopticError, decoding Panoptic reverts when possible. */
1428
+ function toPanopticError(err) {
1429
+ if (err instanceof PanopticError) return err;
1430
+ const parsed = parsePanopticError(err);
1431
+ if (parsed) return parsed.error;
1432
+ return new PanopticError(err instanceof Error ? err.message : "SFPM swap simulation failed", err instanceof Error ? err : void 0);
894
1433
  }
895
1434
  /**
896
- * Count leading hex-zero characters in a 160-bit address value.
897
- *
898
- * Mirrors `PanopticMath.numberOfLeadingHexZeros(addr)`.
899
- * Returns 40 for the zero address.
1435
+ * Quote a swap by simulating `SFPM.multicall([mint, burn])` and decoding the swap
1436
+ * call's `totalMoved` return. Authoritative — captures the width-0 loan-leg wei
1437
+ * rounding a raw QuoterV2 quote would miss.
900
1438
  */
901
- function numberOfLeadingHexZeros(addrInt) {
902
- if (addrInt === 0n) return 40;
903
- let x = addrInt;
904
- let r = 0;
905
- if (x >= 0x100000000000000000000000000000000n) {
906
- x >>= 128n;
907
- r += 32;
908
- }
909
- if (x >= 0x10000000000000000n) {
910
- x >>= 64n;
911
- r += 16;
912
- }
913
- if (x >= 0x100000000n) {
914
- x >>= 32n;
915
- r += 8;
916
- }
917
- if (x >= 0x10000n) {
918
- x >>= 16n;
919
- r += 4;
920
- }
921
- if (x >= 0x100n) {
922
- x >>= 8n;
923
- r += 2;
1439
+ async function quoteSfpmSwap(params) {
1440
+ const { client, plan, account, stateOverride, blockNumber } = params;
1441
+ const { mintData, burnData } = buildSfpmSwapCalldata(plan);
1442
+ const _meta = await getBlockMeta({
1443
+ client,
1444
+ blockNumber
1445
+ });
1446
+ try {
1447
+ const { result } = await client.simulateContract({
1448
+ account,
1449
+ address: plan.sfpmAddress,
1450
+ abi: semiFungiblePositionManagerV3Abi,
1451
+ functionName: "multicall",
1452
+ args: [[mintData, burnData]],
1453
+ blockNumber,
1454
+ stateOverride
1455
+ });
1456
+ const swapIndex = plan.swapOn === "mint" ? 0 : 1;
1457
+ const decoded = decodeFunctionResult({
1458
+ abi: semiFungiblePositionManagerV3Abi,
1459
+ functionName: plan.swapOn === "mint" ? "mintTokenizedPosition" : "burnTokenizedPosition",
1460
+ data: result[swapIndex]
1461
+ });
1462
+ const finalTick = Number(decoded[2]);
1463
+ const { right, left } = unpackLeftRightSigned(decoded[1]);
1464
+ const inSlot = [right, left].find((s) => s > 0n);
1465
+ const outSlot = [right, left].find((s) => s < 0n);
1466
+ if (inSlot === void 0 || outSlot === void 0) return {
1467
+ success: false,
1468
+ error: new PanopticError(`SFPM swap simulation moved no tokens (totalMoved=${decoded[1]})`),
1469
+ _meta
1470
+ };
1471
+ return {
1472
+ success: true,
1473
+ data: {
1474
+ amountIn: inSlot,
1475
+ amountOut: -outSlot,
1476
+ finalTick
1477
+ },
1478
+ gasEstimate: 0n,
1479
+ _meta
1480
+ };
1481
+ } catch (err) {
1482
+ return {
1483
+ success: false,
1484
+ error: toPanopticError(err),
1485
+ _meta
1486
+ };
924
1487
  }
925
- if (x >= 0x10n) r += 1;
926
- return 39 - r;
927
- }
928
- /**
929
- * Construct the bytes32 CREATE3 salt for PanopticFactoryV3.
930
- *
931
- * Mirrors:
932
- * bytes32(abi.encodePacked(
933
- * uint80(uint160(deployerAddress) >> 80), // bits [159:80] of deployer → 10 bytes
934
- * uint40(uint160(v3Pool) >> 120), // bits [159:120] of v3Pool → 5 bytes
935
- * uint40(uint160(riskEngine) >> 120), // bits [159:120] of riskEngine→ 5 bytes
936
- * salt // uint96 → 12 bytes
937
- * ))
938
- */
939
- function computeSaltPrefixV3(deployerAddress, v3Pool, riskEngine) {
940
- const deployer80 = BigInt(deployerAddress) >> 80n & MASK_80;
941
- const pool40 = BigInt(v3Pool) >> 120n & MASK_40;
942
- const risk40 = BigInt(riskEngine) >> 120n & MASK_40;
943
- return deployer80 << 176n | pool40 << 136n | risk40 << 96n;
944
1488
  }
1489
+
1490
+ //#endregion
1491
+ //#region src/panoptic/v2/reads/sfpm.ts
945
1492
  /**
946
- * Compute the Uniswap V4 PoolId for a PoolKey.
1493
+ * Resolve an SFPM poolId to its corresponding Uniswap V3 pool address.
947
1494
  *
948
- * Mirrors `PoolId.toId(key)` = keccak256 of the ABI-encoded PoolKey struct
949
- * (5 fields × 32 bytes = 160 bytes).
1495
+ * Calls `SemiFungiblePositionManagerV3.getUniswapV3PoolFromId(poolId)`.
950
1496
  */
951
- function computePoolIdV4(poolKey) {
952
- const fee = Number(poolKey.fee);
953
- if (!Number.isInteger(fee) || fee < 0 || fee >= 2 ** 24) throw new PanopticValidationError(`fee out of uint24 range: ${fee}`);
954
- const tickSpacing = Number(poolKey.tickSpacing);
955
- if (!Number.isInteger(tickSpacing) || tickSpacing < -(2 ** 23) || tickSpacing > 2 ** 23 - 1) throw new PanopticValidationError(`tickSpacing out of int24 range: ${tickSpacing}`);
956
- const encoded = encodeAbiParameters([
957
- { type: "address" },
958
- { type: "address" },
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));
1497
+ async function getUniswapV3PoolFromId(params) {
1498
+ const { client, sfpmAddress, poolId } = params;
1499
+ return client.readContract({
1500
+ address: sfpmAddress,
1501
+ abi: semiFungiblePositionManagerV3Abi,
1502
+ functionName: "getUniswapV3PoolFromId",
1503
+ args: [poolId]
1504
+ });
970
1505
  }
971
1506
  /**
972
- * Construct the bytes32 CREATE3 salt for PanopticFactoryV4.
1507
+ * Resolve an SFPM poolId to its corresponding Uniswap V4 pool key.
973
1508
  *
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
- * ))
1509
+ * Calls `SemiFungiblePositionManagerV4.getUniswapV4PoolKeyFromId(poolId)`.
981
1510
  */
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;
1511
+ async function getUniswapV4PoolKeyFromId(params) {
1512
+ const { client, sfpmAddress, poolId } = params;
1513
+ const raw = await client.readContract({
1514
+ address: sfpmAddress,
1515
+ abi: semiFungiblePositionManagerV4Abi,
1516
+ functionName: "getUniswapV4PoolKeyFromId",
1517
+ args: [poolId]
1518
+ });
1519
+ return {
1520
+ currency0: raw.currency0,
1521
+ currency1: raw.currency1,
1522
+ fee: BigInt(raw.fee),
1523
+ tickSpacing: BigInt(raw.tickSpacing),
1524
+ hooks: raw.hooks
1525
+ };
987
1526
  }
988
- /** Number of iterations per chunk before yielding back to the event loop. */
989
- const CHUNK_SIZE = 5000n;
990
1527
  /**
991
- * Async version of {@link minePoolAddressLocal} that yields to the event loop
992
- * between chunks of iterations, preventing the browser UI from freezing.
1528
+ * Get the enforced tick limits for a pool from the SFPM.
993
1529
  *
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).
1530
+ * @param params - The parameters
1531
+ * @returns The min and max enforced ticks
996
1532
  */
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
- }
1533
+ async function getEnforcedTickLimits(params) {
1534
+ const { client, sfpmAddress, poolId } = params;
1535
+ const [minTick, maxTick] = await client.readContract({
1536
+ address: sfpmAddress,
1537
+ abi: semiFungiblePositionManagerV4Abi,
1538
+ functionName: "getEnforcedTickLimits",
1539
+ args: [poolId]
1540
+ });
1025
1541
  return {
1026
- bestSalt,
1027
- highestRarity
1542
+ minEnforcedTick: minTick,
1543
+ maxEnforcedTick: maxTick
1544
+ };
1545
+ }
1546
+ /**
1547
+ * Fetch liquidity breakdown for a batch of chunks via SFPM.getAccountLiquidity().
1548
+ *
1549
+ * Uses multicall for efficiency. Returns one result per input chunk,
1550
+ * along with block metadata for freshness tracking.
1551
+ */
1552
+ async function getChunkLiquidities(params) {
1553
+ const { client, sfpmAddress, poolKeyBytes, chunks } = params;
1554
+ if (chunks.length > MAX_TRACKED_CHUNKS) throw new ChunkLimitError(BigInt(chunks.length), 0n);
1555
+ const _meta = params._meta ?? await getBlockMeta({ client });
1556
+ if (chunks.length === 0) return {
1557
+ results: [],
1558
+ _meta
1559
+ };
1560
+ const multicallResults = await client.multicall({
1561
+ contracts: chunks.map((chunk) => ({
1562
+ address: sfpmAddress,
1563
+ abi: semiFungiblePositionManagerV4Abi,
1564
+ functionName: "getAccountLiquidity",
1565
+ args: [
1566
+ poolKeyBytes,
1567
+ chunk.owner,
1568
+ chunk.tokenType,
1569
+ Number(chunk.tickLower),
1570
+ Number(chunk.tickUpper)
1571
+ ]
1572
+ })),
1573
+ allowFailure: true
1574
+ });
1575
+ const results = multicallResults.map((result) => {
1576
+ if (result.status === "failure") return {
1577
+ netLiquidity: 0n,
1578
+ removedLiquidity: 0n,
1579
+ totalLiquidity: 0n,
1580
+ shortLiquidity: 0n,
1581
+ longLiquidity: 0n
1582
+ };
1583
+ const packed = result.result;
1584
+ const netLiquidity = packed & (1n << 128n) - 1n;
1585
+ const removedLiquidity = packed >> 128n;
1586
+ const totalLiquidity = netLiquidity + removedLiquidity;
1587
+ return {
1588
+ netLiquidity,
1589
+ removedLiquidity,
1590
+ totalLiquidity,
1591
+ shortLiquidity: totalLiquidity,
1592
+ longLiquidity: removedLiquidity
1593
+ };
1594
+ });
1595
+ return {
1596
+ results,
1597
+ _meta
1028
1598
  };
1029
1599
  }
1030
1600
 
1031
1601
  //#endregion
1032
- //#region src/panoptic/v2/utils/priceConvert.ts
1033
- const Q128$3 = 1n << 128n;
1602
+ //#region src/panoptic/v2/reads/factory.ts
1034
1603
  /**
1035
- * A non-positive price has no meaningful conversion, and `convert1to0` would
1036
- * divide by zero — surface that as a typed SDK error rather than a RangeError
1037
- * thrown from inside the arithmetic.
1604
+ * Get the token URI from a PanopticFactory NFT.
1038
1605
  */
1039
- function assertPositivePrice(sqrtPriceX96) {
1040
- if (sqrtPriceX96 <= 0n) throw new PanopticError(`sqrtPriceX96 must be positive, got ${sqrtPriceX96}`);
1606
+ async function getFactoryTokenURI(params) {
1607
+ const { client, factoryAddress, version, tokenId } = params;
1608
+ const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
1609
+ return client.readContract({
1610
+ address: factoryAddress,
1611
+ abi,
1612
+ functionName: "tokenURI",
1613
+ args: [tokenId]
1614
+ });
1041
1615
  }
1042
- /** Convert a token0 amount to its token1-equivalent at the given sqrtPriceX96. */
1043
- function convert0to1$2(amount, sqrtPriceX96) {
1044
- assertPositivePrice(sqrtPriceX96);
1045
- if (sqrtPriceX96 < Q128$3) return amount * sqrtPriceX96 * sqrtPriceX96 >> 192n;
1046
- const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
1047
- return amount * sp2Hi >> 128n;
1616
+ /**
1617
+ * Get the owner of a PanopticFactory NFT.
1618
+ */
1619
+ async function getFactoryOwnerOf(params) {
1620
+ const { client, factoryAddress, version, tokenId } = params;
1621
+ const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
1622
+ return client.readContract({
1623
+ address: factoryAddress,
1624
+ abi,
1625
+ functionName: "ownerOf",
1626
+ args: [tokenId]
1627
+ });
1048
1628
  }
1049
- /** Convert a token1 amount to its token0-equivalent at the given sqrtPriceX96. */
1050
- function convert1to0$2(amount, sqrtPriceX96) {
1051
- assertPositivePrice(sqrtPriceX96);
1052
- if (sqrtPriceX96 < Q128$3) {
1053
- const denom = sqrtPriceX96 * sqrtPriceX96;
1054
- return amount * (1n << 192n) / denom;
1055
- }
1056
- const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
1057
- return amount * (1n << 128n) / sp2Hi;
1629
+ /**
1630
+ * Construct NFT metadata for a pool via the factory contract.
1631
+ */
1632
+ async function getFactoryConstructMetadata(params) {
1633
+ const { client, factoryAddress, version, panopticPoolAddress, symbol0, symbol1, fee } = params;
1634
+ const abi = version === "v3" ? panopticFactoryV3Abi : panopticFactoryV4Abi;
1635
+ return client.readContract({
1636
+ address: factoryAddress,
1637
+ abi,
1638
+ functionName: "constructMetadata",
1639
+ args: [
1640
+ panopticPoolAddress,
1641
+ symbol0,
1642
+ symbol1,
1643
+ fee
1644
+ ]
1645
+ });
1058
1646
  }
1059
1647
  /**
1060
- * Convert an amount denominated in `fromTokenIndex` into the other token's
1061
- * terms, so the two sides of a pool flow can be compared on one scale.
1648
+ * Get the PanopticPool address for a given pool and risk engine.
1062
1649
  */
1063
- function convertToTokenIndex(amount, fromTokenIndex, toTokenIndex, sqrtPriceX96) {
1064
- if (fromTokenIndex === toTokenIndex) return amount;
1065
- return fromTokenIndex === 0n ? convert0to1$2(amount, sqrtPriceX96) : convert1to0$2(amount, sqrtPriceX96);
1650
+ async function getPanopticPoolAddress(params) {
1651
+ const { client, factoryAddress, riskEngine } = params;
1652
+ if (params.version === "v3") return client.readContract({
1653
+ address: factoryAddress,
1654
+ abi: panopticFactoryV3Abi,
1655
+ functionName: "getPanopticPool",
1656
+ args: [params.univ3pool, riskEngine]
1657
+ });
1658
+ return client.readContract({
1659
+ address: factoryAddress,
1660
+ abi: panopticFactoryV4Abi,
1661
+ functionName: "getPanopticPool",
1662
+ args: [{
1663
+ currency0: params.poolKey.currency0,
1664
+ currency1: params.poolKey.currency1,
1665
+ fee: Number(params.poolKey.fee),
1666
+ tickSpacing: Number(params.poolKey.tickSpacing),
1667
+ hooks: params.poolKey.hooks
1668
+ }, riskEngine]
1669
+ });
1066
1670
  }
1067
-
1068
- //#endregion
1069
- //#region src/panoptic/v2/simulations/creditWrap.ts
1070
1671
  /**
1071
- * Wrap a dispatch with a temporary credit leg that is opened and closed in the
1072
- * same transaction, netting to a swap.
1073
- *
1074
- * `swapAtMint` is not a calldata flag — it is the ORDER of the tick-limit pair:
1075
- * descending `[high, low]` turns the swap on, ascending `[low, high]` leaves it
1076
- * off. Exactly one of the two legs carries it, and which one is what makes the
1077
- * swap exact-in vs exact-out.
1672
+ * Mine for an optimal pool address salt with high rarity.
1078
1673
  */
1079
- function buildCreditWrappedDispatch(params) {
1080
- const { dispatch, creditTokenId, creditPositionSize, direction, placement } = params;
1081
- const low = params.tickLimitLow <= params.tickLimitHigh ? params.tickLimitLow : params.tickLimitHigh;
1082
- const high = params.tickLimitLow <= params.tickLimitHigh ? params.tickLimitHigh : params.tickLimitLow;
1083
- const swapping = [
1084
- high,
1085
- low,
1086
- 0n
1087
- ];
1088
- const notSwapping = [
1089
- low,
1090
- high,
1091
- 0n
1092
- ];
1093
- const mintLimits = direction === "exact-out" ? swapping : notSwapping;
1094
- const burnLimits = direction === "exact-out" ? notSwapping : swapping;
1095
- const positionIdList = placement === "straddle" ? [
1096
- creditTokenId,
1097
- ...dispatch.positionIdList,
1098
- creditTokenId
1099
- ] : placement === "prepend" ? [
1100
- creditTokenId,
1101
- creditTokenId,
1102
- ...dispatch.positionIdList
1103
- ] : [
1104
- ...dispatch.positionIdList,
1105
- creditTokenId,
1106
- creditTokenId
1107
- ];
1108
- const positionSizes = placement === "straddle" ? [
1109
- creditPositionSize,
1110
- ...dispatch.positionSizes,
1111
- 0n
1112
- ] : placement === "prepend" ? [
1113
- creditPositionSize,
1114
- 0n,
1115
- ...dispatch.positionSizes
1116
- ] : [
1117
- ...dispatch.positionSizes,
1118
- creditPositionSize,
1119
- 0n
1120
- ];
1121
- const tickAndSpreadLimits = placement === "straddle" ? [
1122
- mintLimits,
1123
- ...dispatch.tickAndSpreadLimits,
1124
- burnLimits
1125
- ] : placement === "prepend" ? [
1126
- mintLimits,
1127
- burnLimits,
1128
- ...dispatch.tickAndSpreadLimits
1129
- ] : [
1130
- ...dispatch.tickAndSpreadLimits,
1131
- mintLimits,
1132
- burnLimits
1133
- ];
1134
- const wrapped = {
1135
- positionIdList,
1136
- finalPositionIdList: [...dispatch.finalPositionIdList],
1137
- positionSizes,
1138
- tickAndSpreadLimits,
1139
- usePremiaAsCollateral: dispatch.usePremiaAsCollateral,
1140
- builderCode: dispatch.builderCode
1674
+ async function minePoolAddress(params) {
1675
+ const { client, factoryAddress, deployerAddress, riskEngine, salt, loops, minTargetRarity } = params;
1676
+ let result;
1677
+ if (params.version === "v3") result = await client.readContract({
1678
+ address: factoryAddress,
1679
+ abi: panopticFactoryV3Abi,
1680
+ functionName: "minePoolAddress",
1681
+ args: [
1682
+ deployerAddress,
1683
+ params.v3Pool,
1684
+ riskEngine,
1685
+ salt,
1686
+ loops,
1687
+ minTargetRarity
1688
+ ]
1689
+ });
1690
+ else result = await client.readContract({
1691
+ address: factoryAddress,
1692
+ abi: panopticFactoryV4Abi,
1693
+ functionName: "minePoolAddress",
1694
+ args: [
1695
+ deployerAddress,
1696
+ {
1697
+ currency0: params.poolKey.currency0,
1698
+ currency1: params.poolKey.currency1,
1699
+ fee: Number(params.poolKey.fee),
1700
+ tickSpacing: Number(params.poolKey.tickSpacing),
1701
+ hooks: params.poolKey.hooks
1702
+ },
1703
+ riskEngine,
1704
+ salt,
1705
+ loops,
1706
+ minTargetRarity
1707
+ ]
1708
+ });
1709
+ return {
1710
+ bestSalt: BigInt(result[0]),
1711
+ highestRarity: result[1]
1141
1712
  };
1142
- return wrapped;
1143
1713
  }
1144
-
1145
- //#endregion
1146
- //#region src/panoptic/v2/simulations/simulateDispatch.ts
1147
1714
  /**
1148
- * Simulate a raw dispatch operation.
1149
- *
1150
- * Uses PanopticPool.multicall with getAssetsOf-dispatch-getAssetsOf pattern
1151
- * to measure exact collateral asset movements.
1715
+ * Simulate a pool deployment to get the predicted pool address.
1152
1716
  *
1153
- * @param params - Simulation parameters
1154
- * @returns Simulation result with dispatch data or error
1717
+ * Uses `simulateContract` on `deployNewPool` — the return value is the new pool address
1718
+ * without actually executing the transaction.
1155
1719
  */
1156
- async function simulateDispatch(params) {
1157
- const { client, poolAddress, account, positionIdList, finalPositionIdList, positionSizes, tickAndSpreadLimits, usePremiaAsCollateral = false, builderCode = 0n, blockNumber, existingPositionIdList, measurePremia = false } = params;
1158
- const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
1159
- const metaPromise = getBlockMeta({
1160
- client,
1161
- blockNumber: targetBlockNumber
1162
- });
1163
- try {
1164
- const callData = encodeFunctionData({
1165
- abi: panopticPoolV2Abi,
1166
- functionName: "dispatch",
1720
+ async function simulateDeployNewPool(params) {
1721
+ const { client, factoryAddress, account, riskEngine, salt } = params;
1722
+ if (params.version === "v3") {
1723
+ const { result: result$1 } = await client.simulateContract({
1724
+ address: factoryAddress,
1725
+ abi: panopticFactoryV3Abi,
1726
+ functionName: "deployNewPool",
1167
1727
  args: [
1168
- positionIdList,
1169
- finalPositionIdList,
1170
- positionSizes,
1171
- tickAndSpreadLimits.map((t) => [
1172
- Number(t[0]),
1173
- Number(t[1]),
1174
- Number(t[2])
1175
- ]),
1176
- usePremiaAsCollateral,
1177
- builderCode
1178
- ]
1728
+ params.token0,
1729
+ params.token1,
1730
+ Number(params.fee),
1731
+ riskEngine,
1732
+ salt
1733
+ ],
1734
+ account
1179
1735
  });
1180
- const preFullPositionsCallData = existingPositionIdList !== void 0 ? encodeFunctionData({
1181
- abi: panopticPoolV2Abi,
1182
- functionName: "getFullPositionsData",
1183
- args: [
1184
- account,
1185
- false,
1186
- existingPositionIdList
1187
- ]
1188
- }) : void 0;
1189
- const prePremiaCallData = measurePremia && existingPositionIdList !== void 0 ? encodeFunctionData({
1190
- abi: panopticPoolV2Abi,
1191
- functionName: "getFullPositionsData",
1192
- args: [
1193
- account,
1194
- true,
1195
- existingPositionIdList
1196
- ]
1197
- }) : void 0;
1198
- const postFullPositionsCallData = encodeFunctionData({
1199
- abi: panopticPoolV2Abi,
1200
- functionName: "getFullPositionsData",
1201
- args: [
1202
- account,
1203
- false,
1204
- finalPositionIdList
1205
- ]
1736
+ return result$1;
1737
+ }
1738
+ const { result } = await client.simulateContract({
1739
+ address: factoryAddress,
1740
+ abi: panopticFactoryV4Abi,
1741
+ functionName: "deployNewPool",
1742
+ args: [
1743
+ {
1744
+ currency0: params.poolKey.currency0,
1745
+ currency1: params.poolKey.currency1,
1746
+ fee: Number(params.poolKey.fee),
1747
+ tickSpacing: Number(params.poolKey.tickSpacing),
1748
+ hooks: params.poolKey.hooks
1749
+ },
1750
+ riskEngine,
1751
+ salt
1752
+ ],
1753
+ account
1754
+ });
1755
+ return result;
1756
+ }
1757
+ /**
1758
+ * Resolve an SFPM poolId to its PanopticPool address.
1759
+ *
1760
+ * Chains two on-chain lookups:
1761
+ * 1. SFPM: poolId → Uniswap pool address (V3) or pool key (V4)
1762
+ * 2. Factory: Uniswap pool + riskEngine → PanopticPool address
1763
+ */
1764
+ async function getPanopticPoolFromPoolId(params) {
1765
+ const { client, sfpmAddress, factoryAddress, riskEngine, poolId, version } = params;
1766
+ if (version === "v3") {
1767
+ const univ3pool = await getUniswapV3PoolFromId({
1768
+ client,
1769
+ sfpmAddress,
1770
+ poolId
1206
1771
  });
1207
- const postPremiaCallData = measurePremia ? encodeFunctionData({
1208
- abi: panopticPoolV2Abi,
1209
- functionName: "getFullPositionsData",
1210
- args: [
1211
- account,
1212
- true,
1213
- finalPositionIdList
1214
- ]
1215
- }) : void 0;
1216
- const flowResult = await simulateWithTokenFlow({
1772
+ return getPanopticPoolAddress({
1773
+ version: "v3",
1217
1774
  client,
1218
- poolAddress,
1219
- user: account,
1220
- callData,
1221
- blockNumber: targetBlockNumber,
1222
- preCallData: preFullPositionsCallData ? [preFullPositionsCallData, ...prePremiaCallData ? [prePremiaCallData] : []] : void 0,
1223
- postCallData: [postFullPositionsCallData, ...postPremiaCallData ? [postPremiaCallData] : []]
1775
+ factoryAddress,
1776
+ univ3pool,
1777
+ riskEngine
1224
1778
  });
1225
- if (!flowResult.success || !flowResult.tokenFlow) throw flowResult.rawError ?? new PanopticError(flowResult.error || "Token flow simulation failed");
1226
- const tokenFlow = flowResult.tokenFlow;
1227
- const preSnapshot = existingPositionIdList ?? [];
1228
- const positionsCreated = finalPositionIdList.filter((id) => !preSnapshot.includes(id));
1229
- const positionsClosed = preSnapshot.filter((id) => !finalPositionIdList.includes(id));
1230
- const decodeFullPositions = (data$1) => {
1231
- if (!data$1) return null;
1232
- try {
1233
- const decoded = decodeFunctionResult({
1234
- abi: panopticPoolV2Abi,
1235
- functionName: "getFullPositionsData",
1236
- data: data$1
1237
- });
1238
- const shortPremium = decodeLeftRightUnsigned(decoded[0]);
1239
- const longPremium = decodeLeftRightUnsigned(decoded[1]);
1240
- const reqs = decoded[3];
1241
- let collateralRequirements0 = 0n;
1242
- let collateralRequirements1 = 0n;
1243
- for (const packed of reqs) {
1244
- const r = decodeLeftRightUnsigned(packed);
1245
- collateralRequirements0 += r.right;
1246
- collateralRequirements1 += r.left;
1247
- }
1248
- return {
1249
- collateralRequirements0,
1250
- collateralRequirements1,
1251
- netPremia0: shortPremium.right - longPremium.right,
1252
- netPremia1: shortPremium.left - longPremium.left
1253
- };
1254
- } catch {
1255
- return null;
1256
- }
1257
- };
1258
- const prePositions = decodeFullPositions(flowResult.preCallResults?.[0]);
1259
- const postPositions = decodeFullPositions(flowResult.postCallResults?.[0]);
1260
- const prePremia = decodeFullPositions(flowResult.preCallResults?.[1]);
1261
- const postPremia = decodeFullPositions(flowResult.postCallResults?.[1]);
1262
- const _meta = await metaPromise;
1263
- const data = {
1264
- netAmount0: tokenFlow.delta0,
1265
- netAmount1: tokenFlow.delta1,
1266
- premiaReceived0: prePremia !== null && postPremia !== null ? prePremia.netPremia0 - postPremia.netPremia0 : null,
1267
- premiaReceived1: prePremia !== null && postPremia !== null ? prePremia.netPremia1 - postPremia.netPremia1 : null,
1268
- positionsCreated,
1269
- positionsClosed,
1270
- postCollateral0: tokenFlow.balanceAfter0,
1271
- postCollateral1: tokenFlow.balanceAfter1,
1272
- preMarginExcess0: prePositions === null ? null : tokenFlow.balanceBefore0 - prePositions.collateralRequirements0,
1273
- preMarginExcess1: prePositions === null ? null : tokenFlow.balanceBefore1 - prePositions.collateralRequirements1,
1274
- postMarginExcess0: postPositions === null ? null : tokenFlow.balanceAfter0 - postPositions.collateralRequirements0,
1275
- postMarginExcess1: postPositions === null ? null : tokenFlow.balanceAfter1 - postPositions.collateralRequirements1
1276
- };
1277
- return {
1278
- success: true,
1279
- data,
1280
- gasEstimate: flowResult.gasEstimate,
1281
- tokenFlow,
1282
- _meta
1283
- };
1284
- } catch (error) {
1285
- const _meta = await metaPromise;
1286
- return {
1287
- success: false,
1288
- error: error instanceof PanopticError ? error : new PanopticError(error instanceof Error ? error.message : "Simulation failed", error instanceof Error ? error : void 0),
1289
- _meta
1290
- };
1291
1779
  }
1780
+ const poolKey = await getUniswapV4PoolKeyFromId({
1781
+ client,
1782
+ sfpmAddress,
1783
+ poolId
1784
+ });
1785
+ return getPanopticPoolAddress({
1786
+ version: "v4",
1787
+ client,
1788
+ factoryAddress,
1789
+ poolKey,
1790
+ riskEngine
1791
+ });
1792
+ }
1793
+ /**
1794
+ * Resolve an SFPM poolId to its PanopticPool address without knowing the version.
1795
+ *
1796
+ * Tries both V3 and V4 lookups in parallel. The factory returns `address(0)` for
1797
+ * non-existent pools, so the non-zero result identifies the correct version.
1798
+ *
1799
+ * At least one of `v3` or `v4` must be provided.
1800
+ *
1801
+ * @throws {PanopticValidationError} If no version config is provided or neither resolves.
1802
+ */
1803
+ async function resolvePanopticPoolFromPoolId(params) {
1804
+ const { client, poolId, riskEngine, v3, v4 } = params;
1805
+ if (!v3 && !v4) throw new Error("At least one of v3 or v4 must be provided");
1806
+ const isNotFoundError = (err) => {
1807
+ if (typeof err === "object" && err !== null && "name" in err) {
1808
+ const name = err.name;
1809
+ return name === "ContractFunctionExecutionError" || name === "ContractFunctionRevertedError";
1810
+ }
1811
+ return false;
1812
+ };
1813
+ const results = await Promise.all([v3 ? getPanopticPoolFromPoolId({
1814
+ version: "v3",
1815
+ client,
1816
+ sfpmAddress: v3.sfpmAddress,
1817
+ factoryAddress: v3.factoryAddress,
1818
+ riskEngine,
1819
+ poolId
1820
+ }).catch((err) => {
1821
+ if (isNotFoundError(err)) return zeroAddress;
1822
+ throw err;
1823
+ }) : Promise.resolve(zeroAddress), v4 ? getPanopticPoolFromPoolId({
1824
+ version: "v4",
1825
+ client,
1826
+ sfpmAddress: v4.sfpmAddress,
1827
+ factoryAddress: v4.factoryAddress,
1828
+ riskEngine,
1829
+ poolId
1830
+ }).catch((err) => {
1831
+ if (isNotFoundError(err)) return zeroAddress;
1832
+ throw err;
1833
+ }) : Promise.resolve(zeroAddress)]);
1834
+ const [v3Result, v4Result] = results;
1835
+ if (v3Result !== zeroAddress) return {
1836
+ panopticPoolAddress: v3Result,
1837
+ version: "v3"
1838
+ };
1839
+ if (v4Result !== zeroAddress) return {
1840
+ panopticPoolAddress: v4Result,
1841
+ version: "v4"
1842
+ };
1843
+ throw new Error(`No PanopticPool found for poolId ${poolId}`);
1292
1844
  }
1293
1845
 
1294
1846
  //#endregion
1295
- //#region src/panoptic/v2/simulations/tokenShortfallRecovery.ts
1296
- const BPS_DENOMINATOR$2 = 10000n;
1297
- const MAX_RECOVERY_QUOTE_ATTEMPTS = 8;
1847
+ //#region src/panoptic/v2/reads/minePoolAddressLocal.ts
1298
1848
  /**
1299
- * Wrap a dispatch with a temporary credit leg that sources the shortfall.
1849
+ * keccak256 of the CREATE3 proxy initcode used by ClonesWithImmutableArgs.
1850
+ * Source: packages/panoptic-v2-core/lib/clones-with-immutable-args/src/ClonesWithImmutableArgs.sol
1851
+ */
1852
+ const CREATE3_PROXY_BYTECODE_HASH = "0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f";
1853
+ const MASK_80 = (1n << 80n) - 1n;
1854
+ const MASK_40 = (1n << 40n) - 1n;
1855
+ const MASK_96 = (1n << 96n) - 1n;
1856
+ /** Encode a BigInt as a big-endian fixed-length byte array. */
1857
+ function bigintToBytes(value, byteLength) {
1858
+ const bytes = new Uint8Array(byteLength);
1859
+ let v = value;
1860
+ for (let i = byteLength - 1; i >= 0; i--) {
1861
+ bytes[i] = Number(v & 0xffn);
1862
+ v >>= 8n;
1863
+ }
1864
+ return bytes;
1865
+ }
1866
+ /** Parse a 0x-prefixed address into 20 bytes. */
1867
+ function addressToBytes(addr) {
1868
+ const hex = addr.slice(2).padStart(40, "0");
1869
+ const bytes = new Uint8Array(20);
1870
+ for (let i = 0; i < 20; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1871
+ return bytes;
1872
+ }
1873
+ /** Parse a 0x-prefixed 32-byte hex string into bytes. */
1874
+ function hex32ToBytes(hex) {
1875
+ const h = hex.slice(2).padStart(64, "0");
1876
+ const bytes = new Uint8Array(32);
1877
+ for (let i = 0; i < 32; i++) bytes[i] = parseInt(h.slice(i * 2, i * 2 + 2), 16);
1878
+ return bytes;
1879
+ }
1880
+ /**
1881
+ * Compute the CREATE3 deployed address for a given factory and packed salt.
1300
1882
  *
1301
- * Exact-output construction: mint the credit with `swapAtMint=true` (paying a
1302
- * swapped amount of the token the account has), run the user's operations, then
1303
- * burn it with `swapAtMint=false` to receive exactly the missing token.
1883
+ * Mirrors `ClonesWithImmutableArgs.addressOfClone3(salt)` (with `address(this)` = factory):
1884
+ * proxy = CREATE2(factory, salt, PROXY_BYTECODE_HASH)
1885
+ * deployed = CREATE1(proxy, nonce=1)
1304
1886
  *
1305
- * A credit rather than a loan so the recovery is never capped by the shortfall
1306
- * token's utilization — the case that fails today on a >94% utilized tracker.
1887
+ * Returns the deployed address as a uint160 BigInt.
1307
1888
  */
1308
- function buildTokenShortfallRecoveryDispatch(params) {
1309
- return buildCreditWrappedDispatch({
1310
- ...params,
1311
- direction: "exact-out",
1312
- placement: "straddle"
1313
- });
1314
- }
1315
- function buildPrefixedExactInputRecoveryDispatch(params) {
1316
- return buildCreditWrappedDispatch({
1317
- ...params,
1318
- direction: "exact-in",
1319
- placement: "prepend"
1320
- });
1889
+ function addressOfClone3(factory, salt) {
1890
+ const saltBytes = bigintToBytes(salt, 32);
1891
+ const create2Input = new Uint8Array(85);
1892
+ create2Input[0] = 255;
1893
+ create2Input.set(addressToBytes(factory), 1);
1894
+ create2Input.set(saltBytes, 21);
1895
+ create2Input.set(hex32ToBytes(CREATE3_PROXY_BYTECODE_HASH), 53);
1896
+ const proxyHash = keccak256(create2Input, "bytes");
1897
+ const proxyAddress = proxyHash.slice(12);
1898
+ const create1Input = new Uint8Array(23);
1899
+ create1Input[0] = 214;
1900
+ create1Input[1] = 148;
1901
+ create1Input.set(proxyAddress, 2);
1902
+ create1Input[22] = 1;
1903
+ const deployedHash = keccak256(create1Input, "bytes");
1904
+ let addr = 0n;
1905
+ for (let i = 12; i < 32; i++) addr = addr << 8n | BigInt(deployedHash[i]);
1906
+ return addr;
1321
1907
  }
1322
1908
  /**
1323
- * Extract a fully-decoded `NotEnoughTokens` revert from an arbitrary error.
1909
+ * Count leading hex-zero characters in a 160-bit address value.
1324
1910
  *
1325
- * Returns `null` when the error is something else, or when only the 4-byte
1326
- * selector could be matched (the parser's fallback path constructs the error
1327
- * with undefined args, which is not actionable).
1911
+ * Mirrors `PanopticMath.numberOfLeadingHexZeros(addr)`.
1912
+ * Returns 40 for the zero address.
1328
1913
  */
1329
- function getNotEnoughTokensError(error) {
1330
- const visited = new Set();
1331
- let current = error;
1332
- while (current !== void 0 && current !== null && !visited.has(current)) {
1333
- visited.add(current);
1334
- const parsed = current instanceof NotEnoughTokensError ? current : parsePanopticError(current)?.error;
1335
- const candidate = parsed instanceof NotEnoughTokensError ? parsed : null;
1336
- if (candidate !== null) {
1337
- const args = candidate;
1338
- if (args.tokenAddress !== void 0 && args.assetsRequested !== void 0 && args.assetBalance !== void 0) return candidate;
1339
- }
1340
- current = current instanceof Error && "cause" in current ? current.cause : void 0;
1914
+ function numberOfLeadingHexZeros(addrInt) {
1915
+ if (addrInt === 0n) return 40;
1916
+ let x = addrInt;
1917
+ let r = 0;
1918
+ if (x >= 0x100000000000000000000000000000000n) {
1919
+ x >>= 128n;
1920
+ r += 32;
1341
1921
  }
1342
- return null;
1343
- }
1344
- function maximumAmountIn(estimatedAmountIn, slippageBps) {
1345
- if (slippageBps < 0n) throw new PanopticError("slippageBps must be non-negative");
1346
- return (estimatedAmountIn * (BPS_DENOMINATOR$2 + slippageBps) + BPS_DENOMINATOR$2 - 1n) / BPS_DENOMINATOR$2;
1347
- }
1348
- function ceilDiv$1(numerator, denominator) {
1349
- return (numerator + denominator - 1n) / denominator;
1350
- }
1351
- function getInputAmount(tokenFlow, tokenInIndex) {
1352
- const delta = tokenInIndex === 0n ? tokenFlow.delta0 : tokenFlow.delta1;
1353
- return delta < 0n ? -delta : delta;
1354
- }
1355
- function getBalanceBefore(tokenFlow, tokenIndex) {
1356
- return tokenIndex === 0n ? tokenFlow.balanceBefore0 : tokenFlow.balanceBefore1;
1357
- }
1358
- function getOutputAmount(tokenFlow, tokenOutIndex) {
1359
- const delta = tokenOutIndex === 0n ? tokenFlow.delta0 : tokenFlow.delta1;
1360
- return delta > 0n ? delta : 0n;
1922
+ if (x >= 0x10000000000000000n) {
1923
+ x >>= 64n;
1924
+ r += 16;
1925
+ }
1926
+ if (x >= 0x100000000n) {
1927
+ x >>= 32n;
1928
+ r += 8;
1929
+ }
1930
+ if (x >= 0x10000n) {
1931
+ x >>= 16n;
1932
+ r += 4;
1933
+ }
1934
+ if (x >= 0x100n) {
1935
+ x >>= 8n;
1936
+ r += 2;
1937
+ }
1938
+ if (x >= 0x10n) r += 1;
1939
+ return 39 - r;
1361
1940
  }
1362
- async function quoteTokenShortfallRecovery(params) {
1363
- const shortfallError = getNotEnoughTokensError(params.error);
1364
- if (shortfallError === null) return {
1365
- available: false,
1366
- reason: "not-token-shortfall"
1367
- };
1368
- let amountOut = shortfallError.assetsRequested - shortfallError.assetBalance;
1369
- if (amountOut <= 0n) return {
1370
- available: false,
1371
- reason: "invalid-shortfall",
1372
- detail: `requested=${shortfallError.assetsRequested} <= balance=${shortfallError.assetBalance}`
1373
- };
1374
- if (params.slippageBps < 0n || params.slippageBps > BPS_DENOMINATOR$2) return {
1375
- available: false,
1376
- reason: "invalid-slippage",
1377
- detail: `slippageBps=${params.slippageBps} is outside [0, ${BPS_DENOMINATOR$2}]`
1378
- };
1379
- const targetBlockNumber = params.blockNumber ?? await params.client.getBlockNumber();
1380
- const pool = await getPool({
1381
- client: params.client,
1382
- poolAddress: params.poolAddress,
1383
- chainId: params.chainId,
1384
- blockNumber: targetBlockNumber
1385
- });
1386
- const defaultTickLimits = tickLimits(pool.currentTick, params.slippageBps);
1387
- const tickLimitLow = params.tickLimitLow ?? defaultTickLimits.low;
1388
- const tickLimitHigh = params.tickLimitHigh ?? defaultTickLimits.high;
1389
- if (tickLimitLow >= tickLimitHigh) return {
1390
- available: false,
1391
- reason: "invalid-tick-limits",
1392
- detail: `tickLimitLow=${tickLimitLow} >= tickLimitHigh=${tickLimitHigh}`
1393
- };
1394
- const token0 = pool.collateralTracker0.token;
1395
- const token1 = pool.collateralTracker1.token;
1396
- const shortfallTokenIndex = (error) => {
1397
- const address = error.tokenAddress.toLowerCase();
1398
- if (address === token0.toLowerCase() || address === pool.collateralTracker0.address.toLowerCase()) return 0n;
1399
- if (address === token1.toLowerCase() || address === pool.collateralTracker1.address.toLowerCase()) return 1n;
1400
- return null;
1401
- };
1402
- const tokenOutIndex = shortfallTokenIndex(shortfallError);
1403
- if (tokenOutIndex === null) return {
1404
- available: false,
1405
- reason: "unsupported-token",
1406
- detail: `${shortfallError.tokenAddress} is neither collateral token of ${params.poolAddress}`
1407
- };
1408
- const tokenInIndex = tokenOutIndex === 0n ? 1n : 0n;
1409
- const tokenOut = tokenOutIndex === 0n ? token0 : token1;
1410
- const tokenIn = tokenInIndex === 0n ? token0 : token1;
1411
- const collisionIds = Array.from(new Set([
1412
- ...params.existingPositionIds,
1413
- ...params.dispatch.positionIdList,
1414
- ...params.dispatch.finalPositionIdList
1415
- ]));
1416
- const quotePrefixedExactInput = async (initialRequiredOutput) => {
1417
- let requiredOutput = initialRequiredOutput;
1418
- const spotInput = convertToTokenIndex(requiredOutput, tokenOutIndex, tokenInIndex, pool.sqrtPriceX96);
1419
- let creditInput = maximumAmountIn(spotInput > 0n ? spotInput : 1n, params.slippageBps);
1420
- for (let attempt = 0; attempt < MAX_RECOVERY_QUOTE_ATTEMPTS; attempt++) {
1421
- const credit = buildUniqueCredit(pool.poolId, tokenInIndex, tokenInIndex, pool.currentTick, pool.tickSpacing, collisionIds, creditInput);
1422
- const wrapArgs = {
1423
- creditTokenId: credit.tokenId,
1424
- creditPositionSize: credit.adjustedSize,
1425
- tickLimitLow,
1426
- tickLimitHigh
1427
- };
1428
- const swapDispatch = buildPrefixedExactInputRecoveryDispatch({
1429
- ...wrapArgs,
1430
- dispatch: {
1431
- positionIdList: [],
1432
- finalPositionIdList: [...params.existingPositionIds],
1433
- positionSizes: [],
1434
- tickAndSpreadLimits: [],
1435
- usePremiaAsCollateral: false,
1436
- builderCode: 0n
1437
- }
1438
- });
1439
- const swapSimulation = await simulateDispatch({
1440
- client: params.client,
1441
- poolAddress: params.poolAddress,
1442
- account: params.account,
1443
- existingPositionIdList: params.existingPositionIds,
1444
- ...swapDispatch,
1445
- blockNumber: targetBlockNumber
1446
- });
1447
- if (!swapSimulation.success || swapSimulation.tokenFlow === void 0) return {
1448
- available: false,
1449
- reason: "swap-unavailable",
1450
- detail: swapSimulation.success ? "prefixed swap simulation returned no token flow" : `prefixed swap simulation reverted: ${swapSimulation.error.message}`,
1451
- error: swapSimulation.success ? void 0 : swapSimulation.error
1452
- };
1453
- const estimatedAmountIn = getInputAmount(swapSimulation.tokenFlow, tokenInIndex);
1454
- const estimatedAmountOut = getOutputAmount(swapSimulation.tokenFlow, tokenOutIndex);
1455
- const sourceBalance = getBalanceBefore(swapSimulation.tokenFlow, tokenInIndex);
1456
- if (sourceBalance < estimatedAmountIn) return {
1457
- available: false,
1458
- reason: "swap-unavailable",
1459
- detail: `source balance ${sourceBalance} < exact input cost ${estimatedAmountIn}`,
1460
- error: new PanopticError("Insufficient source collateral for the recovery swap")
1461
- };
1462
- if (estimatedAmountOut < requiredOutput) {
1463
- creditInput = estimatedAmountOut > 0n ? ceilDiv$1(creditInput * requiredOutput, estimatedAmountOut) + 1n : creditInput * 2n;
1464
- continue;
1941
+ /**
1942
+ * Construct the bytes32 CREATE3 salt for PanopticFactoryV3.
1943
+ *
1944
+ * Mirrors:
1945
+ * bytes32(abi.encodePacked(
1946
+ * uint80(uint160(deployerAddress) >> 80), // bits [159:80] of deployer → 10 bytes
1947
+ * uint40(uint160(v3Pool) >> 120), // bits [159:120] of v3Pool → 5 bytes
1948
+ * uint40(uint160(riskEngine) >> 120), // bits [159:120] of riskEngine→ 5 bytes
1949
+ * salt // uint96 → 12 bytes
1950
+ * ))
1951
+ */
1952
+ function computeSaltPrefixV3(deployerAddress, v3Pool, riskEngine) {
1953
+ const deployer80 = BigInt(deployerAddress) >> 80n & MASK_80;
1954
+ const pool40 = BigInt(v3Pool) >> 120n & MASK_40;
1955
+ const risk40 = BigInt(riskEngine) >> 120n & MASK_40;
1956
+ return deployer80 << 176n | pool40 << 136n | risk40 << 96n;
1957
+ }
1958
+ /**
1959
+ * Compute the Uniswap V4 PoolId for a PoolKey.
1960
+ *
1961
+ * Mirrors `PoolId.toId(key)` = keccak256 of the ABI-encoded PoolKey struct
1962
+ * (5 fields × 32 bytes = 160 bytes).
1963
+ */
1964
+ function computePoolIdV4(poolKey) {
1965
+ const fee = Number(poolKey.fee);
1966
+ if (!Number.isInteger(fee) || fee < 0 || fee >= 2 ** 24) throw new PanopticValidationError(`fee out of uint24 range: ${fee}`);
1967
+ const tickSpacing = Number(poolKey.tickSpacing);
1968
+ if (!Number.isInteger(tickSpacing) || tickSpacing < -(2 ** 23) || tickSpacing > 2 ** 23 - 1) throw new PanopticValidationError(`tickSpacing out of int24 range: ${tickSpacing}`);
1969
+ const encoded = encodeAbiParameters([
1970
+ { type: "address" },
1971
+ { type: "address" },
1972
+ { type: "uint24" },
1973
+ { type: "int24" },
1974
+ { type: "address" }
1975
+ ], [
1976
+ poolKey.currency0,
1977
+ poolKey.currency1,
1978
+ Number(poolKey.fee),
1979
+ Number(poolKey.tickSpacing),
1980
+ poolKey.hooks
1981
+ ]);
1982
+ return BigInt(keccak256(encoded));
1983
+ }
1984
+ /**
1985
+ * Construct the bytes32 CREATE3 salt for PanopticFactoryV4.
1986
+ *
1987
+ * Mirrors:
1988
+ * bytes32(abi.encodePacked(
1989
+ * uint80(uint160(deployerAddress) >> 80),
1990
+ * uint40(uint256(PoolId.unwrap(key.toId())) >> 120), // bits [159:120] of poolId
1991
+ * uint40(uint160(riskEngine) >> 120),
1992
+ * salt
1993
+ * ))
1994
+ */
1995
+ function computeSaltPrefixV4(deployerAddress, poolKey, riskEngine) {
1996
+ const deployer80 = BigInt(deployerAddress) >> 80n & MASK_80;
1997
+ const poolId40 = computePoolIdV4(poolKey) >> 120n & MASK_40;
1998
+ const risk40 = BigInt(riskEngine) >> 120n & MASK_40;
1999
+ return deployer80 << 176n | poolId40 << 136n | risk40 << 96n;
2000
+ }
2001
+ /** Number of iterations per chunk before yielding back to the event loop. */
2002
+ const CHUNK_SIZE = 5000n;
2003
+ /**
2004
+ * Async version of {@link minePoolAddressLocal} that yields to the event loop
2005
+ * between chunks of iterations, preventing the browser UI from freezing.
2006
+ *
2007
+ * @param params - Mining parameters (versioned: 'v3' or 'v4'). No `client` required.
2008
+ * @returns The best salt found and its rarity (number of leading hex zeros).
2009
+ */
2010
+ async function minePoolAddressLocalAsync(params) {
2011
+ const { factoryAddress, deployerAddress, riskEngine, salt, loops, minTargetRarity } = params;
2012
+ let bestSalt = salt;
2013
+ let highestRarity = 0n;
2014
+ const maxSalt = salt + loops;
2015
+ const saltPrefix = params.version === "v3" ? computeSaltPrefixV3(deployerAddress, params.v3Pool, riskEngine) : computeSaltPrefixV4(deployerAddress, params.poolKey, riskEngine);
2016
+ let currentSalt = salt;
2017
+ while (currentSalt < maxSalt) {
2018
+ const chunkEnd = currentSalt + CHUNK_SIZE < maxSalt ? currentSalt + CHUNK_SIZE : maxSalt;
2019
+ let done = false;
2020
+ for (; currentSalt < chunkEnd; currentSalt++) {
2021
+ const newSalt = saltPrefix | currentSalt & MASK_96;
2022
+ const addrInt = addressOfClone3(factoryAddress, newSalt);
2023
+ const rarity = BigInt(numberOfLeadingHexZeros(addrInt));
2024
+ if (rarity > highestRarity) {
2025
+ highestRarity = rarity;
2026
+ bestSalt = currentSalt;
2027
+ }
2028
+ if (rarity >= minTargetRarity) {
2029
+ highestRarity = rarity;
2030
+ bestSalt = currentSalt;
2031
+ done = true;
2032
+ break;
1465
2033
  }
1466
- const recoveredDispatch = buildPrefixedExactInputRecoveryDispatch({
1467
- ...wrapArgs,
1468
- dispatch: params.dispatch
1469
- });
1470
- const recoverySimulation = await simulateDispatch({
1471
- client: params.client,
1472
- poolAddress: params.poolAddress,
1473
- account: params.account,
1474
- existingPositionIdList: params.existingPositionIds,
1475
- ...recoveredDispatch,
1476
- measurePremia: true,
1477
- blockNumber: targetBlockNumber
1478
- });
1479
- if (recoverySimulation.success && recoverySimulation.tokenFlow !== void 0) return {
1480
- available: true,
1481
- quote: {
1482
- tokenIn,
1483
- tokenOut,
1484
- direction: "exact-in",
1485
- amountOut: requiredOutput,
1486
- estimatedAmountOut,
1487
- estimatedAmountIn,
1488
- maximumAmountIn: estimatedAmountIn,
1489
- slippageBps: params.slippageBps,
1490
- netTokenInChange: tokenInIndex === 0n ? recoverySimulation.tokenFlow.delta0 : recoverySimulation.tokenFlow.delta1,
1491
- netTokenOutChange: tokenOutIndex === 0n ? recoverySimulation.tokenFlow.delta0 : recoverySimulation.tokenFlow.delta1,
1492
- creditTokenId: credit.tokenId,
1493
- dispatch: recoveredDispatch,
1494
- simulation: {
1495
- ...recoverySimulation,
1496
- tokenFlow: recoverySimulation.tokenFlow
1497
- },
1498
- tokenFlow: recoverySimulation.tokenFlow,
1499
- _meta: recoverySimulation._meta
1500
- }
1501
- };
1502
- if (recoverySimulation.success) return {
1503
- available: false,
1504
- reason: "recovery-unavailable",
1505
- detail: "prefixed recovery simulation returned no token flow",
1506
- error: new PanopticError("Recovery simulation did not return token flow")
1507
- };
1508
- const remainingShortfall = getNotEnoughTokensError(recoverySimulation.error);
1509
- if (remainingShortfall === null || shortfallTokenIndex(remainingShortfall) !== tokenOutIndex) return {
1510
- available: false,
1511
- reason: "recovery-unavailable",
1512
- detail: `prefixed recovery reverted with a non-output shortfall: ${recoverySimulation.error.message}`,
1513
- error: recoverySimulation.error
1514
- };
1515
- const residual = remainingShortfall.assetsRequested - remainingShortfall.assetBalance;
1516
- requiredOutput += residual > 0n ? residual : requiredOutput;
1517
- creditInput = ceilDiv$1(creditInput * requiredOutput, estimatedAmountOut) + 1n;
1518
- }
1519
- return {
1520
- available: false,
1521
- reason: "recovery-unavailable",
1522
- detail: `prefixed recovery remained short after ${MAX_RECOVERY_QUOTE_ATTEMPTS} attempts`,
1523
- error: new PanopticError("Could not size the prefixed recovery swap")
1524
- };
1525
- };
1526
- let creditOutSize = amountOut;
1527
- for (let attempt = 0; attempt < MAX_RECOVERY_QUOTE_ATTEMPTS; attempt++) {
1528
- const credit = buildUniqueCredit(pool.poolId, tokenOutIndex, tokenOutIndex, pool.currentTick, pool.tickSpacing, collisionIds, creditOutSize);
1529
- const recoveredDispatch = buildTokenShortfallRecoveryDispatch({
1530
- dispatch: params.dispatch,
1531
- creditTokenId: credit.tokenId,
1532
- creditPositionSize: credit.adjustedSize,
1533
- tickLimitLow,
1534
- tickLimitHigh
1535
- });
1536
- const swapDispatch = buildTokenShortfallRecoveryDispatch({
1537
- dispatch: {
1538
- positionIdList: [],
1539
- finalPositionIdList: [...params.existingPositionIds],
1540
- positionSizes: [],
1541
- tickAndSpreadLimits: [],
1542
- usePremiaAsCollateral: false,
1543
- builderCode: 0n
1544
- },
1545
- creditTokenId: credit.tokenId,
1546
- creditPositionSize: credit.adjustedSize,
1547
- tickLimitLow,
1548
- tickLimitHigh
1549
- });
1550
- const swapSimulation = await simulateDispatch({
1551
- client: params.client,
1552
- poolAddress: params.poolAddress,
1553
- account: params.account,
1554
- existingPositionIdList: params.existingPositionIds,
1555
- ...swapDispatch,
1556
- blockNumber: targetBlockNumber
1557
- });
1558
- if (!swapSimulation.success || swapSimulation.tokenFlow === void 0) {
1559
- const bootstrapShortfall = swapSimulation.success ? null : getNotEnoughTokensError(swapSimulation.error);
1560
- if (bootstrapShortfall !== null && shortfallTokenIndex(bootstrapShortfall) === tokenOutIndex) return quotePrefixedExactInput(amountOut);
1561
- return {
1562
- available: false,
1563
- reason: "swap-unavailable",
1564
- detail: swapSimulation.success ? "swap-only simulation returned no token flow" : `swap-only simulation reverted: ${swapSimulation.error.message}`,
1565
- error: swapSimulation.success ? void 0 : swapSimulation.error
1566
- };
1567
- }
1568
- const estimatedAmountIn = getInputAmount(swapSimulation.tokenFlow, tokenInIndex);
1569
- const maxAmountIn = maximumAmountIn(estimatedAmountIn, params.slippageBps);
1570
- const swapOutput = getOutputAmount(swapSimulation.tokenFlow, tokenOutIndex);
1571
- const sourceBalance = getBalanceBefore(swapSimulation.tokenFlow, tokenInIndex);
1572
- if (swapOutput < amountOut) {
1573
- creditOutSize = swapOutput > 0n ? ceilDiv$1(creditOutSize * amountOut, swapOutput) + 1n : creditOutSize * 2n;
1574
- continue;
1575
- }
1576
- if (sourceBalance < maxAmountIn) return {
1577
- available: false,
1578
- reason: "swap-unavailable",
1579
- detail: `source balance ${sourceBalance} < maximumAmountIn ${maxAmountIn} (estimated ${estimatedAmountIn}, slippageBps ${params.slippageBps})`,
1580
- error: new PanopticError("Insufficient source collateral for the recovery swap")
1581
- };
1582
- const recoverySimulation = await simulateDispatch({
1583
- client: params.client,
1584
- poolAddress: params.poolAddress,
1585
- account: params.account,
1586
- existingPositionIdList: params.existingPositionIds,
1587
- ...recoveredDispatch,
1588
- measurePremia: true,
1589
- blockNumber: targetBlockNumber
1590
- });
1591
- if (recoverySimulation.success) {
1592
- if (recoverySimulation.tokenFlow !== void 0) return {
1593
- available: true,
1594
- quote: {
1595
- tokenIn,
1596
- tokenOut,
1597
- direction: "exact-out",
1598
- amountOut,
1599
- estimatedAmountOut: swapOutput,
1600
- estimatedAmountIn,
1601
- maximumAmountIn: maxAmountIn,
1602
- slippageBps: params.slippageBps,
1603
- netTokenInChange: tokenInIndex === 0n ? recoverySimulation.tokenFlow.delta0 : recoverySimulation.tokenFlow.delta1,
1604
- netTokenOutChange: tokenOutIndex === 0n ? recoverySimulation.tokenFlow.delta0 : recoverySimulation.tokenFlow.delta1,
1605
- creditTokenId: credit.tokenId,
1606
- dispatch: recoveredDispatch,
1607
- simulation: {
1608
- ...recoverySimulation,
1609
- tokenFlow: recoverySimulation.tokenFlow
1610
- },
1611
- tokenFlow: recoverySimulation.tokenFlow,
1612
- _meta: recoverySimulation._meta
1613
- }
1614
- };
1615
- return {
1616
- available: false,
1617
- reason: "recovery-unavailable",
1618
- detail: "wrapped dispatch simulation returned no token flow",
1619
- error: new PanopticError("Recovery simulation did not return token flow")
1620
- };
1621
2034
  }
1622
- const remainingShortfall = getNotEnoughTokensError(recoverySimulation.error);
1623
- if (remainingShortfall === null || shortfallTokenIndex(remainingShortfall) !== tokenOutIndex) return {
1624
- available: false,
1625
- reason: "recovery-unavailable",
1626
- detail: `wrapped dispatch reverted with a non-shortfall error: ${recoverySimulation.error.message}`,
1627
- error: recoverySimulation.error
1628
- };
1629
- const decodedShortfall = remainingShortfall.assetsRequested - remainingShortfall.assetBalance;
1630
- const additionalAmountOut = decodedShortfall > 0n ? decodedShortfall : amountOut;
1631
- amountOut += additionalAmountOut;
1632
- creditOutSize = swapOutput > 0n ? ceilDiv$1(creditOutSize * amountOut, swapOutput) + 1n : creditOutSize * 2n;
2035
+ if (done) break;
2036
+ await new Promise((resolve) => setTimeout(resolve, 0));
1633
2037
  }
1634
2038
  return {
1635
- available: false,
1636
- reason: "recovery-unavailable",
1637
- detail: `still short after ${MAX_RECOVERY_QUOTE_ATTEMPTS} sizing attempts (last target ${amountOut})`,
1638
- error: new PanopticError("Could not cover recovery swap costs within the quote attempt limit")
2039
+ bestSalt,
2040
+ highestRarity
1639
2041
  };
1640
2042
  }
1641
2043
 
@@ -1656,7 +2058,7 @@ const MAX_EFFECTIVE_LIQUIDITY_LIMIT = 8388607;
1656
2058
  * returned value must be scaled down to the caller's actual `positionSize`.
1657
2059
  */
1658
2060
  const MAX_UINT64$1 = 2n ** 64n - 1n;
1659
- const FP96$1 = 1n << 96n;
2061
+ const FP96 = 1n << 96n;
1660
2062
  /**
1661
2063
  * `getRequiredBase` returns `type(uint128).max` as an error sentinel (invalid
1662
2064
  * tokenId or reverting `getMargin`). Detect it so we don't scale a garbage value.
@@ -1677,7 +2079,11 @@ const REQUIRED_BASE_ERROR_SENTINEL = 2n ** 128n - 1n;
1677
2079
  * @returns Estimated collateral requirements with block metadata
1678
2080
  */
1679
2081
  async function estimateCollateralRequired(params) {
1680
- const { client, poolAddress, tokenId, positionSize, atTick, queryAddress, blockNumber } = params;
2082
+ return scaleCollateralRequired(await getCollateralRequiredBase(params), params.positionSize);
2083
+ }
2084
+ /** Account- and size-independent requirement, with its valuation tick and block. */
2085
+ async function getCollateralRequiredBase(params) {
2086
+ const { client, poolAddress, tokenId, atTick, queryAddress, blockNumber } = params;
1681
2087
  const targetBlockNumber = blockNumber ?? params._meta?.blockNumber ?? await client.getBlockNumber();
1682
2088
  let effectiveTick;
1683
2089
  if (atTick !== void 0) effectiveTick = atTick;
@@ -1704,8 +2110,17 @@ async function estimateCollateralRequired(params) {
1704
2110
  client,
1705
2111
  blockNumber: targetBlockNumber
1706
2112
  })]);
2113
+ return {
2114
+ requiredBase,
2115
+ effectiveTick,
2116
+ _meta
2117
+ };
2118
+ }
2119
+ /** Preserve the contract sentinel and integer rounding when scaling a cached base. */
2120
+ function scaleCollateralRequired(base, positionSize) {
2121
+ const { requiredBase, effectiveTick, _meta } = base;
1707
2122
  const scaledRequirement = requiredBase >= REQUIRED_BASE_ERROR_SENTINEL ? requiredBase : requiredBase * positionSize / MAX_UINT64$1;
1708
- const denominatedInToken0 = tickToSqrtPriceX96(effectiveTick) < FP96$1;
2123
+ const denominatedInToken0 = tickToSqrtPriceX96(effectiveTick) < FP96;
1709
2124
  return {
1710
2125
  required0: denominatedInToken0 ? scaledRequirement : 0n,
1711
2126
  required1: denominatedInToken0 ? 0n : scaledRequirement,
@@ -1722,8 +2137,10 @@ async function estimateCollateralRequired(params) {
1722
2137
  * @returns Maximum position size with bounds and block metadata
1723
2138
  */
1724
2139
  async function getMaxPositionSize(params) {
2140
+ params.signal?.throwIfAborted();
1725
2141
  const { client, poolAddress, account, tokenId, queryAddress, existingPositionIds, storage, chainId, refine = true, precisionPct = 1, swapAtMint = false, usePremiaAsCollateral = false, blockNumber } = params;
1726
- const targetBlockNumber = blockNumber ?? params._meta?.blockNumber ?? await client.getBlockNumber();
2142
+ const targetBlockNumber = blockNumber ?? params.bounds?._meta.blockNumber ?? params._meta?.blockNumber ?? await client.getBlockNumber();
2143
+ if (params.bounds && params.bounds._meta.blockNumber !== targetBlockNumber) throw new PanopticError("MAX bounds block does not match the refinement block");
1727
2144
  let positionIds;
1728
2145
  if (existingPositionIds !== void 0) positionIds = existingPositionIds;
1729
2146
  else if (storage && chainId !== void 0) positionIds = await getTrackedPositionIds({
@@ -1733,7 +2150,7 @@ async function getMaxPositionSize(params) {
1733
2150
  storage
1734
2151
  });
1735
2152
  else positionIds = [];
1736
- const [boundsResult, _meta] = await Promise.all([client.readContract({
2153
+ const [boundsResult, _meta] = await Promise.all([params.bounds ? [params.bounds.maxSizeAtMinUtil, params.bounds.maxSizeAtMaxUtil] : client.readContract({
1737
2154
  address: queryAddress,
1738
2155
  abi: panopticQueryAbi,
1739
2156
  functionName: "getMaxPositionSizeBounds",
@@ -1744,11 +2161,12 @@ async function getMaxPositionSize(params) {
1744
2161
  tokenId
1745
2162
  ],
1746
2163
  blockNumber: targetBlockNumber
1747
- }), params._meta ?? getBlockMeta({
2164
+ }), params.bounds?._meta ?? params._meta ?? getBlockMeta({
1748
2165
  client,
1749
2166
  blockNumber: targetBlockNumber
1750
2167
  })]);
1751
2168
  const [maxSizeAtMinUtil, maxSizeAtMaxUtil] = boundsResult;
2169
+ params.signal?.throwIfAborted();
1752
2170
  const precisionDivisor = BigInt(Math.floor(100 / precisionPct));
1753
2171
  if (!refine || maxSizeAtMinUtil === maxSizeAtMaxUtil || maxSizeAtMinUtil - maxSizeAtMaxUtil <= maxSizeAtMaxUtil / precisionDivisor) return {
1754
2172
  maxSize: maxSizeAtMaxUtil,
@@ -1766,7 +2184,9 @@ async function getMaxPositionSize(params) {
1766
2184
  high: maxSizeAtMinUtil * 2n,
1767
2185
  precisionDivisor,
1768
2186
  swapAtMint,
1769
- usePremiaAsCollateral
2187
+ usePremiaAsCollateral,
2188
+ blockNumber: targetBlockNumber,
2189
+ signal: params.signal
1770
2190
  });
1771
2191
  return {
1772
2192
  maxSize,
@@ -1790,9 +2210,11 @@ async function binarySearchMaxSize(params) {
1790
2210
  existingPositionIds,
1791
2211
  positionSize,
1792
2212
  swapAtMint,
1793
- usePremiaAsCollateral
2213
+ usePremiaAsCollateral,
2214
+ blockNumber: params.blockNumber
1794
2215
  });
1795
2216
  while (high - low > 1n && high - low > low / precisionDivisor) {
2217
+ params.signal?.throwIfAborted();
1796
2218
  const range = high - low;
1797
2219
  const p1 = low + range / 6n;
1798
2220
  const p2 = low + range * 2n / 6n;
@@ -1806,6 +2228,7 @@ async function binarySearchMaxSize(params) {
1806
2228
  trySize(p4),
1807
2229
  trySize(p5)
1808
2230
  ]);
2231
+ params.signal?.throwIfAborted();
1809
2232
  if (s5) low = p5;
1810
2233
  else if (s4) {
1811
2234
  low = p4;
@@ -1856,7 +2279,8 @@ async function tryDispatchSimulation(params) {
1856
2279
  abi: panopticPoolV2Abi,
1857
2280
  functionName: "multicall",
1858
2281
  args: [[callData]],
1859
- account
2282
+ account,
2283
+ blockNumber: params.blockNumber
1860
2284
  });
1861
2285
  return true;
1862
2286
  } catch {
@@ -1977,7 +2401,7 @@ async function getItmAmounts(params) {
1977
2401
  * high-decimal amount can't masquerade as real flow.
1978
2402
  */
1979
2403
  const FLOW_NEUTRAL_DUST_THRESHOLD = 1000n;
1980
- const Q192$1 = 1n << 192n;
2404
+ const Q192$2 = 1n << 192n;
1981
2405
  const POOL_ID_MASK$1 = (1n << 64n) - 1n;
1982
2406
  /**
1983
2407
  * Floor integer square root for bigints (Newton's method). Exact for all
@@ -2013,7 +2437,7 @@ function buildNeutralLeg(tokenIndex, signedAmount, positionSize) {
2013
2437
  const absAmount = signedAmount < 0n ? -signedAmount : signedAmount;
2014
2438
  const legAsset = tokenIndex === 0n ? 1n : 0n;
2015
2439
  const legIsLong = signedAmount < 0n;
2016
- const sqrtKrawX96 = isqrt(absAmount * Q192$1 / positionSize);
2440
+ const sqrtKrawX96 = isqrt(absAmount * Q192$2 / positionSize);
2017
2441
  let signedTick;
2018
2442
  try {
2019
2443
  signedTick = sqrtPriceX96ToTick(sqrtKrawX96);
@@ -2271,7 +2695,7 @@ async function createFlowNeutralTokenId(params) {
2271
2695
  const sqrtPX96 = tickToSqrtPriceX96(tickBefore);
2272
2696
  const valueAbs = (index, amount) => {
2273
2697
  const abs$1 = amount < 0n ? -amount : amount;
2274
- return index === 0n ? abs$1 * sqrtPX96 * sqrtPX96 / Q192$1 : abs$1;
2698
+ return index === 0n ? abs$1 * sqrtPX96 * sqrtPX96 / Q192$2 : abs$1;
2275
2699
  };
2276
2700
  const flowFor = (index) => index === 0n ? credit.creditAmount0 : credit.creditAmount1;
2277
2701
  if (queryAddress !== void 0) {
@@ -2665,7 +3089,7 @@ async function estimateCollateralBreakdown(params) {
2665
3089
  */
2666
3090
  function apportion(groups, isolated, total) {
2667
3091
  if (groups.length !== isolated.length) throw new PanopticError(`apportion: groups and isolated must have equal lengths (${groups.length} !== ${isolated.length})`);
2668
- const weightSum = isolated.reduce((sum, value) => sum + (value ?? 0n), 0n);
3092
+ const weightSum = isolated.reduce((sum$1, value) => sum$1 + (value ?? 0n), 0n);
2669
3093
  if (total === null || total <= 0n || weightSum <= 0n) return groups.map((group, i) => ({
2670
3094
  ...group,
2671
3095
  isolatedRequired0: isolated[i],
@@ -3166,7 +3590,7 @@ async function getPoolLiquidities(params) {
3166
3590
 
3167
3591
  //#endregion
3168
3592
  //#region src/panoptic/v2/reads/accountGreeks.ts
3169
- const Q192 = 1n << 192n;
3593
+ const Q192$1 = 1n << 192n;
3170
3594
  /**
3171
3595
  * Calculate account-level greeks using stored position data.
3172
3596
  *
@@ -3287,8 +3711,8 @@ async function getAccountGreeks(params) {
3287
3711
  const assetBal = isAssetToken0 ? collateral.token0.assets : collateral.token1.assets;
3288
3712
  const otherBal = isAssetToken0 ? collateral.token1.assets : collateral.token0.assets;
3289
3713
  const sqrtPriceX96 = tickToSqrtPriceX96(BigInt(currentTick));
3290
- if (isAssetToken0) collateralValue = assetBal * sqrtPriceX96 * sqrtPriceX96 / Q192 + otherBal;
3291
- else collateralValue = assetBal * Q192 / (sqrtPriceX96 * sqrtPriceX96) + otherBal;
3714
+ if (isAssetToken0) collateralValue = assetBal * sqrtPriceX96 * sqrtPriceX96 / Q192$1 + otherBal;
3715
+ else collateralValue = assetBal * Q192$1 / (sqrtPriceX96 * sqrtPriceX96) + otherBal;
3292
3716
  collateralDelta = assetBal;
3293
3717
  }
3294
3718
  return {
@@ -3328,7 +3752,7 @@ function calculateAccountGreeksPure(params) {
3328
3752
  const totalDelta$1 = [];
3329
3753
  for (let i = 0; i < n; i++) {
3330
3754
  const sqrtPriceX96 = tickToSqrtPriceX96(atTicks[i]);
3331
- const assetValueInNumeraire = isAssetToken0 ? assetBal * sqrtPriceX96 * sqrtPriceX96 / Q192 : assetBal * Q192 / (sqrtPriceX96 * sqrtPriceX96);
3755
+ const assetValueInNumeraire = isAssetToken0 ? assetBal * sqrtPriceX96 * sqrtPriceX96 / Q192$1 : assetBal * Q192$1 / (sqrtPriceX96 * sqrtPriceX96);
3332
3756
  totalValue$1.push(assetValueInNumeraire + otherBal);
3333
3757
  totalDelta$1.push(assetBal);
3334
3758
  }
@@ -3378,7 +3802,7 @@ function calculateAccountGreeksPure(params) {
3378
3802
  }
3379
3803
  for (let i = 0; i < n; i++) {
3380
3804
  const sqrtPriceX96 = tickToSqrtPriceX96(atTicks[i]);
3381
- const assetValueInNumeraire = isAssetToken0 ? assetBal * sqrtPriceX96 * sqrtPriceX96 / Q192 : assetBal * Q192 / (sqrtPriceX96 * sqrtPriceX96);
3805
+ const assetValueInNumeraire = isAssetToken0 ? assetBal * sqrtPriceX96 * sqrtPriceX96 / Q192$1 : assetBal * Q192$1 / (sqrtPriceX96 * sqrtPriceX96);
3382
3806
  totalValue[i] += assetValueInNumeraire + otherBal;
3383
3807
  totalDelta[i] += assetBal;
3384
3808
  }
@@ -3391,250 +3815,6 @@ function calculateAccountGreeksPure(params) {
3391
3815
  };
3392
3816
  }
3393
3817
 
3394
- //#endregion
3395
- //#region src/panoptic/v2/reads/margin.ts
3396
- const FP96 = 1n << 96n;
3397
- const Q128$2 = 1n << 128n;
3398
- /** Cap for a usage ratio with no collateral behind it. */
3399
- const MAX_USAGE_BPS = 1000000n;
3400
- const bigintMax = (a, b) => a > b ? a : b;
3401
- const bigintMin = (a, b) => a < b ? a : b;
3402
- /**
3403
- * Convert a token0 amount to its token1-equivalent at the given sqrtPriceX96.
3404
- *
3405
- * Matches the on-chain `PanopticMath.convert0to1` truncation, with an
3406
- * overflow-safe branch when `sqrtPriceX96^2` would not fit in uint256.
3407
- */
3408
- function convert0to1$1(amount, sqrtPriceX96) {
3409
- if (sqrtPriceX96 < Q128$2) return amount * sqrtPriceX96 * sqrtPriceX96 >> 192n;
3410
- const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
3411
- return amount * sp2Hi >> 128n;
3412
- }
3413
- /**
3414
- * Convert a token1 amount to its token0-equivalent at the given sqrtPriceX96.
3415
- */
3416
- function convert1to0$1(amount, sqrtPriceX96) {
3417
- if (sqrtPriceX96 < Q128$2) {
3418
- const denom = sqrtPriceX96 * sqrtPriceX96;
3419
- return amount * (1n << 192n) / denom;
3420
- }
3421
- const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
3422
- return amount * (1n << 128n) / sp2Hi;
3423
- }
3424
- /**
3425
- * Get margin buffer and distance-to-liquidation for an account.
3426
- *
3427
- * Reads, all pinned to the same block:
3428
- * - `getCurrentTick` (sequencing dependency for sqrtPrice conversion)
3429
- * - `getFullPositionsData(account, true, tokenIds)` → collateralRequirements
3430
- * - `CollateralTracker.assetsOf(account)` on both trackers → gross collateral
3431
- * - `PanopticQuery.getLiquidationPrices(...)` → liquidation boundaries
3432
- *
3433
- * @param params - The parameters
3434
- * @returns Margin buffer with liquidation distance and block metadata
3435
- */
3436
- async function getMarginBuffer(params) {
3437
- const { client, poolAddress, account, tokenIds, queryAddress, blockNumber, mintBuffer } = params;
3438
- const targetBlockNumber = blockNumber ?? params._meta?.blockNumber ?? await client.getBlockNumber();
3439
- let collateralToken0;
3440
- let collateralToken1;
3441
- if (params.collateralAddresses) {
3442
- collateralToken0 = params.collateralAddresses.collateralToken0;
3443
- collateralToken1 = params.collateralAddresses.collateralToken1;
3444
- } else {
3445
- const addrs = await client.multicall({
3446
- contracts: [{
3447
- address: poolAddress,
3448
- abi: panopticPoolV2Abi,
3449
- functionName: "collateralToken0"
3450
- }, {
3451
- address: poolAddress,
3452
- abi: panopticPoolV2Abi,
3453
- functionName: "collateralToken1"
3454
- }],
3455
- blockNumber: targetBlockNumber,
3456
- allowFailure: false
3457
- });
3458
- collateralToken0 = addrs[0];
3459
- collateralToken1 = addrs[1];
3460
- }
3461
- const hasPositions = tokenIds.length > 0;
3462
- const calls = [
3463
- {
3464
- target: poolAddress,
3465
- callData: encodeFunctionData({
3466
- abi: panopticPoolV2Abi,
3467
- functionName: "getCurrentTick"
3468
- })
3469
- },
3470
- {
3471
- target: collateralToken0,
3472
- callData: encodeFunctionData({
3473
- abi: collateralTrackerV2Abi,
3474
- functionName: "assetsOf",
3475
- args: [account]
3476
- })
3477
- },
3478
- {
3479
- target: collateralToken1,
3480
- callData: encodeFunctionData({
3481
- abi: collateralTrackerV2Abi,
3482
- functionName: "assetsOf",
3483
- args: [account]
3484
- })
3485
- }
3486
- ];
3487
- const positionDataIndex = hasPositions ? calls.length : null;
3488
- if (positionDataIndex !== null) calls.push({
3489
- target: poolAddress,
3490
- callData: encodeFunctionData({
3491
- abi: panopticPoolV2Abi,
3492
- functionName: "getFullPositionsData",
3493
- args: [
3494
- account,
3495
- true,
3496
- tokenIds
3497
- ]
3498
- })
3499
- });
3500
- const liqPricesIndex = hasPositions ? calls.length : null;
3501
- if (liqPricesIndex !== null) calls.push({
3502
- target: queryAddress,
3503
- callData: encodeFunctionData({
3504
- abi: panopticQueryAbi,
3505
- functionName: "getLiquidationPrices",
3506
- args: [
3507
- poolAddress,
3508
- account,
3509
- tokenIds
3510
- ]
3511
- })
3512
- });
3513
- const checkCollateralIndex = hasPositions ? calls.length : null;
3514
- if (checkCollateralIndex !== null) calls.push({
3515
- target: queryAddress,
3516
- callData: encodeFunctionData({
3517
- abi: panopticQueryAbi,
3518
- functionName: "checkCollateral",
3519
- args: [
3520
- poolAddress,
3521
- account,
3522
- tokenIds
3523
- ]
3524
- })
3525
- });
3526
- const { _meta, results } = await readBlockAndAggregate({
3527
- client,
3528
- calls,
3529
- blockNumber: targetBlockNumber
3530
- });
3531
- const currentTickResult = decodeFunctionResult({
3532
- abi: panopticPoolV2Abi,
3533
- functionName: "getCurrentTick",
3534
- data: requireReturnData(results, 0, "PanopticPool.getCurrentTick")
3535
- });
3536
- const currentTick = BigInt(currentTickResult);
3537
- const assets0 = decodeFunctionResult({
3538
- abi: collateralTrackerV2Abi,
3539
- functionName: "assetsOf",
3540
- data: requireReturnData(results, 1, "CollateralTracker.assetsOf token0")
3541
- });
3542
- const assets1 = decodeFunctionResult({
3543
- abi: collateralTrackerV2Abi,
3544
- functionName: "assetsOf",
3545
- data: requireReturnData(results, 2, "CollateralTracker.assetsOf token1")
3546
- });
3547
- const positionDataResult = positionDataIndex === null ? null : decodeFunctionResult({
3548
- abi: panopticPoolV2Abi,
3549
- functionName: "getFullPositionsData",
3550
- data: requireReturnData(results, positionDataIndex, "PanopticPool.getFullPositionsData")
3551
- });
3552
- const liqPricesResult = liqPricesIndex === null ? null : decodeFunctionResult({
3553
- abi: panopticQueryAbi,
3554
- functionName: "getLiquidationPrices",
3555
- data: requireReturnData(results, liqPricesIndex, "PanopticQuery.getLiquidationPrices")
3556
- });
3557
- const checkCollateralResult = checkCollateralIndex === null ? null : decodeFunctionResult({
3558
- abi: panopticQueryAbi,
3559
- functionName: "checkCollateral",
3560
- data: requireReturnData(results, checkCollateralIndex, "PanopticQuery.checkCollateral")
3561
- });
3562
- let usageBps0 = null;
3563
- let usageBps1 = null;
3564
- let crossMarginUsageBps = null;
3565
- let mintableMarginBinding = null;
3566
- if (checkCollateralResult) {
3567
- const [balances0, requireds0, balances1, requireds1] = checkCollateralResult;
3568
- const ratio = (required, balance) => {
3569
- if (required === void 0 || balance === void 0) return null;
3570
- if (balance === 0n) return required > 0n ? MAX_USAGE_BPS : 0n;
3571
- return required * 10000n / balance;
3572
- };
3573
- usageBps0 = ratio(requireds0[0], balances0[0]);
3574
- usageBps1 = ratio(requireds1[0], balances1[0]);
3575
- if (usageBps0 !== null || usageBps1 !== null) crossMarginUsageBps = usageBps0 === null ? usageBps1 : usageBps1 === null ? usageBps0 : bigintMax(usageBps0, usageBps1);
3576
- const mintable = (balance, required) => balance === void 0 || required === void 0 ? null : mintableAfterBuffer(balance, required, mintBuffer);
3577
- const mintable0 = mintable(balances0[0], requireds0[0]);
3578
- const mintable1 = mintable(balances1[0], requireds1[0]);
3579
- if (mintable0 !== null || mintable1 !== null) mintableMarginBinding = mintable0 === null ? mintable1 : mintable1 === null ? mintable0 : bigintMin(mintable0, mintable1);
3580
- }
3581
- let required0Native = 0n;
3582
- let required1Native = 0n;
3583
- if (positionDataResult) {
3584
- const collateralRequirements = positionDataResult[3];
3585
- for (const packed of collateralRequirements) {
3586
- const decoded = decodeLeftRightUnsigned(packed);
3587
- required0Native += decoded.right;
3588
- required1Native += decoded.left;
3589
- }
3590
- }
3591
- const sqrtPriceX96 = tickToSqrtPriceX96(currentTick);
3592
- const denominatedInToken = sqrtPriceX96 < FP96 ? 0 : 1;
3593
- const currentMargin0 = assets0 + convert1to0$1(assets1, sqrtPriceX96);
3594
- const requiredMargin0 = required0Native + convert1to0$1(required1Native, sqrtPriceX96);
3595
- const currentMargin1 = assets1 + convert0to1$1(assets0, sqrtPriceX96);
3596
- const requiredMargin1 = required1Native + convert0to1$1(required0Native, sqrtPriceX96);
3597
- const buffer0 = currentMargin0 - requiredMargin0;
3598
- const buffer1 = currentMargin1 - requiredMargin1;
3599
- const bufferPercent0 = requiredMargin0 === 0n ? null : buffer0 * 10000n / requiredMargin0;
3600
- const bufferPercent1 = requiredMargin1 === 0n ? null : buffer1 * 10000n / requiredMargin1;
3601
- let lowerLiquidationTick = null;
3602
- let upperLiquidationTick = null;
3603
- let liquidationDistance = null;
3604
- if (liqPricesResult) {
3605
- const liqPriceDown = BigInt(liqPricesResult[0]);
3606
- const liqPriceUp = BigInt(liqPricesResult[1]);
3607
- lowerLiquidationTick = liqPriceDown === NO_LOWER_LIQUIDATION_TICK ? null : liqPriceDown;
3608
- upperLiquidationTick = liqPriceUp === NO_UPPER_LIQUIDATION_TICK ? null : liqPriceUp;
3609
- if (lowerLiquidationTick !== null && upperLiquidationTick !== null) {
3610
- const distLower = currentTick - lowerLiquidationTick;
3611
- const distUpper = upperLiquidationTick - currentTick;
3612
- liquidationDistance = distLower < distUpper ? distLower : distUpper;
3613
- } else if (lowerLiquidationTick !== null) liquidationDistance = currentTick - lowerLiquidationTick;
3614
- else if (upperLiquidationTick !== null) liquidationDistance = upperLiquidationTick - currentTick;
3615
- }
3616
- return {
3617
- buffer0,
3618
- buffer1,
3619
- bufferPercent0,
3620
- bufferPercent1,
3621
- currentMargin0,
3622
- currentMargin1,
3623
- requiredMargin0,
3624
- requiredMargin1,
3625
- denominatedInToken,
3626
- liquidationDistance,
3627
- lowerLiquidationTick,
3628
- upperLiquidationTick,
3629
- crossMarginUsageBps,
3630
- usageBps0,
3631
- usageBps1,
3632
- mintableMarginBinding,
3633
- currentTick,
3634
- _meta
3635
- };
3636
- }
3637
-
3638
3818
  //#endregion
3639
3819
  //#region src/panoptic/v2/reads/hedge.ts
3640
3820
  /**
@@ -3866,10 +4046,30 @@ async function getCollateralTotalAssetsBatch(client, collateralTrackerAddresses,
3866
4046
  return collateralTrackerAddresses.map(() => 0n);
3867
4047
  }
3868
4048
  }
3869
-
3870
- //#endregion
3871
- //#region src/panoptic/v2/reads/buyingPower.ts
3872
- const Q128$1 = 1n << 128n;
4049
+
4050
+ //#endregion
4051
+ //#region src/panoptic/v2/reads/buyingPower.ts
4052
+ const Q128$1 = 1n << 128n;
4053
+ /** Immutable collateral tracker addresses, suitable for a pool-scoped cache. */
4054
+ async function getPoolCollateralAddresses(params) {
4055
+ const [collateralToken0, collateralToken1] = await multicall(params.client, {
4056
+ contracts: [{
4057
+ address: params.poolAddress,
4058
+ abi: panopticPoolV2Abi,
4059
+ functionName: "collateralToken0"
4060
+ }, {
4061
+ address: params.poolAddress,
4062
+ abi: panopticPoolV2Abi,
4063
+ functionName: "collateralToken1"
4064
+ }],
4065
+ blockNumber: params.blockNumber,
4066
+ allowFailure: false
4067
+ });
4068
+ return {
4069
+ collateralToken0,
4070
+ collateralToken1
4071
+ };
4072
+ }
3873
4073
  function convert0to1(amount, sqrtPriceX96) {
3874
4074
  if (sqrtPriceX96 < Q128$1) return amount * sqrtPriceX96 * sqrtPriceX96 >> 192n;
3875
4075
  const sp2Hi = sqrtPriceX96 * sqrtPriceX96 >> 64n;
@@ -3906,21 +4106,13 @@ async function getAccountBuyingPower(params) {
3906
4106
  collateralToken0 = params.collateralAddresses.collateralToken0;
3907
4107
  collateralToken1 = params.collateralAddresses.collateralToken1;
3908
4108
  } else {
3909
- const addrs = await multicall(client, {
3910
- contracts: [{
3911
- address: poolAddress,
3912
- abi: panopticPoolV2Abi,
3913
- functionName: "collateralToken0"
3914
- }, {
3915
- address: poolAddress,
3916
- abi: panopticPoolV2Abi,
3917
- functionName: "collateralToken1"
3918
- }],
3919
- blockNumber,
3920
- allowFailure: false
4109
+ const addresses = await getPoolCollateralAddresses({
4110
+ client,
4111
+ poolAddress,
4112
+ blockNumber
3921
4113
  });
3922
- collateralToken0 = addrs[0];
3923
- collateralToken1 = addrs[1];
4114
+ collateralToken0 = addresses.collateralToken0;
4115
+ collateralToken1 = addresses.collateralToken1;
3924
4116
  }
3925
4117
  const calls = [
3926
4118
  {
@@ -4024,14 +4216,16 @@ async function getAccountBuyingPower(params) {
4024
4216
  */
4025
4217
  async function getOpenPositionPreview(params) {
4026
4218
  const { client, poolAddress, account, existingPositionIds, tokenId, positionSize, queryAddress, tickLimitLow, tickLimitHigh, spreadLimit, swapAtMint, usePremiaAsCollateral, chainId, blockNumber } = params;
4027
- const [currentBuyingPower, simulation] = await Promise.all([getAccountBuyingPower({
4219
+ const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
4220
+ const [currentBuyingPower, simulation] = await Promise.all([params.buyingPower ?? getAccountBuyingPower({
4028
4221
  client,
4029
4222
  poolAddress,
4030
4223
  account,
4031
4224
  tokenIds: existingPositionIds,
4032
4225
  queryAddress,
4033
- blockNumber
4226
+ blockNumber: targetBlockNumber
4034
4227
  }), simulateOpenPosition({
4228
+ estimateGas: params.estimateGas,
4035
4229
  client,
4036
4230
  poolAddress,
4037
4231
  account,
@@ -4044,8 +4238,9 @@ async function getOpenPositionPreview(params) {
4044
4238
  swapAtMint,
4045
4239
  usePremiaAsCollateral,
4046
4240
  chainId,
4047
- blockNumber
4241
+ blockNumber: targetBlockNumber
4048
4242
  })]);
4243
+ if (currentBuyingPower._meta.blockNumber !== targetBlockNumber) throw new PanopticError("Buying power snapshot block does not match the preview block");
4049
4244
  let isSolvent = true;
4050
4245
  let tokenShortfall = null;
4051
4246
  if (!simulation.success) {
@@ -5329,6 +5524,370 @@ async function processClosedPositions(client, queryAddress, positions) {
5329
5524
  return entries;
5330
5525
  }
5331
5526
 
5527
+ //#endregion
5528
+ //#region src/panoptic/v2/reads/hedgeCapacity.ts
5529
+ const SCALE$1 = 10000000n;
5530
+ const FEE_SCALE = 10000n;
5531
+ const Q192 = 1n << 192n;
5532
+ const MASK128$1 = (1n << 128n) - 1n;
5533
+ const ceilDiv$1 = (value, divisor) => (value + divisor - 1n) / divisor;
5534
+ function accountHedgeDelta({ positionIds, balances, collateralDelta, tick, tickSpacing, assetIndex }) {
5535
+ return positionIds.reduce((sum$1, id, index) => {
5536
+ const balance = balances[index];
5537
+ if (balance === void 0) throw new Error("Missing position balance");
5538
+ const { positionSize, tickAtMint } = decodePositionBalance(balance);
5539
+ return sum$1 + calculatePositionDeltaDebtOnly({
5540
+ legs: decodeTokenId(id).legs,
5541
+ positionSize,
5542
+ mintTick: tickAtMint,
5543
+ currentTick: tick,
5544
+ poolTickSpacing: tickSpacing,
5545
+ assetIndex
5546
+ });
5547
+ }, collateralDelta);
5548
+ }
5549
+ /** A separate loan leaves existing option legs and their risk partners unchanged. */
5550
+ function buildCapacityHedge({ delta, assetIndex, tick, tickSpacing, poolId, notionalFee, swapFee }) {
5551
+ const borrowToken = delta >= 0n ? assetIndex : assetIndex === 0n ? 1n : 0n;
5552
+ const receiveToken = borrowToken === 0n ? 1n : 0n;
5553
+ const amount = delta >= 0n ? delta : ceilDiv$1((toVaultFrameAtTick(-delta, assetIndex, borrowToken, tick) + 1n) * 1000000n * FEE_SCALE, (1000000n - swapFee) * (FEE_SCALE - notionalFee));
5554
+ const commission = ceilDiv$1(amount * notionalFee, FEE_SCALE);
5555
+ const proceeds = toVaultFrameAtTick((amount - commission) * (1000000n - swapFee) / 1000000n, borrowToken, receiveToken, tick);
5556
+ const tokenId = addLegToTokenId(poolId, {
5557
+ index: 0n,
5558
+ asset: borrowToken,
5559
+ tokenType: borrowToken,
5560
+ width: 0n,
5561
+ strike: tick / tickSpacing * tickSpacing,
5562
+ optionRatio: 1n,
5563
+ riskPartner: 0n,
5564
+ isLong: 0n
5565
+ });
5566
+ return {
5567
+ tokenId,
5568
+ amount,
5569
+ borrowToken,
5570
+ receiveToken,
5571
+ commission,
5572
+ proceeds
5573
+ };
5574
+ }
5575
+ /** Both cross-collateral constraints, with conversion and mint-buffer rounding matching RiskEngine. */
5576
+ function hedgeMargin({ tokenData, balanceChanges, crossRatios, tick, buffer }) {
5577
+ const balances = tokenData.map((data, i) => (data & MASK128$1) + balanceChanges[i]);
5578
+ const [required0, required1] = applyMintBufferPerToken(tokenData[0] >> 128n, tokenData[1] >> 128n, {
5579
+ numerator: buffer,
5580
+ denominator: SCALE$1
5581
+ });
5582
+ const surplus0 = balances[0] > required0 ? (balances[0] - required0) * crossRatios[0] / SCALE$1 : 0n;
5583
+ const surplus1 = balances[1] > required1 ? (balances[1] - required1) * crossRatios[1] / SCALE$1 : 0n;
5584
+ const sqrt = tickToSqrtPriceX96(tick);
5585
+ const price = sqrt * sqrt;
5586
+ const pairs = sqrt < 1n << 96n ? [[balances[0] + surplus1 * Q192 / price, required0], [balances[1] * Q192 / price + surplus0, ceilDiv$1(required1 * Q192, price)]] : [[balances[0] * price / Q192 + surplus1, ceilDiv$1(required0 * price, Q192)], [balances[1] + surplus0 * price / Q192, required1]];
5587
+ const headroom = pairs.map(([balance, required]) => balance - required);
5588
+ return {
5589
+ solvent: balances.every((balance) => balance >= 0n) && headroom.every((value) => value >= 0n),
5590
+ headroom: headroom[0] < headroom[1] ? headroom[0] : headroom[1],
5591
+ denomination: sqrt < 1n << 96n ? 0n : 1n
5592
+ };
5593
+ }
5594
+ async function findHedgeBoundary(current, samples, direction, evaluate) {
5595
+ if (!current.affordable) return current.tick;
5596
+ const ordered = samples.filter((point) => direction * (point.tick - current.tick) > 0).sort((a, b) => direction * (a.tick - b.tick));
5597
+ let affordableTick = current.tick;
5598
+ for (const point of ordered) {
5599
+ if (point.affordable) {
5600
+ affordableTick = point.tick;
5601
+ continue;
5602
+ }
5603
+ let failedTick = point.tick;
5604
+ while (Math.abs(failedTick - affordableTick) > 1) {
5605
+ const middle = Math.floor((failedTick + affordableTick) / 2);
5606
+ const result = await evaluate(middle);
5607
+ if (result.affordable) affordableTick = middle;
5608
+ else failedTick = middle;
5609
+ }
5610
+ return failedTick;
5611
+ }
5612
+ return null;
5613
+ }
5614
+
5615
+ //#endregion
5616
+ //#region src/panoptic/v2/reads/hedgeLimits.ts
5617
+ const SCALE = 10000000n;
5618
+ const MAX_SIZE = (1n << 127n) - 1n;
5619
+ const MASK128 = (1n << 128n) - 1n;
5620
+ const RANGE = 12800;
5621
+ const STEP = 400;
5622
+ const SAMPLE_BATCH_SIZE = 32;
5623
+ /** Snapshot estimate; prices, balances and requirements share one block, with spot swaps excluding price impact. */
5624
+ async function getHedgeLimits({ client, poolAddress, account, positionIds, assetIndex, blockNumber: requestedBlock }) {
5625
+ if (positionIds.length === 0 || positionIds.length > 26 || new Set(positionIds).size !== positionIds.length) throw new Error("Hedge limits require a complete, nonempty list of open positions");
5626
+ const blockNumber = requestedBlock ?? await client.getBlockNumber();
5627
+ const metadata = await getPoolMetadata({
5628
+ client,
5629
+ poolAddress
5630
+ });
5631
+ const { collateralToken0Address: ct0, collateralToken1Address: ct1, riskEngineAddress: engine } = metadata;
5632
+ if (metadata.fee >= 1000000n) throw new Error("A fixed swap fee is required for this estimate");
5633
+ const [full, currentTick, pool0, pool1, total0, total1, interest0, interest1, buffer, notionalFee, cross0, cross1, maxLegs, accountLegs, supply0, supply1] = await client.multicall({
5634
+ contracts: [
5635
+ {
5636
+ address: poolAddress,
5637
+ abi: panopticPoolV2Abi,
5638
+ functionName: "getFullPositionsData",
5639
+ args: [
5640
+ account,
5641
+ false,
5642
+ positionIds
5643
+ ]
5644
+ },
5645
+ {
5646
+ address: poolAddress,
5647
+ abi: panopticPoolV2Abi,
5648
+ functionName: "getCurrentTick"
5649
+ },
5650
+ {
5651
+ address: ct0,
5652
+ abi: collateralTrackerV2Abi,
5653
+ functionName: "getPoolData"
5654
+ },
5655
+ {
5656
+ address: ct1,
5657
+ abi: collateralTrackerV2Abi,
5658
+ functionName: "getPoolData"
5659
+ },
5660
+ {
5661
+ address: ct0,
5662
+ abi: collateralTrackerV2Abi,
5663
+ functionName: "totalAssets"
5664
+ },
5665
+ {
5666
+ address: ct1,
5667
+ abi: collateralTrackerV2Abi,
5668
+ functionName: "totalAssets"
5669
+ },
5670
+ {
5671
+ address: ct0,
5672
+ abi: collateralTrackerV2Abi,
5673
+ functionName: "assetsAndInterest",
5674
+ args: [account]
5675
+ },
5676
+ {
5677
+ address: ct1,
5678
+ abi: collateralTrackerV2Abi,
5679
+ functionName: "assetsAndInterest",
5680
+ args: [account]
5681
+ },
5682
+ {
5683
+ address: engine,
5684
+ abi: riskEngineAbi,
5685
+ functionName: "BP_DECREASE_BUFFER"
5686
+ },
5687
+ {
5688
+ address: engine,
5689
+ abi: riskEngineAbi,
5690
+ functionName: "NOTIONAL_FEE"
5691
+ },
5692
+ {
5693
+ address: engine,
5694
+ abi: riskEngineAbi,
5695
+ functionName: "CROSS_BUFFER_0"
5696
+ },
5697
+ {
5698
+ address: engine,
5699
+ abi: riskEngineAbi,
5700
+ functionName: "CROSS_BUFFER_1"
5701
+ },
5702
+ {
5703
+ address: engine,
5704
+ abi: riskEngineAbi,
5705
+ functionName: "MAX_OPEN_LEGS"
5706
+ },
5707
+ {
5708
+ address: poolAddress,
5709
+ abi: panopticPoolV2Abi,
5710
+ functionName: "numberOfLegs",
5711
+ args: [account]
5712
+ },
5713
+ {
5714
+ address: ct0,
5715
+ abi: collateralTrackerV2Abi,
5716
+ functionName: "totalSupply"
5717
+ },
5718
+ {
5719
+ address: ct1,
5720
+ abi: collateralTrackerV2Abi,
5721
+ functionName: "totalSupply"
5722
+ }
5723
+ ],
5724
+ allowFailure: false,
5725
+ blockNumber
5726
+ });
5727
+ const [shortPremium, longPremium, balances] = full;
5728
+ if (balances.length !== positionIds.length || balances.some((value) => (value & MASK128) === 0n)) throw new Error("Position snapshot is incomplete; refresh positions and retry");
5729
+ const decoded = positionIds.map((id) => decodeTokenId(id).legs);
5730
+ const legCount = decoded.reduce((sum$1, legs) => sum$1 + legs.length, 0);
5731
+ if (BigInt(legCount) !== accountLegs) throw new Error("The account position list is incomplete");
5732
+ const borrowCapacity = [availableToBorrow(pool0, supply0), availableToBorrow(pool1, supply1)];
5733
+ const currentUtilizations = [pool0[3], pool1[3]];
5734
+ const globalUtilizations = balances.reduce((result, value) => {
5735
+ const decodedBalance = decodePositionBalance(value);
5736
+ return [result[0] > decodedBalance.poolUtilization0 ? result[0] : decodedBalance.poolUtilization0, result[1] > decodedBalance.poolUtilization1 ? result[1] : decodedBalance.poolUtilization1];
5737
+ }, [0n, 0n]);
5738
+ const [baseCross0, baseCross1] = await client.multicall({
5739
+ contracts: [{
5740
+ address: engine,
5741
+ abi: riskEngineAbi,
5742
+ functionName: "crossBufferRatio",
5743
+ args: [globalUtilizations[0], cross0]
5744
+ }, {
5745
+ address: engine,
5746
+ abi: riskEngineAbi,
5747
+ functionName: "crossBufferRatio",
5748
+ args: [globalUtilizations[1], cross1]
5749
+ }],
5750
+ allowFailure: false,
5751
+ blockNumber
5752
+ });
5753
+ const [assets, interest] = assetIndex === 0n ? interest0 : interest1;
5754
+ const premiumShift = assetIndex * 128n;
5755
+ const collateralDelta = (assets > interest ? assets - interest : 0n) + (shortPremium >> premiumShift & MASK128) - (longPremium >> premiumShift & MASK128);
5756
+ const marginCall = (tick, ids, positionBalances) => ({
5757
+ address: engine,
5758
+ abi: riskEngineAbi,
5759
+ functionName: "getMargin",
5760
+ args: [
5761
+ positionBalances,
5762
+ tick,
5763
+ account,
5764
+ ids,
5765
+ shortPremium,
5766
+ longPremium,
5767
+ ct0,
5768
+ ct1
5769
+ ]
5770
+ });
5771
+ const evaluate = async (tick) => {
5772
+ const delta = accountHedgeDelta({
5773
+ positionIds,
5774
+ balances,
5775
+ collateralDelta,
5776
+ tick: BigInt(tick),
5777
+ tickSpacing: metadata.tickSpacing,
5778
+ assetIndex
5779
+ });
5780
+ const hedge = buildCapacityHedge({
5781
+ delta,
5782
+ assetIndex,
5783
+ tick: BigInt(tick),
5784
+ tickSpacing: metadata.tickSpacing,
5785
+ poolId: metadata.poolId,
5786
+ notionalFee: BigInt(notionalFee),
5787
+ swapFee: metadata.fee
5788
+ });
5789
+ const unavailable = (reason) => ({
5790
+ tick,
5791
+ affordable: false,
5792
+ reason,
5793
+ delta,
5794
+ headroom: null
5795
+ });
5796
+ if (hedge.amount > 0n && BigInt(legCount) >= maxLegs) return unavailable("leg-limit");
5797
+ const totalAssets = hedge.borrowToken === 0n ? total0 : total1;
5798
+ if (hedge.amount > 0n && (hedge.amount > MAX_SIZE || hedge.amount > borrowCapacity[Number(hedge.borrowToken)] || totalAssets === 0n)) return unavailable("capacity");
5799
+ const projectedUtilization = currentUtilizations.map((util, i) => hedge.amount > 0n && BigInt(i) === hedge.borrowToken ? ceilDiv$1(util * totalAssets + hedge.amount * 10000n, totalAssets) : util);
5800
+ if (hedge.amount > 0n && projectedUtilization.some((util) => util > 10000n)) return unavailable("capacity");
5801
+ const global = projectedUtilization.map((util, i) => util > globalUtilizations[i] ? util : globalUtilizations[i]);
5802
+ const syntheticBalance = hedge.amount | projectedUtilization[0] << 128n | projectedUtilization[1] << 144n;
5803
+ const hedgedIds = hedge.amount === 0n ? positionIds : [...positionIds, hedge.tokenId];
5804
+ const hedgedBalances = hedge.amount === 0n ? balances : [...balances, syntheticBalance];
5805
+ const [base, after, ratio0, ratio1] = await client.multicall({
5806
+ contracts: [
5807
+ marginCall(tick, positionIds, balances),
5808
+ marginCall(tick, hedgedIds, hedgedBalances),
5809
+ {
5810
+ address: engine,
5811
+ abi: riskEngineAbi,
5812
+ functionName: "crossBufferRatio",
5813
+ args: [global[0], cross0]
5814
+ },
5815
+ {
5816
+ address: engine,
5817
+ abi: riskEngineAbi,
5818
+ functionName: "crossBufferRatio",
5819
+ args: [global[1], cross1]
5820
+ }
5821
+ ],
5822
+ allowFailure: false,
5823
+ blockNumber
5824
+ });
5825
+ const before = hedgeMargin({
5826
+ tokenData: [base[0], base[1]],
5827
+ balanceChanges: [0n, 0n],
5828
+ crossRatios: [baseCross0, baseCross1],
5829
+ tick: BigInt(tick),
5830
+ buffer: SCALE
5831
+ });
5832
+ const balanceChanges = hedge.borrowToken === 0n ? [0n, hedge.proceeds] : [hedge.proceeds, 0n];
5833
+ const margin = hedgeMargin({
5834
+ tokenData: [after[0], after[1]],
5835
+ balanceChanges,
5836
+ crossRatios: hedge.amount === 0n ? [baseCross0, baseCross1] : [ratio0, ratio1],
5837
+ tick: BigInt(tick),
5838
+ buffer: hedge.amount === 0n ? SCALE : BigInt(buffer)
5839
+ });
5840
+ return {
5841
+ tick,
5842
+ affordable: before.solvent && margin.solvent,
5843
+ reason: !before.solvent ? "insolvent" : !margin.solvent ? "margin" : "available",
5844
+ delta,
5845
+ headroom: toVaultFrameAtTick(margin.headroom, margin.denomination, assetIndex === 0n ? 1n : 0n, BigInt(tick))
5846
+ };
5847
+ };
5848
+ const current = await evaluate(currentTick);
5849
+ const minTick = Math.max(-887200, currentTick - RANGE);
5850
+ const maxTick = Math.min(887200, currentTick + RANGE);
5851
+ if (!current.affordable) return {
5852
+ current,
5853
+ lowerTick: currentTick,
5854
+ upperTick: currentTick,
5855
+ minTick,
5856
+ maxTick,
5857
+ blockNumber
5858
+ };
5859
+ const ticks = new Set([minTick, maxTick]);
5860
+ for (let tick = minTick; tick <= maxTick; tick += STEP) ticks.add(tick);
5861
+ for (const leg of decoded.flat()) {
5862
+ const halfWidth = leg.width * metadata.tickSpacing / 2n;
5863
+ for (const tick of [
5864
+ leg.strike - halfWidth,
5865
+ leg.strike,
5866
+ leg.strike + halfWidth
5867
+ ]) for (const offset of [
5868
+ -1,
5869
+ 0,
5870
+ 1
5871
+ ]) {
5872
+ const value = Number(tick) + offset;
5873
+ if (value >= minTick && value <= maxTick) ticks.add(value);
5874
+ }
5875
+ }
5876
+ ticks.delete(currentTick);
5877
+ const samples = [];
5878
+ const sortedTicks = [...ticks].sort((a, b) => a - b);
5879
+ for (let i = 0; i < sortedTicks.length; i += SAMPLE_BATCH_SIZE) samples.push(...await Promise.all(sortedTicks.slice(i, i + SAMPLE_BATCH_SIZE).map(evaluate)));
5880
+ const [lowerTick, upperTick] = await Promise.all([findHedgeBoundary(current, samples, -1, evaluate), findHedgeBoundary(current, samples, 1, evaluate)]);
5881
+ return {
5882
+ current,
5883
+ lowerTick,
5884
+ upperTick,
5885
+ minTick,
5886
+ maxTick,
5887
+ blockNumber
5888
+ };
5889
+ }
5890
+
5332
5891
  //#endregion
5333
5892
  //#region src/panoptic/v2/sync/getSyncStatus.ts
5334
5893
  /**
@@ -5879,6 +6438,154 @@ async function cleanupStalePendingPositions(params, currentBlock, maxAgeBlocks =
5879
6438
  else if (remaining.length !== pending.length) await storage.set(key, jsonSerializer.stringify(remaining));
5880
6439
  }
5881
6440
 
6441
+ //#endregion
6442
+ //#region src/panoptic/v2/liquidator/index.ts
6443
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
6444
+ /**
6445
+ * A LiquidateParams skeleton with every optional route zeroed: no flash loan,
6446
+ * no swaps, no native funding, zero floors. Spread and override.
6447
+ */
6448
+ function emptyLiquidateParams(pool, account, positionIdListTo) {
6449
+ return {
6450
+ pool,
6451
+ account,
6452
+ positionIdListTo,
6453
+ usePremiaAsCollateral: 0n,
6454
+ flashToken: ZERO_ADDRESS,
6455
+ flashAmount: 0n,
6456
+ nativeFundingAmount: 0n,
6457
+ preSwapTarget: ZERO_ADDRESS,
6458
+ preSwapCallData: "0x",
6459
+ preSwapTokenIn: ZERO_ADDRESS,
6460
+ preSwapAmountIn: 0n,
6461
+ swapTarget: ZERO_ADDRESS,
6462
+ swapCallData: "0x",
6463
+ swapTokenIn: ZERO_ADDRESS,
6464
+ swapAmountIn: 0n,
6465
+ minDelta0: 0n,
6466
+ minDelta1: 0n
6467
+ };
6468
+ }
6469
+ /**
6470
+ * Reproduce the pool's liquidation gate exactly, pinned to one block.
6471
+ *
6472
+ * `dispatchFrom` checks solvency at `[spotTick, twapTick, latestTick,
6473
+ * currentTick]` — note `twapTick` (riskEngine EMA via `getTWAP()`)
6474
+ * deliberately REPLACES the `medianTick` that `getOracleTicks()` returns.
6475
+ * Never gate liquidatability on the 3-arg `checkCollateral` overload: it
6476
+ * iterates `getOracleTicks()` (which includes medianTick and omits twapTick)
6477
+ * and disagrees with the pool at the margin.
6478
+ *
6479
+ * Two phases, both pinned to the same block: (1) read the oracle ticks and
6480
+ * TWAP, (2) `isAccountSolvent` at each of the four gate ticks.
6481
+ */
6482
+ async function screenAccountExact(params) {
6483
+ const { client, poolAddress, queryAddress, account, tokenIds } = params;
6484
+ const tickCalls = [{
6485
+ target: poolAddress,
6486
+ callData: encodeFunctionData({
6487
+ abi: panopticPoolV2Abi,
6488
+ functionName: "getOracleTicks"
6489
+ })
6490
+ }, {
6491
+ target: poolAddress,
6492
+ callData: encodeFunctionData({
6493
+ abi: panopticPoolV2Abi,
6494
+ functionName: "getTWAP"
6495
+ })
6496
+ }];
6497
+ const phase1 = await readBlockAndAggregate({
6498
+ client,
6499
+ calls: tickCalls,
6500
+ blockNumber: params.blockNumber
6501
+ });
6502
+ const oracle = decodeFunctionResult({
6503
+ abi: panopticPoolV2Abi,
6504
+ functionName: "getOracleTicks",
6505
+ data: requireReturnData(phase1.results, 0, "PanopticPool.getOracleTicks")
6506
+ });
6507
+ const twapTick = BigInt(decodeFunctionResult({
6508
+ abi: panopticPoolV2Abi,
6509
+ functionName: "getTWAP",
6510
+ data: requireReturnData(phase1.results, 1, "PanopticPool.getTWAP")
6511
+ }));
6512
+ const ticks = {
6513
+ currentTick: BigInt(oracle[0]),
6514
+ spotTick: BigInt(oracle[1]),
6515
+ latestTick: BigInt(oracle[3]),
6516
+ twapTick
6517
+ };
6518
+ const gateTicks = [
6519
+ ticks.spotTick,
6520
+ ticks.twapTick,
6521
+ ticks.latestTick,
6522
+ ticks.currentTick
6523
+ ];
6524
+ const solvencyCalls = gateTicks.map((atTick) => ({
6525
+ target: queryAddress,
6526
+ callData: encodeFunctionData({
6527
+ abi: panopticQueryAbi$1,
6528
+ functionName: "isAccountSolvent",
6529
+ args: [
6530
+ poolAddress,
6531
+ account,
6532
+ tokenIds,
6533
+ Number(atTick)
6534
+ ]
6535
+ })
6536
+ }));
6537
+ const phase2 = await readBlockAndAggregate({
6538
+ client,
6539
+ calls: solvencyCalls,
6540
+ blockNumber: phase1._meta.blockNumber
6541
+ });
6542
+ if (phase2._meta.blockHash !== phase1._meta.blockHash) throw new PanopticError(`screenAccountExact: block ${phase1._meta.blockNumber} hash changed between phases (${phase1._meta.blockHash} -> ${phase2._meta.blockHash}); reorg mid-screen — retry`);
6543
+ const solventAt = gateTicks.map((_, i) => decodeFunctionResult({
6544
+ abi: panopticQueryAbi$1,
6545
+ functionName: "isAccountSolvent",
6546
+ data: requireReturnData(phase2.results, i, `PanopticQuery.isAccountSolvent[${i}]`)
6547
+ }));
6548
+ return {
6549
+ isLiquidatable: solventAt.every((solvent) => !solvent),
6550
+ solventAt,
6551
+ ticks,
6552
+ _meta: phase2._meta
6553
+ };
6554
+ }
6555
+ /**
6556
+ * Run `PanopticLiquidator.quoteLiquidation` via `eth_call` (it is
6557
+ * state-mutating by design and must never be mined).
6558
+ */
6559
+ async function quoteLiquidation(params) {
6560
+ const { client, liquidatorAddress, owner, stateOverride, value, blockNumber } = params;
6561
+ const response = await client.call({
6562
+ account: owner,
6563
+ to: liquidatorAddress,
6564
+ data: encodeFunctionData({
6565
+ abi: panopticLiquidatorAbi,
6566
+ functionName: "quoteLiquidation",
6567
+ args: [params.params]
6568
+ }),
6569
+ value,
6570
+ stateOverride,
6571
+ blockNumber
6572
+ });
6573
+ if (response.data === void 0) throw new PanopticError("quoteLiquidation eth_call returned no data");
6574
+ const [bonus0, bonus1, shortfall0, shortfall1, protocolLoss0, protocolLoss1] = decodeFunctionResult({
6575
+ abi: panopticLiquidatorAbi,
6576
+ functionName: "quoteLiquidation",
6577
+ data: response.data
6578
+ });
6579
+ return {
6580
+ bonus0,
6581
+ bonus1,
6582
+ shortfall0,
6583
+ shortfall1,
6584
+ protocolLoss0,
6585
+ protocolLoss1
6586
+ };
6587
+ }
6588
+
5882
6589
  //#endregion
5883
6590
  //#region src/panoptic/v2/simulations/simulateBatchDispatch.ts
5884
6591
  /**
@@ -6670,9 +7377,6 @@ const DEFAULT_MIN_SWAP_RATIO_BPS = 50n;
6670
7377
  function deltaAt(tokenFlow, index) {
6671
7378
  return index === 0n ? tokenFlow.delta0 : tokenFlow.delta1;
6672
7379
  }
6673
- function balanceBeforeAt(tokenFlow, index) {
6674
- return index === 0n ? tokenFlow.balanceBefore0 : tokenFlow.balanceBefore1;
6675
- }
6676
7380
  function abs(value) {
6677
7381
  return value < 0n ? -value : value;
6678
7382
  }
@@ -6731,7 +7435,8 @@ async function quoteOneTokenFlow(params) {
6731
7435
  const otherTracker = otherTokenIndex === 0n ? pool.collateralTracker0.address : pool.collateralTracker1.address;
6732
7436
  const baseSimulation = await simulateDispatch({
6733
7437
  ...simulateArgs,
6734
- ...params.dispatch
7438
+ ...params.dispatch,
7439
+ settleSequence: params.settleSequence
6735
7440
  });
6736
7441
  let swapAmount;
6737
7442
  let direction;
@@ -6773,6 +7478,7 @@ async function quoteOneTokenFlow(params) {
6773
7478
  chainId: params.chainId,
6774
7479
  existingPositionIds: params.existingPositionIds,
6775
7480
  dispatch: params.dispatch,
7481
+ settleSequence: params.settleSequence,
6776
7482
  error: shortfall,
6777
7483
  slippageBps: params.slippageBps,
6778
7484
  tickLimitLow: params.tickLimitLow,
@@ -6855,13 +7561,6 @@ async function quoteOneTokenFlow(params) {
6855
7561
  };
6856
7562
  estimatedCounterAmount = abs(deltaAt(swapSimulation.tokenFlow, targetTokenIndex));
6857
7563
  maximumAmountIn$1 = padForSlippage(estimatedCounterAmount, params.slippageBps);
6858
- const sourceBalance = balanceBeforeAt(swapSimulation.tokenFlow, targetTokenIndex);
6859
- if (sourceBalance < maximumAmountIn$1) return {
6860
- available: false,
6861
- reason: "swap-unavailable",
6862
- detail: `source balance ${sourceBalance} < maximumAmountIn ${maximumAmountIn$1} (estimated ${estimatedCounterAmount}, slippageBps ${params.slippageBps})`,
6863
- error: new PanopticError("Insufficient collateral to fund the one-token-out swap")
6864
- };
6865
7564
  }
6866
7565
  const wrappedDispatch = buildCreditWrappedDispatch({
6867
7566
  ...creditWrapArgs,
@@ -6869,7 +7568,8 @@ async function quoteOneTokenFlow(params) {
6869
7568
  });
6870
7569
  const wrappedSimulation = await simulateDispatch({
6871
7570
  ...simulateArgs,
6872
- ...wrappedDispatch
7571
+ ...wrappedDispatch,
7572
+ settleSequence: params.settleSequence
6873
7573
  });
6874
7574
  if (!wrappedSimulation.success || wrappedSimulation.tokenFlow === void 0) return {
6875
7575
  available: false,
@@ -8891,4 +9591,196 @@ function isGasError(error) {
8891
9591
  }
8892
9592
 
8893
9593
  //#endregion
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 };
9594
+ //#region src/analytics/market-indicators.ts
9595
+ const D = Decimal.clone({ precision: 40 });
9596
+ const TICK_LOG = new D("1.0001").ln();
9597
+ const MARKET_INDICATOR_PERIODS = {
9598
+ atr: 14,
9599
+ efficiency: 10,
9600
+ moments: 96,
9601
+ rsi: 14,
9602
+ "variance-ratio": 96
9603
+ };
9604
+ const VARIANCE_RATIO_LAG = 4;
9605
+ /** Sorts and fills only internal no-swap gaps; never invents leading/trailing history. */
9606
+ function prepareIndicatorCandles(candles, interval) {
9607
+ if (interval <= 0n) throw new RangeError("Candle interval must be positive");
9608
+ const sorted = [...candles].sort((a, b) => a.time < b.time ? -1 : a.time > b.time ? 1 : 0);
9609
+ const result = [];
9610
+ for (const candle of sorted) {
9611
+ if (candle.time < 0n || candle.time % interval !== 0n || candle.lowTick < -887272n || candle.highTick > 887272n || candle.lowTick > candle.highTick || candle.openTick < candle.lowTick || candle.openTick > candle.highTick || candle.closeTick < candle.lowTick || candle.closeTick > candle.highTick) throw new RangeError("Invalid candle timestamp or OHLC ticks");
9612
+ const previous = result[result.length - 1];
9613
+ if (previous && previous.time === candle.time) throw new RangeError("Duplicate candle timestamp");
9614
+ if (previous) {
9615
+ if ((candle.time - result[0].time) / interval >= 4096n) throw new RangeError("Indicator history exceeds 4096 candles");
9616
+ for (let time = previous.time + interval; time < candle.time; time += interval) result.push({
9617
+ time,
9618
+ openTick: previous.closeTick,
9619
+ highTick: previous.closeTick,
9620
+ lowTick: previous.closeTick,
9621
+ closeTick: previous.closeTick
9622
+ });
9623
+ }
9624
+ result.push(candle);
9625
+ }
9626
+ return result;
9627
+ }
9628
+ function sum(values) {
9629
+ return values.reduce((total, value) => total.plus(value), new D(0));
9630
+ }
9631
+ function tickChanges(candles, isAssetToken0) {
9632
+ return candles.slice(1).map((candle, index) => ({
9633
+ time: candle.time,
9634
+ value: (candle.closeTick - candles[index].closeTick) * (isAssetToken0 ? 1n : -1n)
9635
+ }));
9636
+ }
9637
+ function wilder(values, period) {
9638
+ let average = new D(0);
9639
+ return values.map((value, index) => {
9640
+ if (index < period) {
9641
+ average = average.plus(value.div(period));
9642
+ return index === period - 1 ? average : null;
9643
+ }
9644
+ average = average.mul(period - 1).plus(value).div(period);
9645
+ return average;
9646
+ });
9647
+ }
9648
+ function centeredTickChanges(values) {
9649
+ const count = BigInt(values.length);
9650
+ const total = values.reduce((sum$1, value) => sum$1 + value, 0n);
9651
+ const deviations = values.map((value) => count * value - total);
9652
+ const squares = deviations.reduce((sum$1, value) => sum$1 + value ** 2n, 0n);
9653
+ return {
9654
+ count,
9655
+ total,
9656
+ deviations,
9657
+ squares
9658
+ };
9659
+ }
9660
+ /** Central population moments: skewness and excess kurtosis (normal = 0). */
9661
+ function returnMoments(values) {
9662
+ const { count, deviations, squares } = centeredTickChanges(values);
9663
+ if (squares === 0n) return {
9664
+ value: null,
9665
+ secondaryValue: null
9666
+ };
9667
+ const cubes = deviations.reduce((sum$1, value) => sum$1 + value ** 3n, 0n);
9668
+ const fourths = deviations.reduce((sum$1, value) => sum$1 + value ** 4n, 0n);
9669
+ const secondMoment = new D(squares.toString());
9670
+ return {
9671
+ value: new D((cubes * cubes * count).toString()).div(secondMoment.pow(3)).sqrt().mul(cubes < 0n ? -1 : 1),
9672
+ secondaryValue: new D((count * fourths).toString()).div((squares * squares).toString()).minus(3)
9673
+ };
9674
+ }
9675
+ /** Lo–MacKinlay overlapping, finite-sample-corrected ratio; no significance test. */
9676
+ function varianceRatio(values) {
9677
+ const { count, total, squares } = centeredTickChanges(values);
9678
+ if (squares === 0n) return null;
9679
+ const lag = VARIANCE_RATIO_LAG;
9680
+ const q = BigInt(lag);
9681
+ let overlappingSquares = 0n;
9682
+ for (let index = lag; index <= values.length; index++) {
9683
+ const change = values.slice(index - lag, index).reduce((sum$1, value) => sum$1 + value, 0n);
9684
+ overlappingSquares += (count * change - q * total) ** 2n;
9685
+ }
9686
+ const numerator = overlappingSquares * (count - 1n) * count;
9687
+ const denominator = q * (count - q + 1n) * (count - q) * squares;
9688
+ return new D(numerator.toString()).div(denominator.toString());
9689
+ }
9690
+ /** Prices use quote units per asset; return statistics use equally spaced log closes. */
9691
+ function calculateMarketIndicator(indicator, candles, { intervalSeconds, token0Decimals, token1Decimals, isAssetToken0 }) {
9692
+ if (token0Decimals < 0n || token0Decimals > 255n || token1Decimals < 0n || token1Decimals > 255n) throw new RangeError("Token decimals must be between 0 and 255");
9693
+ const prepared = prepareIndicatorCandles(candles, intervalSeconds);
9694
+ const period = MARKET_INDICATOR_PERIODS[indicator];
9695
+ if (indicator === "moments" || indicator === "variance-ratio") {
9696
+ const returns = tickChanges(prepared, isAssetToken0);
9697
+ return returns.map((point, index) => {
9698
+ if (index < period - 1) return {
9699
+ time: point.time,
9700
+ value: null
9701
+ };
9702
+ const window = returns.slice(index - period + 1, index + 1).map((value) => value.value);
9703
+ return {
9704
+ time: point.time,
9705
+ ...indicator === "moments" ? returnMoments(window) : { value: varianceRatio(window) }
9706
+ };
9707
+ });
9708
+ }
9709
+ const decimalScale = new D(10).pow((token0Decimals - token1Decimals).toString());
9710
+ const prices = new Map();
9711
+ const price = (tick) => {
9712
+ const cached = prices.get(tick);
9713
+ if (cached) return cached;
9714
+ const direct = new D("1.0001").pow(tick.toString()).mul(decimalScale);
9715
+ const value = isAssetToken0 ? direct : new D(1).div(direct);
9716
+ prices.set(tick, value);
9717
+ return value;
9718
+ };
9719
+ const closes = prepared.map((candle) => price(candle.closeTick));
9720
+ if (indicator === "atr") {
9721
+ const ranges = prepared.map((candle, index) => {
9722
+ const high = price(isAssetToken0 ? candle.highTick : candle.lowTick);
9723
+ const low = price(isAssetToken0 ? candle.lowTick : candle.highTick);
9724
+ const previous = closes[index - 1];
9725
+ return previous ? D.max(high.minus(low), high.minus(previous).abs(), low.minus(previous).abs()) : high.minus(low);
9726
+ });
9727
+ return wilder(ranges, period).map((value, index) => ({
9728
+ time: prepared[index].time,
9729
+ value
9730
+ }));
9731
+ }
9732
+ const changes = closes.slice(1).map((close, index) => close.minus(closes[index]));
9733
+ if (indicator === "rsi") {
9734
+ const gains = wilder(changes.map((value) => D.max(value, 0)), period);
9735
+ const losses = wilder(changes.map((value) => D.max(value.neg(), 0)), period);
9736
+ return gains.map((gain, index) => {
9737
+ const loss = losses[index];
9738
+ const value = gain === null || loss === null ? null : gain.isZero() && loss.isZero() ? new D(50) : loss.isZero() ? new D(100) : new D(100).minus(new D(100).div(new D(1).plus(gain.div(loss))));
9739
+ return {
9740
+ time: prepared[index + 1].time,
9741
+ value
9742
+ };
9743
+ });
9744
+ }
9745
+ return closes.map((close, index) => {
9746
+ if (index < period) return {
9747
+ time: prepared[index].time,
9748
+ value: null
9749
+ };
9750
+ const distance = close.minus(closes[index - period]).abs();
9751
+ const path = sum(changes.slice(index - period, index).map((value) => value.abs()));
9752
+ return {
9753
+ time: prepared[index].time,
9754
+ value: path.isZero() ? new D(0) : distance.div(path)
9755
+ };
9756
+ });
9757
+ }
9758
+ /** Sample variance of equal-interval log returns, grouped by UTC candle-start hour/day. */
9759
+ function calculateVarianceProfile(candles, isAssetToken0, intervalSeconds = 3600n) {
9760
+ if (intervalSeconds <= 0n || intervalSeconds > 86400n || 86400n % intervalSeconds !== 0n) throw new RangeError("Profile candle interval must divide a UTC day");
9761
+ const prepared = prepareIndicatorCandles(candles, intervalSeconds);
9762
+ const returns = tickChanges(prepared, isAssetToken0);
9763
+ const hours = Array.from({ length: 24 }, () => []);
9764
+ const weekdays = Array.from({ length: 7 }, () => []);
9765
+ for (const point of returns) {
9766
+ const hour = Number(point.time / 3600n % 24n);
9767
+ const weekday = Number((point.time / 86400n + 3n) % 7n);
9768
+ hours[hour].push(point.value);
9769
+ weekdays[weekday].push(point.value);
9770
+ }
9771
+ const summarize = (buckets) => buckets.map((values, bucket) => {
9772
+ const { count, squares } = centeredTickChanges(values);
9773
+ return {
9774
+ bucket,
9775
+ count: values.length,
9776
+ variance: count < 2n ? null : new D(squares.toString()).mul(TICK_LOG.pow(2)).div((count * count * (count - 1n)).toString())
9777
+ };
9778
+ });
9779
+ return {
9780
+ hours: summarize(hours),
9781
+ weekdays: summarize(weekdays)
9782
+ };
9783
+ }
9784
+
9785
+ //#endregion
9786
+ export { DEFAULT_MIN_SWAP_RATIO_BPS, DEFAULT_RECONNECT_CONFIG, MARKET_INDICATOR_PERIODS, REQUIRED_BASE_ERROR_SENTINEL, STRIKE_LADDER_TARGET_STRIKES, VARIANCE_RATIO_LAG, addPendingPosition, addTrackedChunks, apportion, assertCanBurn, assertCanForceExercise, assertCanLiquidate, assertCanMint, assertFresh, assertHealthy, assertLpPositionFunded, assertTradeable, buildCreditWrappedDispatch, buildSfpmSwapCalldata, buildSfpmSwapPlan, buildTemporaryLoanRecoveryDispatch, buildTokenShortfallRecoveryDispatch, calculateAccountGreeksPure, calculateMarketIndicator, calculateSpreadWad, calculateVarianceProfile, checkCollateralAcrossTicks, classifyStrategyGroups, classifyStrike, cleanupStalePendingPositions, clearPendingPositions, clearTrackedChunks, clearTradeHistory, collateralRuleKindFor, computeV4PoolId, confirmPendingPosition, convertToAssets, convertToShares, createEventPoller, createEventSubscription, createFlowNeutralTokenId, decodePanopticTokenURI, emptyLiquidateParams, encodePoolKeyBytes, encodeV3PoolKeyBytes, ensureSfpmV3PoolInitialized, estimateBlockNumbers, estimateCollateralBreakdown, estimateCollateralRequired, failPendingPosition, feesFromFeeGrowthDelta, fetchSfpmV3PoolId, getAccountBuyingPower, getAccountGreeks, getAccountHistory, getChunkLiquidities, getChunkSpreads, getClosedPositions, getCollateralRequiredBase, getCollateralSharePrices, getCollateralTotalAssetsBatch, getDeltaHedgeParams, getEnforcedTickLimits, getExecutableLpMaxSize, getFactoryConstructMetadata, getFactoryOwnerOf, getFactoryTokenURI, getGuardianUnlockState, getHedgeLimits, getItmAmounts, getMarginBuffer, getMaxPositionSize, getMaxRedeem, getMaxWithdrawable, getNativeTokenPrice, getNotEnoughTokensError, getOpenPositionPreview, getPanopticPoolAddress, getPanopticPoolFromPoolId, getPendingPositions, getPoolCollateralAddresses, 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, prepareIndicatorCandles, previewDeposit, previewMint, previewRedeem, previewWithdraw, quoteLiquidation, quoteOneTokenFlow, quoteSfpmSwap, quoteTemporaryLoanRecovery, quoteTokenShortfallRecovery, removeTrackedChunks, resolveBlockNumbers, resolveLadderStrike, resolvePanopticPoolFromPoolId, resolveUniswapV4PoolKey, saveClosedPosition, scaleCollateralRequired, scanChunks, screenAccountExact, simulateBatchDispatch, simulateClosePosition, simulateDeployNewPool, simulateDeposit, simulateDispatch, simulateForceExercise, simulateLiquidate, simulateSFPMBurn, simulateSFPMMint, simulateSwapExactIn, simulateSwapExactOut, simulateWithdraw, slippageBpsToTickDistance, watchEvents };