@panoptic-eng/sdk 1.0.4 → 1.0.6

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -20
  3. package/dist/cow/index.d.ts +5 -0
  4. package/dist/cow/index.js +5 -0
  5. package/dist/cow/types.d.ts +4 -0
  6. package/dist/cow/types.js +0 -0
  7. package/dist/cow-Dy-Cd09v.js +1405 -0
  8. package/dist/cow-Dy-Cd09v.js.map +1 -0
  9. package/dist/index-BuJcj5aO.d.ts +275 -0
  10. package/dist/index-BuJcj5aO.d.ts.map +1 -0
  11. package/dist/index-DVMjZi1E.d.ts +1801 -0
  12. package/dist/index-DVMjZi1E.d.ts.map +1 -0
  13. package/dist/index.d.ts +6192 -2513
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3242 -5537
  16. package/dist/index.js.map +1 -1
  17. package/dist/irm-CBrX8bjH.js +2375 -0
  18. package/dist/irm-CBrX8bjH.js.map +1 -0
  19. package/dist/irm-CGykVo3q.d.ts +32 -0
  20. package/dist/irm-CGykVo3q.d.ts.map +1 -0
  21. package/dist/irm-zWjtffWA.d.ts +85 -0
  22. package/dist/irm-zWjtffWA.d.ts.map +1 -0
  23. package/dist/panoptic/v2/index.d.ts +9370 -0
  24. package/dist/panoptic/v2/index.d.ts.map +1 -0
  25. package/dist/panoptic/v2/index.js +10765 -0
  26. package/dist/panoptic/v2/index.js.map +1 -0
  27. package/dist/panoptic/v2/types/index.d.ts +3 -0
  28. package/dist/panoptic/v2/types/index.js +0 -0
  29. package/dist/position-FxaZi608.js +10530 -0
  30. package/dist/position-FxaZi608.js.map +1 -0
  31. package/dist/router-gzYGM1OO.js +1012 -0
  32. package/dist/router-gzYGM1OO.js.map +1 -0
  33. package/dist/types-BQejAFnu.d.ts +245 -0
  34. package/dist/types-BQejAFnu.d.ts.map +1 -0
  35. package/dist/types-CRvvn2ce.d.ts +128 -0
  36. package/dist/types-CRvvn2ce.d.ts.map +1 -0
  37. package/dist/uniswap/index.d.ts +291 -0
  38. package/dist/uniswap/index.d.ts.map +1 -0
  39. package/dist/uniswap/index.js +5 -0
  40. package/dist/writes-CVCD6Ers.js +4302 -0
  41. package/dist/writes-CVCD6Ers.js.map +1 -0
  42. package/package.json +24 -7
@@ -0,0 +1,4302 @@
1
+ import { BatchValidationError, DEFAULT_VEGOID, InputListFailError, InvalidTickLimitsError, InvalidTokenIdParameterError, LEG_BITS, LEG_LIMITS, LEG_MASKS, LoanSlotExhaustedError, MaxRetriesExceededError, MissingPositionIdsError, NoLoanPositionsError, OracleRateLimitedError, PanopticError$1 as PanopticError, ProviderLagError, REORG_DEPTH, SCHEMA_VERSION, STORAGE_PREFIX, STRIKE_CONVERSION_FACTOR, SafeModeError, StaleDataError, SwapTokenMismatchError, SyncTimeoutError, TOKEN_ID_BITS, UnhealthyPoolError, calculatePositionGreeks, collateralTrackerV2Abi, createTxResult, decodeLeftRightUnsigned, getBlockMeta, getPool$1 as getPool, getPoolMetadata$1 as getPoolMetadata, getPositions, panopticErrorsAbi, panopticFactoryV3Abi, panopticFactoryV4Abi, panopticPoolV2Abi, parsePanopticError, submitWrite, tickLimits$1 as tickLimits } from "./position-FxaZi608.js";
2
+ import { decodeFunctionData, decodeFunctionResult, encodeFunctionData, erc20Abi, toFunctionSelector } from "viem";
3
+
4
+ //#region src/panoptic/v2/writes/broadcaster.ts
5
+ /**
6
+ * Default public broadcaster singleton.
7
+ *
8
+ * Note: This singleton requires a client to be provided via the broadcast call's context.
9
+ * For standalone usage, use `createPublicBroadcaster(client)` instead.
10
+ *
11
+ * When used with write functions, the SDK handles client injection automatically.
12
+ */
13
+ const publicBroadcaster = { async broadcast(signedTx) {
14
+ throw new Error("publicBroadcaster requires a client. Use createPublicBroadcaster(client) for standalone usage.");
15
+ } };
16
+ /**
17
+ * Create a nonce manager for concurrent transaction submission.
18
+ *
19
+ * The nonce manager tracks pending nonces to allow multiple transactions
20
+ * to be submitted without waiting for confirmation.
21
+ *
22
+ * @param client - The public client for querying nonces
23
+ * @returns A NonceManager instance
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const nonceManager = createNonceManager(client)
28
+ * const nonce1 = await nonceManager.getNextNonce(account)
29
+ * const nonce2 = await nonceManager.getNextNonce(account) // nonce1 + 1
30
+ * // On failure, reset to re-fetch from chain:
31
+ * nonceManager.reset(account)
32
+ * ```
33
+ */
34
+ function createNonceManager(client) {
35
+ const pendingNonces = new Map();
36
+ return {
37
+ async getNextNonce(account) {
38
+ const chainNonce = BigInt(await client.getTransactionCount({
39
+ address: account,
40
+ blockTag: "pending"
41
+ }));
42
+ const pending = pendingNonces.get(account);
43
+ const localNext = pending !== void 0 ? pending + 1n : chainNonce;
44
+ const nextNonce = localNext > chainNonce ? localNext : chainNonce;
45
+ pendingNonces.set(account, nextNonce);
46
+ return nextNonce;
47
+ },
48
+ reset(account) {
49
+ pendingNonces.delete(account);
50
+ }
51
+ };
52
+ }
53
+
54
+ //#endregion
55
+ //#region src/panoptic/v2/writes/approve.ts
56
+ /**
57
+ * Approve token spending for a collateral tracker.
58
+ *
59
+ * @param params - The approval parameters
60
+ * @returns TxResult with hash and wait function
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const result = await approve({
65
+ * client,
66
+ * walletClient,
67
+ * account,
68
+ * tokenAddress: WETH,
69
+ * spenderAddress: collateralToken0,
70
+ * amount: MaxUint256,
71
+ * })
72
+ * const receipt = await result.wait()
73
+ * ```
74
+ */
75
+ async function approve(params) {
76
+ const { client, walletClient, account, tokenAddress, spenderAddress, amount, txOverrides } = params;
77
+ return submitWrite({
78
+ client,
79
+ walletClient,
80
+ account,
81
+ address: tokenAddress,
82
+ abi: erc20Abi,
83
+ functionName: "approve",
84
+ args: [spenderAddress, amount],
85
+ txOverrides
86
+ });
87
+ }
88
+ /**
89
+ * Approve token spending and wait for confirmation.
90
+ *
91
+ * @param params - The approval parameters
92
+ * @returns TxReceipt after confirmation
93
+ */
94
+ async function approveAndWait(params) {
95
+ const result = await approve(params);
96
+ return result.wait();
97
+ }
98
+ /**
99
+ * Approve both tokens for a Panoptic pool.
100
+ * Fetches collateral tracker addresses and underlying tokens, then approves both.
101
+ *
102
+ * @param params - The approval parameters
103
+ * @returns Array of two TxResults
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const [result0, result1] = await approvePool({
108
+ * client,
109
+ * walletClient,
110
+ * account,
111
+ * poolAddress,
112
+ * amount0: MaxUint256,
113
+ * amount1: MaxUint256,
114
+ * })
115
+ * await Promise.all([result0.wait(), result1.wait()])
116
+ * ```
117
+ */
118
+ async function approvePool(params) {
119
+ const { client, walletClient, account, poolAddress, amount0, amount1 } = params;
120
+ const [collateralToken0, collateralToken1] = await client.multicall({
121
+ contracts: [{
122
+ address: poolAddress,
123
+ abi: panopticPoolV2Abi,
124
+ functionName: "collateralToken0"
125
+ }, {
126
+ address: poolAddress,
127
+ abi: panopticPoolV2Abi,
128
+ functionName: "collateralToken1"
129
+ }],
130
+ allowFailure: false
131
+ });
132
+ const [token0, token1] = await client.multicall({
133
+ contracts: [{
134
+ address: collateralToken0,
135
+ abi: collateralTrackerV2Abi,
136
+ functionName: "asset"
137
+ }, {
138
+ address: collateralToken1,
139
+ abi: collateralTrackerV2Abi,
140
+ functionName: "asset"
141
+ }],
142
+ allowFailure: false
143
+ });
144
+ const [result0, result1] = await Promise.all([approve({
145
+ client,
146
+ walletClient,
147
+ account,
148
+ tokenAddress: token0,
149
+ spenderAddress: collateralToken0,
150
+ amount: amount0
151
+ }), approve({
152
+ client,
153
+ walletClient,
154
+ account,
155
+ tokenAddress: token1,
156
+ spenderAddress: collateralToken1,
157
+ amount: amount1
158
+ })]);
159
+ return [result0, result1];
160
+ }
161
+ /**
162
+ * Check if approval is needed for a token transfer.
163
+ *
164
+ * @param params - The check parameters
165
+ * @returns Approval status
166
+ */
167
+ async function checkApproval(params) {
168
+ const { client, tokenAddress, owner, spender, amount } = params;
169
+ const currentAllowance = await client.readContract({
170
+ address: tokenAddress,
171
+ abi: erc20Abi,
172
+ functionName: "allowance",
173
+ args: [owner, spender]
174
+ });
175
+ return {
176
+ needsApproval: currentAllowance < amount,
177
+ currentAllowance,
178
+ requiredAmount: amount
179
+ };
180
+ }
181
+
182
+ //#endregion
183
+ //#region src/panoptic/v2/writes/vault.ts
184
+ /**
185
+ * Deposit assets into a collateral tracker.
186
+ *
187
+ * @param params - Deposit parameters
188
+ * @returns TxResult
189
+ *
190
+ * @example
191
+ * ```typescript
192
+ * const result = await deposit({
193
+ * client,
194
+ * walletClient,
195
+ * account,
196
+ * collateralTrackerAddress,
197
+ * assets: parseEther('1'),
198
+ * })
199
+ * const receipt = await result.wait()
200
+ * ```
201
+ */
202
+ async function deposit(params) {
203
+ const { client, walletClient, account, collateralTrackerAddress, assets, isNativeETH, receiver = account, txOverrides } = params;
204
+ return submitWrite({
205
+ client,
206
+ walletClient,
207
+ account,
208
+ address: collateralTrackerAddress,
209
+ abi: collateralTrackerV2Abi,
210
+ functionName: "deposit",
211
+ args: [assets, receiver],
212
+ value: isNativeETH ? assets : void 0,
213
+ txOverrides
214
+ });
215
+ }
216
+ /**
217
+ * Deposit assets and wait for confirmation.
218
+ */
219
+ async function depositAndWait(params) {
220
+ const result = await deposit(params);
221
+ return result.wait();
222
+ }
223
+ /**
224
+ * Withdraw assets from a collateral tracker.
225
+ *
226
+ * @param params - Withdraw parameters
227
+ * @returns TxResult
228
+ */
229
+ async function withdraw(params) {
230
+ const { client, walletClient, account, collateralTrackerAddress, assets, receiver = account, owner = account, txOverrides } = params;
231
+ return submitWrite({
232
+ client,
233
+ walletClient,
234
+ account,
235
+ address: collateralTrackerAddress,
236
+ abi: collateralTrackerV2Abi,
237
+ functionName: "withdraw",
238
+ args: [
239
+ assets,
240
+ receiver,
241
+ owner
242
+ ],
243
+ txOverrides
244
+ });
245
+ }
246
+ /**
247
+ * Withdraw assets and wait for confirmation.
248
+ */
249
+ async function withdrawAndWait(params) {
250
+ const result = await withdraw(params);
251
+ return result.wait();
252
+ }
253
+ /**
254
+ * Withdraw assets with position list validation.
255
+ * This overload validates that the withdrawal won't make positions undercollateralized.
256
+ *
257
+ * @param params - Withdraw parameters with positions
258
+ * @returns TxResult
259
+ */
260
+ async function withdrawWithPositions(params) {
261
+ const { client, walletClient, account, collateralTrackerAddress, assets, receiver = account, owner = account, positionIdList, usePremiaAsCollateral, txOverrides } = params;
262
+ return submitWrite({
263
+ client,
264
+ walletClient,
265
+ account,
266
+ address: collateralTrackerAddress,
267
+ abi: collateralTrackerV2Abi,
268
+ functionName: "withdraw",
269
+ args: [
270
+ assets,
271
+ receiver,
272
+ owner,
273
+ positionIdList,
274
+ usePremiaAsCollateral
275
+ ],
276
+ txOverrides
277
+ });
278
+ }
279
+ /**
280
+ * Withdraw with positions and wait for confirmation.
281
+ */
282
+ async function withdrawWithPositionsAndWait(params) {
283
+ const result = await withdrawWithPositions(params);
284
+ return result.wait();
285
+ }
286
+ /**
287
+ * Mint shares by depositing assets.
288
+ *
289
+ * @param params - Mint parameters
290
+ * @returns TxResult
291
+ */
292
+ async function mint(params) {
293
+ const { client, walletClient, account, collateralTrackerAddress, shares, receiver = account, txOverrides } = params;
294
+ return submitWrite({
295
+ client,
296
+ walletClient,
297
+ account,
298
+ address: collateralTrackerAddress,
299
+ abi: collateralTrackerV2Abi,
300
+ functionName: "mint",
301
+ args: [shares, receiver],
302
+ txOverrides
303
+ });
304
+ }
305
+ /**
306
+ * Mint shares and wait for confirmation.
307
+ */
308
+ async function mintAndWait(params) {
309
+ const result = await mint(params);
310
+ return result.wait();
311
+ }
312
+ /**
313
+ * Redeem shares for assets.
314
+ *
315
+ * @param params - Redeem parameters
316
+ * @returns TxResult
317
+ */
318
+ async function redeem(params) {
319
+ const { client, walletClient, account, collateralTrackerAddress, shares, receiver = account, owner = account, txOverrides } = params;
320
+ return submitWrite({
321
+ client,
322
+ walletClient,
323
+ account,
324
+ address: collateralTrackerAddress,
325
+ abi: collateralTrackerV2Abi,
326
+ functionName: "redeem",
327
+ args: [
328
+ shares,
329
+ receiver,
330
+ owner
331
+ ],
332
+ txOverrides
333
+ });
334
+ }
335
+ /**
336
+ * Redeem shares and wait for confirmation.
337
+ */
338
+ async function redeemAndWait(params) {
339
+ const result = await redeem(params);
340
+ return result.wait();
341
+ }
342
+
343
+ //#endregion
344
+ //#region src/panoptic/v2/storage/fileStorage.ts
345
+ /**
346
+ * Creates a file-based storage adapter for Node.js environments.
347
+ * Stores data as JSON files in the specified directory.
348
+ *
349
+ * The directory will be created if it doesn't exist.
350
+ *
351
+ * @param directory - Path to the storage directory
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * import { createFileStorage, createConfig } from 'panoptic-v2-sdk'
356
+ *
357
+ * // Node.js only
358
+ * const config = createConfig({
359
+ * chainId: 1n,
360
+ * transport: http('...'),
361
+ * poolAddress: '0x...',
362
+ * storage: createFileStorage('./panoptic-cache'),
363
+ * })
364
+ * ```
365
+ */
366
+ function createFileStorage(directory) {
367
+ let fsPromises = null;
368
+ let pathModule = null;
369
+ let initialized = false;
370
+ const ensureInitialized = async () => {
371
+ if (initialized) return;
372
+ fsPromises = await import("node:fs/promises");
373
+ pathModule = await import("node:path");
374
+ await fsPromises.mkdir(directory, { recursive: true });
375
+ initialized = true;
376
+ };
377
+ const getFilePath = (key) => {
378
+ if (!pathModule) throw new Error("File storage not initialized");
379
+ const safeKey = Buffer.from(key).toString("base64url");
380
+ return pathModule.join(directory, `${safeKey}.json`);
381
+ };
382
+ return {
383
+ async get(key) {
384
+ await ensureInitialized();
385
+ if (!fsPromises) throw new Error("File storage not initialized");
386
+ try {
387
+ const filePath = getFilePath(key);
388
+ const content = await fsPromises.readFile(filePath, "utf-8");
389
+ return content;
390
+ } catch (error) {
391
+ if (error.code === "ENOENT") return null;
392
+ throw error;
393
+ }
394
+ },
395
+ async set(key, value) {
396
+ await ensureInitialized();
397
+ if (!fsPromises) throw new Error("File storage not initialized");
398
+ const filePath = getFilePath(key);
399
+ await fsPromises.writeFile(filePath, value, "utf-8");
400
+ },
401
+ async delete(key) {
402
+ await ensureInitialized();
403
+ if (!fsPromises) throw new Error("File storage not initialized");
404
+ try {
405
+ const filePath = getFilePath(key);
406
+ await fsPromises.unlink(filePath);
407
+ } catch (error) {
408
+ if (error.code !== "ENOENT") throw error;
409
+ }
410
+ },
411
+ async has(key) {
412
+ await ensureInitialized();
413
+ if (!fsPromises) throw new Error("File storage not initialized");
414
+ try {
415
+ const filePath = getFilePath(key);
416
+ await fsPromises.access(filePath);
417
+ return true;
418
+ } catch {
419
+ return false;
420
+ }
421
+ },
422
+ async keys(prefix) {
423
+ await ensureInitialized();
424
+ if (!fsPromises || !pathModule) throw new Error("File storage not initialized");
425
+ const result = [];
426
+ try {
427
+ const files = await fsPromises.readdir(directory);
428
+ for (const file of files) if (file.endsWith(".json")) {
429
+ const safeKey = file.slice(0, -5);
430
+ try {
431
+ const key = Buffer.from(safeKey, "base64url").toString();
432
+ if (key.startsWith(prefix)) result.push(key);
433
+ } catch {}
434
+ }
435
+ } catch (error) {
436
+ if (error.code !== "ENOENT") throw error;
437
+ }
438
+ return result;
439
+ },
440
+ async clear(prefix) {
441
+ await ensureInitialized();
442
+ if (!fsPromises) throw new Error("File storage not initialized");
443
+ const keysToDelete = await this.keys(prefix);
444
+ for (const key of keysToDelete) await this.delete(key);
445
+ }
446
+ };
447
+ }
448
+
449
+ //#endregion
450
+ //#region src/panoptic/v2/storage/keys.ts
451
+ /**
452
+ * Get the schema version key.
453
+ */
454
+ function getSchemaVersionKey() {
455
+ return `${STORAGE_PREFIX}:schemaVersion`;
456
+ }
457
+ /**
458
+ * Get the base prefix for a specific pool.
459
+ */
460
+ function getPoolPrefix(chainId, poolAddress) {
461
+ return `${STORAGE_PREFIX}:v${SCHEMA_VERSION}:chain${chainId}:pool${poolAddress.toLowerCase()}`;
462
+ }
463
+ /**
464
+ * Get the key for tracked position IDs.
465
+ */
466
+ function getPositionsKey(chainId, poolAddress, account) {
467
+ return `${getPoolPrefix(chainId, poolAddress)}:positions:${account.toLowerCase()}`;
468
+ }
469
+ /**
470
+ * Get the key for position mint metadata.
471
+ */
472
+ function getPositionMetaKey(chainId, poolAddress, tokenId) {
473
+ return `${getPoolPrefix(chainId, poolAddress)}:positionMeta:${tokenId.toString()}`;
474
+ }
475
+ /**
476
+ * Get the key for sync checkpoint.
477
+ */
478
+ function getSyncCheckpointKey(chainId, poolAddress, account) {
479
+ return `${getPoolPrefix(chainId, poolAddress)}:sync:${account.toLowerCase()}`;
480
+ }
481
+ /**
482
+ * Get the key for closed positions.
483
+ */
484
+ function getClosedPositionsKey(chainId, poolAddress, account) {
485
+ return `${getPoolPrefix(chainId, poolAddress)}:closed:${account.toLowerCase()}`;
486
+ }
487
+ /**
488
+ * Get the key for tracked chunks.
489
+ */
490
+ function getTrackedChunksKey(chainId, poolAddress) {
491
+ return `${getPoolPrefix(chainId, poolAddress)}:chunks`;
492
+ }
493
+ /**
494
+ * Get the key for pending positions.
495
+ */
496
+ function getPendingPositionsKey(chainId, poolAddress, account) {
497
+ return `${getPoolPrefix(chainId, poolAddress)}:pending:${account.toLowerCase()}`;
498
+ }
499
+ /**
500
+ * Get the key for pool metadata (tickSpacing, etc.).
501
+ */
502
+ function getPoolMetaKey(chainId, poolAddress) {
503
+ return `${getPoolPrefix(chainId, poolAddress)}:poolMeta`;
504
+ }
505
+
506
+ //#endregion
507
+ //#region src/panoptic/v2/storage/memoryStorage.ts
508
+ /**
509
+ * Creates an in-memory storage adapter.
510
+ * Useful for testing and development.
511
+ *
512
+ * Data is lost when the process exits.
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * import { createMemoryStorage, createConfig } from 'panoptic-v2-sdk'
517
+ *
518
+ * const config = createConfig({
519
+ * chainId: 1n,
520
+ * transport: http('...'),
521
+ * poolAddress: '0x...',
522
+ * storage: createMemoryStorage(),
523
+ * })
524
+ * ```
525
+ */
526
+ function createMemoryStorage() {
527
+ const store = new Map();
528
+ return {
529
+ async get(key) {
530
+ return store.get(key) ?? null;
531
+ },
532
+ async set(key, value) {
533
+ store.set(key, value);
534
+ },
535
+ async delete(key) {
536
+ store.delete(key);
537
+ },
538
+ async has(key) {
539
+ return store.has(key);
540
+ },
541
+ async keys(prefix) {
542
+ const result = [];
543
+ for (const key of store.keys()) if (key.startsWith(prefix)) result.push(key);
544
+ return result;
545
+ },
546
+ async clear(prefix) {
547
+ const keysToDelete = [];
548
+ for (const key of store.keys()) if (key.startsWith(prefix)) keysToDelete.push(key);
549
+ for (const key of keysToDelete) store.delete(key);
550
+ }
551
+ };
552
+ }
553
+
554
+ //#endregion
555
+ //#region src/panoptic/v2/storage/serializer.ts
556
+ /**
557
+ * Type guard to check if a value is a tagged BigInt.
558
+ */
559
+ function isBigIntTagged(value) {
560
+ return value !== null && typeof value === "object" && "__type" in value && value.__type === "bigint" && "value" in value && typeof value.value === "string";
561
+ }
562
+ /**
563
+ * Replacer function for JSON.stringify that tags BigInt values.
564
+ */
565
+ function replacer(_key, value) {
566
+ if (typeof value === "bigint") return {
567
+ __type: "bigint",
568
+ value: value.toString()
569
+ };
570
+ return value;
571
+ }
572
+ /**
573
+ * Reviver function for JSON.parse that restores tagged BigInt values.
574
+ */
575
+ function reviver(_key, value) {
576
+ if (isBigIntTagged(value)) return BigInt(value.value);
577
+ return value;
578
+ }
579
+ /**
580
+ * BigInt-safe JSON serializer for the Panoptic v2 SDK.
581
+ *
582
+ * This serializer handles BigInt values by tagging them with a special format
583
+ * that can be restored on parse. The format is compatible with superjson
584
+ * patterns used in Next.js and Remix for SSR hydration.
585
+ *
586
+ * @example
587
+ * ```typescript
588
+ * import { jsonSerializer } from 'panoptic-v2-sdk'
589
+ *
590
+ * const data = { amount: 1000000000000000000n, nested: { value: 42n } }
591
+ *
592
+ * // Serialize to JSON string
593
+ * const json = jsonSerializer.stringify(data)
594
+ * // '{"amount":{"__type":"bigint","value":"1000000000000000000"},"nested":{"value":{"__type":"bigint","value":"42"}}}'
595
+ *
596
+ * // Parse back to original types
597
+ * const parsed = jsonSerializer.parse(json)
598
+ * // { amount: 1000000000000000000n, nested: { value: 42n } }
599
+ * ```
600
+ */
601
+ const jsonSerializer = {
602
+ stringify(value, space) {
603
+ return JSON.stringify(value, replacer, space);
604
+ },
605
+ parse(text) {
606
+ return JSON.parse(text, reviver);
607
+ }
608
+ };
609
+
610
+ //#endregion
611
+ //#region src/panoptic/v2/sync/snapshotRecovery.ts
612
+ /**
613
+ * Recover position snapshot from the last dispatch transaction.
614
+ * This is the primary recovery method - it finds the most recent dispatch()
615
+ * call and extracts the finalPositionIdList from the calldata.
616
+ *
617
+ * @param params - Recovery parameters
618
+ * @returns Snapshot recovery result
619
+ */
620
+ async function recoverSnapshot(params) {
621
+ const { client, poolAddress, account, toBlock } = params;
622
+ const latestBlock = toBlock ?? await client.getBlockNumber();
623
+ const searchFromBlock = params.fromBlock ?? 0n;
624
+ const [mintEvents, burnEvents, forceExerciseEvents, liquidationEvents] = await Promise.all([
625
+ client.getLogs({
626
+ address: poolAddress,
627
+ event: {
628
+ type: "event",
629
+ name: "OptionMinted",
630
+ inputs: [
631
+ {
632
+ type: "address",
633
+ name: "recipient",
634
+ indexed: true
635
+ },
636
+ {
637
+ type: "uint256",
638
+ name: "tokenId",
639
+ indexed: true
640
+ },
641
+ {
642
+ type: "uint256",
643
+ name: "balanceData",
644
+ indexed: false
645
+ }
646
+ ]
647
+ },
648
+ args: { recipient: account },
649
+ fromBlock: searchFromBlock,
650
+ toBlock: latestBlock
651
+ }),
652
+ client.getLogs({
653
+ address: poolAddress,
654
+ event: {
655
+ type: "event",
656
+ name: "OptionBurnt",
657
+ inputs: [
658
+ {
659
+ type: "address",
660
+ name: "recipient",
661
+ indexed: true
662
+ },
663
+ {
664
+ type: "uint128",
665
+ name: "positionSize",
666
+ indexed: false
667
+ },
668
+ {
669
+ type: "uint256",
670
+ name: "tokenId",
671
+ indexed: true
672
+ },
673
+ {
674
+ type: "int256[4]",
675
+ name: "premiaByLeg",
676
+ indexed: false
677
+ }
678
+ ]
679
+ },
680
+ args: { recipient: account },
681
+ fromBlock: searchFromBlock,
682
+ toBlock: latestBlock
683
+ }),
684
+ client.getLogs({
685
+ address: poolAddress,
686
+ event: {
687
+ type: "event",
688
+ name: "ForcedExercised",
689
+ inputs: [
690
+ {
691
+ type: "address",
692
+ name: "exercisor",
693
+ indexed: true
694
+ },
695
+ {
696
+ type: "address",
697
+ name: "user",
698
+ indexed: true
699
+ },
700
+ {
701
+ type: "uint256",
702
+ name: "tokenId",
703
+ indexed: true
704
+ },
705
+ {
706
+ type: "int256",
707
+ name: "exerciseFee",
708
+ indexed: false
709
+ }
710
+ ]
711
+ },
712
+ args: { user: account },
713
+ fromBlock: searchFromBlock,
714
+ toBlock: latestBlock
715
+ }),
716
+ client.getLogs({
717
+ address: poolAddress,
718
+ event: {
719
+ type: "event",
720
+ name: "AccountLiquidated",
721
+ inputs: [
722
+ {
723
+ type: "address",
724
+ name: "liquidator",
725
+ indexed: true
726
+ },
727
+ {
728
+ type: "address",
729
+ name: "liquidatee",
730
+ indexed: true
731
+ },
732
+ {
733
+ type: "int256",
734
+ name: "bonusAmounts",
735
+ indexed: false
736
+ }
737
+ ]
738
+ },
739
+ args: { liquidatee: account },
740
+ fromBlock: searchFromBlock,
741
+ toBlock: latestBlock
742
+ })
743
+ ]);
744
+ const allEvents = [
745
+ ...mintEvents,
746
+ ...burnEvents,
747
+ ...forceExerciseEvents,
748
+ ...liquidationEvents
749
+ ].sort((a, b) => {
750
+ const blockDiff = Number(b.blockNumber - a.blockNumber);
751
+ if (blockDiff !== 0) return blockDiff;
752
+ return Number(b.logIndex - a.logIndex);
753
+ });
754
+ const seen = new Set();
755
+ const uniqueEvents = allEvents.filter((e) => {
756
+ if (seen.has(e.transactionHash)) return false;
757
+ seen.add(e.transactionHash);
758
+ return true;
759
+ });
760
+ for (const event of uniqueEvents) try {
761
+ const [tx, block] = await Promise.all([client.getTransaction({ hash: event.transactionHash }), client.getBlock({ blockNumber: event.blockNumber })]);
762
+ const decoded = decodeDispatchCalldata(tx.input);
763
+ if (!decoded) continue;
764
+ if (decoded.targetAccount && decoded.targetAccount.toLowerCase() !== account.toLowerCase()) continue;
765
+ return {
766
+ success: true,
767
+ positionIds: decoded.positionIds,
768
+ blockNumber: event.blockNumber,
769
+ blockHash: block.hash,
770
+ transactionHash: event.transactionHash
771
+ };
772
+ } catch {}
773
+ return null;
774
+ }
775
+ /**
776
+ * Recover position snapshot from a specific dispatch transaction hash.
777
+ *
778
+ * This is an O(1) alternative to {@link recoverSnapshot} when you already
779
+ * know the tx hash of the most recent dispatch. It fetches the transaction,
780
+ * decodes the `finalPositionIdList` from the calldata, and returns the result
781
+ * — no event scanning required.
782
+ *
783
+ * @param params - Recovery parameters including the transaction hash
784
+ * @returns Snapshot recovery result, or null if the tx is not a dispatch call
785
+ *
786
+ * @example
787
+ * ```typescript
788
+ * const snapshot = await recoverSnapshotFromTx({
789
+ * client,
790
+ * transactionHash: '0xabc...',
791
+ * })
792
+ * if (snapshot) {
793
+ * console.log('Open positions:', snapshot.positionIds)
794
+ * }
795
+ * ```
796
+ */
797
+ async function recoverSnapshotFromTx(params) {
798
+ const { client, transactionHash, account, pool } = params;
799
+ const tx = await client.getTransaction({ hash: transactionHash });
800
+ if (tx.blockNumber == null) return null;
801
+ if (pool && tx.to?.toLowerCase() !== pool.toLowerCase()) return null;
802
+ const decoded = decodeDispatchCalldata(tx.input);
803
+ if (!decoded) return null;
804
+ if (account) {
805
+ if (decoded.targetAccount) {
806
+ if (decoded.targetAccount.toLowerCase() !== account.toLowerCase()) return null;
807
+ } else if (tx.from.toLowerCase() !== account.toLowerCase()) return null;
808
+ }
809
+ const blockNumber = tx.blockNumber;
810
+ const block = await client.getBlock({ blockNumber });
811
+ return {
812
+ success: true,
813
+ positionIds: decoded.positionIds,
814
+ blockNumber,
815
+ blockHash: block.hash,
816
+ transactionHash
817
+ };
818
+ }
819
+ /**
820
+ * Decode position IDs from dispatch calldata.
821
+ *
822
+ * @param input - Transaction input data
823
+ * @returns Decoded dispatch data or null if not a dispatch call
824
+ */
825
+ function decodeDispatchCalldata(input) {
826
+ const direct = decodeDirectDispatch(input);
827
+ if (direct) return direct;
828
+ return decodeWrappedDispatch(input);
829
+ }
830
+ /**
831
+ * Try to decode input as a direct dispatch or dispatchFrom call.
832
+ */
833
+ function decodeDirectDispatch(input) {
834
+ try {
835
+ const decoded = decodeFunctionData({
836
+ abi: panopticPoolV2Abi,
837
+ data: input
838
+ });
839
+ if (decoded.functionName === "dispatch") {
840
+ const args = decoded.args;
841
+ return { positionIds: [...args[1]] };
842
+ }
843
+ if (decoded.functionName === "dispatchFrom") {
844
+ const args = decoded.args;
845
+ return {
846
+ positionIds: [...args[3]],
847
+ targetAccount: args[1]
848
+ };
849
+ }
850
+ return null;
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+ /**
856
+ * 4-byte function selectors for dispatch/dispatchFrom, derived from the generated ABI.
857
+ * Used for scanning raw calldata inside smart contract wallet wrappers.
858
+ */
859
+ const DISPATCH_SELECTOR = toFunctionSelector(panopticPoolV2Abi.find((e) => e.type === "function" && e.name === "dispatch")).slice(2);
860
+ const DISPATCH_FROM_SELECTOR = toFunctionSelector(panopticPoolV2Abi.find((e) => e.type === "function" && e.name === "dispatchFrom")).slice(2);
861
+ /**
862
+ * Scan raw transaction input for embedded dispatch calldata.
863
+ *
864
+ * Smart contract wallets (Safe, Turnkey, ERC-4337) and vault managers
865
+ * wrap dispatch calls inside executeBatch → manage → dispatch chains.
866
+ * Instead of decoding each wrapper layer, we scan the raw hex for the
867
+ * dispatch function selector and attempt to decode from that offset.
868
+ *
869
+ * This handles arbitrary nesting depth without knowing wrapper ABIs.
870
+ */
871
+ function decodeWrappedDispatch(input) {
872
+ const hex = input.slice(2).toLowerCase();
873
+ const selectors = [DISPATCH_SELECTOR, DISPATCH_FROM_SELECTOR];
874
+ const results = [];
875
+ let offset = 0;
876
+ while (offset < hex.length) {
877
+ let earliestIdx = -1;
878
+ for (const selector of selectors) {
879
+ const idx = hex.indexOf(selector, offset);
880
+ if (idx !== -1 && (earliestIdx === -1 || idx < earliestIdx)) earliestIdx = idx;
881
+ }
882
+ if (earliestIdx === -1) break;
883
+ const candidate = `0x${hex.slice(earliestIdx)}`;
884
+ const result = decodeDirectDispatch(candidate);
885
+ if (result) results.push(result);
886
+ offset = earliestIdx + 8;
887
+ }
888
+ return results.length > 0 ? results[results.length - 1] : null;
889
+ }
890
+
891
+ //#endregion
892
+ //#region src/panoptic/v2/sync/getTrackedPositionIds.ts
893
+ /**
894
+ * Get all tracked position IDs for an account from local cache.
895
+ *
896
+ * This function reads from the storage adapter without making RPC calls.
897
+ * The positions returned may include closed positions if the cache is stale.
898
+ * Use `getPositions()` for authoritative on-chain data.
899
+ *
900
+ * @param params - Query parameters
901
+ * @returns Array of position token IDs
902
+ */
903
+ async function getTrackedPositionIds(params) {
904
+ const { chainId, poolAddress, account, storage } = params;
905
+ const key = getPositionsKey(chainId, poolAddress, account);
906
+ const data = await storage.get(key);
907
+ if (!data) return [];
908
+ try {
909
+ const positionIds = jsonSerializer.parse(data);
910
+ return positionIds;
911
+ } catch {
912
+ return [];
913
+ }
914
+ }
915
+ /**
916
+ * Check if a specific position ID is tracked.
917
+ *
918
+ * @param params - Query parameters
919
+ * @param tokenId - Token ID to check
920
+ * @returns Whether the position is tracked
921
+ */
922
+ async function isPositionTracked(params, tokenId) {
923
+ const positionIds = await getTrackedPositionIds(params);
924
+ return positionIds.some((id) => id === tokenId);
925
+ }
926
+ /**
927
+ * Clear all tracked positions for an account.
928
+ *
929
+ * @param params - Query parameters
930
+ */
931
+ async function clearTrackedPositions(params) {
932
+ const { chainId, poolAddress, account, storage } = params;
933
+ const key = getPositionsKey(chainId, poolAddress, account);
934
+ await storage.delete(key);
935
+ }
936
+ /**
937
+ * Get the authoritative list of open position IDs from the chain.
938
+ *
939
+ * Recovers the `finalPositionIdList` from the account's most recent
940
+ * `dispatch()` transaction calldata. This is the on-chain–validated set of
941
+ * open positions (verified against `s_positionsHash` by the contract) and
942
+ * requires only a few RPC calls regardless of account history length.
943
+ *
944
+ * This is the recommended way to get position IDs for `existingPositionIds`
945
+ * (openPosition) and `positionIdList` (closePosition).
946
+ *
947
+ * If `storage` is provided, the local cache is updated with the result.
948
+ *
949
+ * @param params - Query parameters
950
+ * @returns Array of currently open position token IDs, or null if no snapshot was found
951
+ */
952
+ async function getOpenPositionIds(params) {
953
+ const { client, chainId, poolAddress, account, storage, lastDispatchTxHash, fromBlock } = params;
954
+ let snapshot = null;
955
+ if (lastDispatchTxHash) try {
956
+ snapshot = await recoverSnapshotFromTx({
957
+ client,
958
+ transactionHash: lastDispatchTxHash,
959
+ account,
960
+ pool: poolAddress
961
+ });
962
+ } catch {
963
+ snapshot = null;
964
+ }
965
+ if (!snapshot) snapshot = await recoverSnapshot({
966
+ client,
967
+ poolAddress,
968
+ account,
969
+ fromBlock
970
+ });
971
+ if (!snapshot) return null;
972
+ if (storage) {
973
+ const key = getPositionsKey(chainId, poolAddress, account);
974
+ await storage.set(key, jsonSerializer.stringify(snapshot.positionIds));
975
+ }
976
+ return snapshot.positionIds;
977
+ }
978
+
979
+ //#endregion
980
+ //#region src/panoptic/v2/writes/position.ts
981
+ /**
982
+ * Resolve the position ID list: use the explicit param if provided,
983
+ * otherwise read from storage. Throws if neither is available.
984
+ */
985
+ async function resolvePositionIds$1(explicit, storage, chainId, poolAddress, account) {
986
+ if (explicit !== void 0) return explicit;
987
+ if (storage && chainId !== void 0) return getTrackedPositionIds({
988
+ chainId,
989
+ poolAddress,
990
+ account,
991
+ storage
992
+ });
993
+ throw new MissingPositionIdsError();
994
+ }
995
+ /**
996
+ * Save a position ID list to storage.
997
+ */
998
+ async function savePositionIds$1(storage, chainId, poolAddress, account, positionIds) {
999
+ const key = getPositionsKey(chainId, poolAddress, account);
1000
+ await storage.set(key, jsonSerializer.stringify(positionIds));
1001
+ }
1002
+ /**
1003
+ * Open a new position using dispatch.
1004
+ *
1005
+ * @param params - Position parameters
1006
+ * @returns TxResult
1007
+ *
1008
+ * @example
1009
+ * ```typescript
1010
+ * import { MIN_TICK, MAX_TICK } from '@panoptic/sdk'
1011
+ *
1012
+ * const result = await openPosition({
1013
+ * client,
1014
+ * walletClient,
1015
+ * account,
1016
+ * poolAddress,
1017
+ * existingPositionIds: [], // Existing positions held before this mint
1018
+ * tokenId,
1019
+ * positionSize: 1n,
1020
+ * tickLimitLow: MIN_TICK,
1021
+ * tickLimitHigh: MAX_TICK,
1022
+ * swapAtMint: false, // true for covered/single-sided exposure
1023
+ * })
1024
+ * const receipt = await result.wait()
1025
+ * ```
1026
+ */
1027
+ async function openPosition(params) {
1028
+ const { client, walletClient, account, poolAddress, existingPositionIds: explicitIds, tokenId, positionSize, tickLimitLow, tickLimitHigh, spreadLimit = 0n, swapAtMint = false, usePremiaAsCollateral = false, builderCode = 0n, storage, chainId, txOverrides } = params;
1029
+ if (tickLimitLow > tickLimitHigh) throw new InvalidTickLimitsError(tickLimitLow, tickLimitHigh);
1030
+ const existingPositionIds = await resolvePositionIds$1(explicitIds, storage, chainId, poolAddress, account);
1031
+ const positionIdList = [tokenId];
1032
+ const finalPositionIdList = [...existingPositionIds, tokenId];
1033
+ const positionSizes = [positionSize];
1034
+ const tickLimits$1 = swapAtMint ? [
1035
+ Number(tickLimitHigh),
1036
+ Number(tickLimitLow),
1037
+ Number(spreadLimit)
1038
+ ] : [
1039
+ Number(tickLimitLow),
1040
+ Number(tickLimitHigh),
1041
+ Number(spreadLimit)
1042
+ ];
1043
+ return submitWrite({
1044
+ client,
1045
+ walletClient,
1046
+ account,
1047
+ address: poolAddress,
1048
+ abi: panopticPoolV2Abi,
1049
+ functionName: "dispatch",
1050
+ args: [
1051
+ positionIdList,
1052
+ finalPositionIdList,
1053
+ positionSizes,
1054
+ [tickLimits$1],
1055
+ usePremiaAsCollateral,
1056
+ builderCode
1057
+ ],
1058
+ txOverrides
1059
+ });
1060
+ }
1061
+ /**
1062
+ * Build the dispatch calldata for opening a position without executing.
1063
+ * Returns the target address and encoded calldata, suitable for manual
1064
+ * transaction construction (e.g. RFQ broadcast-reconstruct flow).
1065
+ *
1066
+ * @param params - Position parameters (existingPositionIds required, no storage resolution)
1067
+ * @returns { to, data } — contract address and encoded dispatch calldata
1068
+ */
1069
+ function buildOpenPositionCalldata(params) {
1070
+ const { poolAddress, existingPositionIds, tokenId, positionSize, tickLimitLow, tickLimitHigh, spreadLimit = 0n, swapAtMint = false, usePremiaAsCollateral = false, builderCode = 0n } = params;
1071
+ if (tickLimitLow > tickLimitHigh) throw new InvalidTickLimitsError(tickLimitLow, tickLimitHigh);
1072
+ const positionIdList = [tokenId];
1073
+ const finalPositionIdList = [...existingPositionIds, tokenId];
1074
+ const positionSizes = [positionSize];
1075
+ const tickLimits$1 = swapAtMint ? [
1076
+ Number(tickLimitHigh),
1077
+ Number(tickLimitLow),
1078
+ Number(spreadLimit)
1079
+ ] : [
1080
+ Number(tickLimitLow),
1081
+ Number(tickLimitHigh),
1082
+ Number(spreadLimit)
1083
+ ];
1084
+ const data = encodeFunctionData({
1085
+ abi: panopticPoolV2Abi,
1086
+ functionName: "dispatch",
1087
+ args: [
1088
+ positionIdList,
1089
+ finalPositionIdList,
1090
+ positionSizes,
1091
+ [tickLimits$1],
1092
+ usePremiaAsCollateral,
1093
+ builderCode
1094
+ ]
1095
+ });
1096
+ return {
1097
+ to: poolAddress,
1098
+ data
1099
+ };
1100
+ }
1101
+ /**
1102
+ * Open position and wait for confirmation.
1103
+ */
1104
+ async function openPositionAndWait(params) {
1105
+ const result = await openPosition(params);
1106
+ const receipt = await result.wait();
1107
+ const { storage, chainId, account, poolAddress, tokenId, existingPositionIds: explicitIds } = params;
1108
+ if (storage && chainId !== void 0) {
1109
+ const base = explicitIds ?? await getTrackedPositionIds({
1110
+ chainId,
1111
+ poolAddress,
1112
+ account,
1113
+ storage
1114
+ });
1115
+ await savePositionIds$1(storage, chainId, poolAddress, account, [...base, tokenId]);
1116
+ }
1117
+ return receipt;
1118
+ }
1119
+ /**
1120
+ * Close an existing position using dispatch.
1121
+ *
1122
+ * @param params - Close parameters
1123
+ * @returns TxResult
1124
+ */
1125
+ async function closePosition(params) {
1126
+ const { client, walletClient, account, poolAddress, positionIdList: explicitList, tokenId, tickLimitLow, tickLimitHigh, spreadLimit = 0n, swapAtMint = false, usePremiaAsCollateral = false, builderCode = 0n, storage, chainId, txOverrides } = params;
1127
+ if (tickLimitLow > tickLimitHigh) throw new InvalidTickLimitsError(tickLimitLow, tickLimitHigh);
1128
+ const positionIdList = await resolvePositionIds$1(explicitList, storage, chainId, poolAddress, account);
1129
+ const finalPositionIdList = positionIdList.filter((id) => id !== tokenId);
1130
+ const tickLimits$1 = swapAtMint ? [
1131
+ Number(tickLimitHigh),
1132
+ Number(tickLimitLow),
1133
+ Number(spreadLimit)
1134
+ ] : [
1135
+ Number(tickLimitLow),
1136
+ Number(tickLimitHigh),
1137
+ Number(spreadLimit)
1138
+ ];
1139
+ return submitWrite({
1140
+ client,
1141
+ walletClient,
1142
+ account,
1143
+ address: poolAddress,
1144
+ abi: panopticPoolV2Abi,
1145
+ functionName: "dispatch",
1146
+ args: [
1147
+ [tokenId],
1148
+ finalPositionIdList,
1149
+ [0n],
1150
+ [tickLimits$1],
1151
+ usePremiaAsCollateral,
1152
+ builderCode
1153
+ ],
1154
+ txOverrides
1155
+ });
1156
+ }
1157
+ /**
1158
+ * Close position and wait for confirmation.
1159
+ */
1160
+ async function closePositionAndWait(params) {
1161
+ const result = await closePosition(params);
1162
+ const receipt = await result.wait();
1163
+ const { storage, chainId, account, poolAddress, tokenId, positionIdList: explicitList } = params;
1164
+ if (storage && chainId !== void 0) {
1165
+ const base = explicitList ?? await getTrackedPositionIds({
1166
+ chainId,
1167
+ poolAddress,
1168
+ account,
1169
+ storage
1170
+ });
1171
+ await savePositionIds$1(storage, chainId, poolAddress, account, base.filter((id) => id !== tokenId));
1172
+ }
1173
+ return receipt;
1174
+ }
1175
+ /**
1176
+ * Roll a position (close one, open another) in a single transaction.
1177
+ *
1178
+ * @param params - Roll parameters
1179
+ * @returns TxResult
1180
+ */
1181
+ async function rollPosition(params) {
1182
+ const { client, walletClient, account, poolAddress, positionIdList: explicitList, oldTokenId, oldPositionSize, newTokenId, newPositionSize, closeTickLimitLow, closeTickLimitHigh, closeSpreadLimit = 0n, closeSwapAtMint = false, openTickLimitLow, openTickLimitHigh, openSpreadLimit = 0n, openSwapAtMint = false, usePremiaAsCollateral = false, builderCode = 0n, storage, chainId, txOverrides } = params;
1183
+ if (closeTickLimitLow > closeTickLimitHigh) throw new InvalidTickLimitsError(closeTickLimitLow, closeTickLimitHigh);
1184
+ if (openTickLimitLow > openTickLimitHigh) throw new InvalidTickLimitsError(openTickLimitLow, openTickLimitHigh);
1185
+ const positionIdList = await resolvePositionIds$1(explicitList, storage, chainId, poolAddress, account);
1186
+ const finalPositionIdList = positionIdList.filter((id) => id !== oldTokenId).concat([newTokenId]);
1187
+ const positionSizes = [oldPositionSize, newPositionSize];
1188
+ const closeLimits = closeSwapAtMint ? [
1189
+ Number(closeTickLimitHigh),
1190
+ Number(closeTickLimitLow),
1191
+ Number(closeSpreadLimit)
1192
+ ] : [
1193
+ Number(closeTickLimitLow),
1194
+ Number(closeTickLimitHigh),
1195
+ Number(closeSpreadLimit)
1196
+ ];
1197
+ const openLimits = openSwapAtMint ? [
1198
+ Number(openTickLimitHigh),
1199
+ Number(openTickLimitLow),
1200
+ Number(openSpreadLimit)
1201
+ ] : [
1202
+ Number(openTickLimitLow),
1203
+ Number(openTickLimitHigh),
1204
+ Number(openSpreadLimit)
1205
+ ];
1206
+ return submitWrite({
1207
+ client,
1208
+ walletClient,
1209
+ account,
1210
+ address: poolAddress,
1211
+ abi: panopticPoolV2Abi,
1212
+ functionName: "dispatch",
1213
+ args: [
1214
+ positionIdList,
1215
+ finalPositionIdList,
1216
+ positionSizes,
1217
+ [closeLimits, openLimits],
1218
+ usePremiaAsCollateral,
1219
+ builderCode
1220
+ ],
1221
+ txOverrides
1222
+ });
1223
+ }
1224
+ /**
1225
+ * Roll position and wait for confirmation.
1226
+ */
1227
+ async function rollPositionAndWait(params) {
1228
+ const result = await rollPosition(params);
1229
+ const receipt = await result.wait();
1230
+ const { storage, chainId, account, poolAddress, oldTokenId, newTokenId, positionIdList: explicitList } = params;
1231
+ if (storage && chainId !== void 0) {
1232
+ const base = explicitList ?? await getTrackedPositionIds({
1233
+ chainId,
1234
+ poolAddress,
1235
+ account,
1236
+ storage
1237
+ });
1238
+ const updated = base.filter((id) => id !== oldTokenId).concat([newTokenId]);
1239
+ await savePositionIds$1(storage, chainId, poolAddress, account, updated);
1240
+ }
1241
+ return receipt;
1242
+ }
1243
+
1244
+ //#endregion
1245
+ //#region src/panoptic/v2/writes/dispatch.ts
1246
+ /**
1247
+ * Execute a raw dispatch operation.
1248
+ * This is the low-level function for multi-position operations.
1249
+ *
1250
+ * @param params - Dispatch parameters
1251
+ * @returns TxResult
1252
+ *
1253
+ * @example
1254
+ * ```typescript
1255
+ * // Open multiple positions atomically
1256
+ * const result = await dispatch({
1257
+ * client,
1258
+ * walletClient,
1259
+ * account,
1260
+ * poolAddress,
1261
+ * positionIdList: [],
1262
+ * finalPositionIdList: [tokenId1, tokenId2],
1263
+ * positionSizes: [size1, size2],
1264
+ * tickAndSpreadLimits: [limits1, limits2],
1265
+ * })
1266
+ * ```
1267
+ */
1268
+ async function dispatch(params) {
1269
+ const { client, walletClient, account, poolAddress, positionIdList, finalPositionIdList, positionSizes, tickAndSpreadLimits, usePremiaAsCollateral = false, builderCode = 0n, txOverrides } = params;
1270
+ return submitWrite({
1271
+ client,
1272
+ walletClient,
1273
+ account,
1274
+ address: poolAddress,
1275
+ abi: panopticPoolV2Abi,
1276
+ functionName: "dispatch",
1277
+ args: [
1278
+ positionIdList,
1279
+ finalPositionIdList,
1280
+ positionSizes.map((s) => BigInt(s)),
1281
+ tickAndSpreadLimits.map((t) => [
1282
+ Number(t[0]),
1283
+ Number(t[1]),
1284
+ Number(t[2])
1285
+ ]),
1286
+ usePremiaAsCollateral,
1287
+ builderCode
1288
+ ],
1289
+ txOverrides
1290
+ });
1291
+ }
1292
+ /**
1293
+ * Execute dispatch and wait for confirmation.
1294
+ */
1295
+ async function dispatchAndWait(params) {
1296
+ const result = await dispatch(params);
1297
+ return result.wait();
1298
+ }
1299
+
1300
+ //#endregion
1301
+ //#region src/panoptic/v2/batch/validate.ts
1302
+ /**
1303
+ * Run all batch-level and item-level validations.
1304
+ * Pure function; never throws. Empty array means "ready to build".
1305
+ */
1306
+ function validateBatch(params) {
1307
+ const { items, existingPositionIds } = params;
1308
+ const diagnostics = [];
1309
+ if (items.length === 0) {
1310
+ diagnostics.push({
1311
+ itemIndex: -1n,
1312
+ tokenId: 0n,
1313
+ code: "empty-batch",
1314
+ message: "Batch is empty."
1315
+ });
1316
+ return diagnostics;
1317
+ }
1318
+ const head = items[0];
1319
+ if (!head) return diagnostics;
1320
+ const firstPool = head.poolAddress.toLowerCase();
1321
+ const existing = new Set(existingPositionIds);
1322
+ const seen = new Map();
1323
+ items.forEach((item, idx) => {
1324
+ const itemIndex = BigInt(idx);
1325
+ if (item.poolAddress.toLowerCase() !== firstPool) diagnostics.push({
1326
+ itemIndex,
1327
+ tokenId: item.tokenId,
1328
+ code: "cross-pool",
1329
+ message: `Item ${idx} targets pool ${item.poolAddress}, expected ${head.poolAddress}.`
1330
+ });
1331
+ if (item.tickLimitLow > item.tickLimitHigh) diagnostics.push({
1332
+ itemIndex,
1333
+ tokenId: item.tokenId,
1334
+ code: "invalid-tick-limits",
1335
+ message: `Item ${idx} tickLimitLow (${item.tickLimitLow}) > tickLimitHigh (${item.tickLimitHigh}).`
1336
+ });
1337
+ if (item.kind === "mint") {
1338
+ if (item.positionSize <= 0n) diagnostics.push({
1339
+ itemIndex,
1340
+ tokenId: item.tokenId,
1341
+ code: "invalid-position-size",
1342
+ message: `Mint at index ${idx} must have positionSize > 0.`
1343
+ });
1344
+ if (existing.has(item.tokenId)) diagnostics.push({
1345
+ itemIndex,
1346
+ tokenId: item.tokenId,
1347
+ code: "mint-already-onchain",
1348
+ message: `Mint at index ${idx} targets tokenId ${item.tokenId} which is already open on-chain.`
1349
+ });
1350
+ } else if (!existing.has(item.tokenId)) diagnostics.push({
1351
+ itemIndex,
1352
+ tokenId: item.tokenId,
1353
+ code: "burn-not-found",
1354
+ message: `Burn at index ${idx} targets tokenId ${item.tokenId} which is not in the account's positions.`
1355
+ });
1356
+ const prior = seen.get(item.tokenId);
1357
+ if (prior !== void 0) diagnostics.push({
1358
+ itemIndex,
1359
+ tokenId: item.tokenId,
1360
+ code: "duplicate-tokenid-in-batch",
1361
+ message: `Item ${idx} duplicates tokenId ${item.tokenId} also at index ${prior}.`
1362
+ });
1363
+ else seen.set(item.tokenId, itemIndex);
1364
+ });
1365
+ return diagnostics;
1366
+ }
1367
+
1368
+ //#endregion
1369
+ //#region src/panoptic/v2/batch/build.ts
1370
+ /**
1371
+ * Build dispatch() args from a list of batch ops + the account's current
1372
+ * on-chain positionIdList. Returns diagnostics instead of throwing so callers
1373
+ * (UIs, bots) can render conflicts before deciding what to do.
1374
+ *
1375
+ * The order in `items` is preserved as the operation order in `positionIdList`,
1376
+ * `positionSizes`, and `tickAndSpreadLimits`. `finalPositionIdList` is the
1377
+ * post-execution state: existing minus burns, plus mints.
1378
+ */
1379
+ function buildBatchDispatchArgs(params) {
1380
+ const { items, existingPositionIds, usePremiaAsCollateral = false, builderCode = 0n } = params;
1381
+ const diagnostics = validateBatch({
1382
+ items,
1383
+ existingPositionIds
1384
+ });
1385
+ if (diagnostics.length > 0) return {
1386
+ args: null,
1387
+ diagnostics
1388
+ };
1389
+ const positionIdList = items.map((i) => i.tokenId);
1390
+ const positionSizes = items.map((i) => i.kind === "mint" ? i.positionSize : 0n);
1391
+ const tickAndSpreadLimits = items.map((i) => {
1392
+ const low = i.tickLimitLow <= i.tickLimitHigh ? i.tickLimitLow : i.tickLimitHigh;
1393
+ const high = i.tickLimitLow <= i.tickLimitHigh ? i.tickLimitHigh : i.tickLimitLow;
1394
+ const spread = i.kind === "mint" ? i.spreadLimit : 0n;
1395
+ return i.swapAtMint ? [
1396
+ high,
1397
+ low,
1398
+ spread
1399
+ ] : [
1400
+ low,
1401
+ high,
1402
+ spread
1403
+ ];
1404
+ });
1405
+ const mintIds = items.filter((i) => i.kind === "mint").map((i) => i.tokenId);
1406
+ const burnIds = new Set(items.filter((i) => i.kind === "burn").map((i) => i.tokenId));
1407
+ const mintIdSet = new Set(mintIds);
1408
+ const finalPositionIdList = [...existingPositionIds.filter((id) => !burnIds.has(id) && !mintIdSet.has(id)), ...mintIds];
1409
+ return {
1410
+ args: {
1411
+ positionIdList,
1412
+ finalPositionIdList,
1413
+ positionSizes,
1414
+ tickAndSpreadLimits,
1415
+ usePremiaAsCollateral,
1416
+ builderCode
1417
+ },
1418
+ diagnostics: []
1419
+ };
1420
+ }
1421
+
1422
+ //#endregion
1423
+ //#region src/panoptic/v2/writes/executeBatchDispatch.ts
1424
+ /**
1425
+ * Build dispatch args from a batch and submit them as a single transaction.
1426
+ *
1427
+ * Throws `BatchValidationError` (with the full diagnostics list) when the batch
1428
+ * fails validation. Callers that want to render diagnostics in UI should run
1429
+ * `buildBatchDispatchArgs` or `simulateBatchDispatch` first.
1430
+ */
1431
+ async function executeBatchDispatch(params) {
1432
+ const { client, walletClient, account, poolAddress, items, existingPositionIds, usePremiaAsCollateral = false, builderCode = 0n, txOverrides } = params;
1433
+ const { args, diagnostics } = buildBatchDispatchArgs({
1434
+ items,
1435
+ existingPositionIds,
1436
+ usePremiaAsCollateral,
1437
+ builderCode
1438
+ });
1439
+ if (args === null) throw new BatchValidationError(diagnostics);
1440
+ return dispatch({
1441
+ client,
1442
+ walletClient,
1443
+ account,
1444
+ poolAddress,
1445
+ positionIdList: args.positionIdList,
1446
+ finalPositionIdList: args.finalPositionIdList,
1447
+ positionSizes: args.positionSizes,
1448
+ tickAndSpreadLimits: args.tickAndSpreadLimits,
1449
+ usePremiaAsCollateral: args.usePremiaAsCollateral,
1450
+ builderCode: args.builderCode,
1451
+ txOverrides
1452
+ });
1453
+ }
1454
+ /**
1455
+ * Execute a batch dispatch and wait for confirmation.
1456
+ */
1457
+ async function executeBatchDispatchAndWait(params) {
1458
+ const result = await executeBatchDispatch(params);
1459
+ return result.wait();
1460
+ }
1461
+
1462
+ //#endregion
1463
+ //#region src/panoptic/v2/writes/liquidate.ts
1464
+ /**
1465
+ * Liquidate an undercollateralized account using dispatchFrom.
1466
+ *
1467
+ * The liquidator calls dispatchFrom to close the liquidatee's positions
1468
+ * and receive a bonus.
1469
+ *
1470
+ * @param params - Liquidation parameters
1471
+ * @returns TxResult
1472
+ *
1473
+ * @example
1474
+ * ```typescript
1475
+ * const result = await liquidate({
1476
+ * client,
1477
+ * walletClient,
1478
+ * account: liquidatorAddress,
1479
+ * poolAddress,
1480
+ * liquidatee: undercollateralizedAccount,
1481
+ * positionIdListFrom: liquidatorPositions,
1482
+ * positionIdListTo: liquidateePositions,
1483
+ * positionIdListToFinal: [], // Close all positions
1484
+ * })
1485
+ * const receipt = await result.wait()
1486
+ * ```
1487
+ */
1488
+ async function liquidate(params) {
1489
+ const { client, walletClient, account, poolAddress, liquidatee, positionIdListFrom, positionIdListTo, positionIdListToFinal, usePremiaAsCollateral = 0n, txOverrides } = params;
1490
+ return submitWrite({
1491
+ client,
1492
+ walletClient,
1493
+ account,
1494
+ address: poolAddress,
1495
+ abi: panopticPoolV2Abi,
1496
+ functionName: "dispatchFrom",
1497
+ args: [
1498
+ positionIdListFrom,
1499
+ liquidatee,
1500
+ positionIdListTo,
1501
+ positionIdListToFinal,
1502
+ usePremiaAsCollateral
1503
+ ],
1504
+ txOverrides
1505
+ });
1506
+ }
1507
+ /**
1508
+ * Liquidate and wait for confirmation.
1509
+ */
1510
+ async function liquidateAndWait(params) {
1511
+ const result = await liquidate(params);
1512
+ return result.wait();
1513
+ }
1514
+
1515
+ //#endregion
1516
+ //#region src/panoptic/v2/bot/index.ts
1517
+ /**
1518
+ * Assert that data is fresh (not stale).
1519
+ *
1520
+ * Throws StaleDataError if the data's block timestamp is older than maxAgeSeconds.
1521
+ *
1522
+ * @param data - Any SDK data with _meta field
1523
+ * @param maxAgeSeconds - Maximum allowed age in seconds
1524
+ * @param currentTimestamp - Current timestamp (defaults to Date.now() / 1000)
1525
+ * @throws StaleDataError if data is stale
1526
+ *
1527
+ * @example
1528
+ * ```typescript
1529
+ * const pool = await getPool({ client, poolAddress })
1530
+ * assertFresh(pool, 60) // Must be < 60 seconds old
1531
+ * ```
1532
+ */
1533
+ function assertFresh(data, maxAgeSeconds, currentTimestamp) {
1534
+ const maxAge = BigInt(maxAgeSeconds);
1535
+ const now = currentTimestamp !== void 0 ? BigInt(currentTimestamp) : BigInt(Math.floor(Date.now() / 1e3));
1536
+ const blockTimestamp = data._meta.blockTimestamp;
1537
+ const age = now - blockTimestamp;
1538
+ if (age > maxAge) throw new StaleDataError(blockTimestamp, now, age);
1539
+ }
1540
+ /**
1541
+ * Assert that a pool is healthy.
1542
+ *
1543
+ * Throws UnhealthyPoolError if the pool health status is not 'active'.
1544
+ *
1545
+ * @param pool - Pool data from getPool()
1546
+ * @throws UnhealthyPoolError if pool is unhealthy (low_liquidity or paused)
1547
+ *
1548
+ * @example
1549
+ * ```typescript
1550
+ * const pool = await getPool({ client, poolAddress })
1551
+ * assertHealthy(pool) // Throws if pool is paused or has low liquidity
1552
+ * ```
1553
+ */
1554
+ function assertHealthy(pool) {
1555
+ if (pool.healthStatus !== "active") throw new UnhealthyPoolError(pool.healthStatus);
1556
+ }
1557
+ /**
1558
+ * Assert that trading is allowed on a pool.
1559
+ *
1560
+ * Checks both pool health and safe mode status. Throws appropriate errors
1561
+ * if trading is restricted.
1562
+ *
1563
+ * @param pool - Pool data from getPool()
1564
+ * @param safeMode - Optional SafeModeState from getSafeMode()
1565
+ * @throws UnhealthyPoolError if pool is unhealthy
1566
+ * @throws SafeModeError if pool is in restricted or emergency safe mode
1567
+ *
1568
+ * @example
1569
+ * ```typescript
1570
+ * const pool = await getPool({ client, poolAddress })
1571
+ * const safeMode = await getSafeMode({ client, poolAddress })
1572
+ * assertTradeable(pool, safeMode)
1573
+ * ```
1574
+ */
1575
+ function assertTradeable(pool, safeMode) {
1576
+ assertHealthy(pool);
1577
+ if (safeMode && safeMode.mode !== "normal") throw new SafeModeError(safeMode.mode, safeMode.reason ?? "Trading restricted");
1578
+ }
1579
+ /**
1580
+ * Assert that minting is allowed.
1581
+ *
1582
+ * @param safeMode - SafeModeState from getSafeMode()
1583
+ * @throws SafeModeError if minting is not allowed
1584
+ *
1585
+ * @example
1586
+ * ```typescript
1587
+ * const safeMode = await getSafeMode({ client, poolAddress })
1588
+ * assertCanMint(safeMode)
1589
+ * await openPosition({ ... })
1590
+ * ```
1591
+ */
1592
+ function assertCanMint(safeMode) {
1593
+ if (!safeMode.canMint) throw new SafeModeError(safeMode.mode, safeMode.reason ?? "Minting not allowed");
1594
+ }
1595
+ /**
1596
+ * Assert that burning is allowed.
1597
+ *
1598
+ * @param safeMode - SafeModeState from getSafeMode()
1599
+ * @throws SafeModeError if burning is not allowed
1600
+ */
1601
+ function assertCanBurn(safeMode) {
1602
+ if (!safeMode.canBurn) throw new SafeModeError(safeMode.mode, safeMode.reason ?? "Burning not allowed");
1603
+ }
1604
+ /**
1605
+ * Assert that liquidations are allowed.
1606
+ *
1607
+ * @param safeMode - SafeModeState from getSafeMode()
1608
+ * @throws SafeModeError if liquidations are not allowed
1609
+ */
1610
+ function assertCanLiquidate(safeMode) {
1611
+ if (!safeMode.canLiquidate) throw new SafeModeError(safeMode.mode, safeMode.reason ?? "Liquidations not allowed");
1612
+ }
1613
+ /**
1614
+ * Assert that force exercise is allowed.
1615
+ *
1616
+ * @param safeMode - SafeModeState from getSafeMode()
1617
+ * @throws SafeModeError if force exercise is not allowed
1618
+ */
1619
+ function assertCanForceExercise(safeMode) {
1620
+ if (!safeMode.canForceExercise) throw new SafeModeError(safeMode.mode, safeMode.reason ?? "Force exercise not allowed");
1621
+ }
1622
+ /**
1623
+ * Common retryable RPC error codes.
1624
+ */
1625
+ const RETRYABLE_RPC_CODES = new Set([
1626
+ -32e3,
1627
+ -32001,
1628
+ -32002,
1629
+ -32003,
1630
+ -32005,
1631
+ -32097,
1632
+ -32098,
1633
+ -32099
1634
+ ]);
1635
+ /**
1636
+ * Common retryable error message patterns.
1637
+ */
1638
+ const RETRYABLE_PATTERNS = [
1639
+ /timeout/i,
1640
+ /timed out/i,
1641
+ /rate limit/i,
1642
+ /too many requests/i,
1643
+ /429/i,
1644
+ /503/i,
1645
+ /504/i,
1646
+ /502/i,
1647
+ /connection refused/i,
1648
+ /connection reset/i,
1649
+ /network error/i,
1650
+ /econnreset/i,
1651
+ /econnrefused/i,
1652
+ /etimedout/i,
1653
+ /socket hang up/i,
1654
+ /temporarily unavailable/i,
1655
+ /service unavailable/i,
1656
+ /server error/i,
1657
+ /internal error/i,
1658
+ /nonce too low/i,
1659
+ /replacement transaction underpriced/i,
1660
+ /already known/i
1661
+ ];
1662
+ /**
1663
+ * Check if an error is a retryable RPC error.
1664
+ *
1665
+ * Returns true for transient errors that may succeed on retry:
1666
+ * - Timeouts
1667
+ * - Rate limits
1668
+ * - Connection errors
1669
+ * - Server errors (5xx)
1670
+ * - Nonce errors (recoverable with correct nonce)
1671
+ *
1672
+ * @param error - The error to check
1673
+ * @returns True if the error is likely retryable
1674
+ *
1675
+ * @example
1676
+ * ```typescript
1677
+ * try {
1678
+ * await openPosition({ ... })
1679
+ * } catch (error) {
1680
+ * if (isRetryableRpcError(error)) {
1681
+ * // Wait and retry
1682
+ * await sleep(1000)
1683
+ * await openPosition({ ... })
1684
+ * } else {
1685
+ * throw error // Non-retryable, propagate
1686
+ * }
1687
+ * }
1688
+ * ```
1689
+ */
1690
+ function isRetryableRpcError(error) {
1691
+ if (error === null || error === void 0) return false;
1692
+ if (typeof error === "object") {
1693
+ const errorObj = error;
1694
+ if (typeof errorObj.code === "number" && RETRYABLE_RPC_CODES.has(errorObj.code)) return true;
1695
+ if (errorObj.cause && isRetryableRpcError(errorObj.cause)) return true;
1696
+ if (errorObj.details && typeof errorObj.details === "object") {
1697
+ const details = errorObj.details;
1698
+ if (typeof details.code === "number" && RETRYABLE_RPC_CODES.has(details.code)) return true;
1699
+ }
1700
+ }
1701
+ const message = getErrorMessage(error);
1702
+ if (message) {
1703
+ for (const pattern of RETRYABLE_PATTERNS) if (pattern.test(message)) return true;
1704
+ }
1705
+ return false;
1706
+ }
1707
+ /**
1708
+ * Extract error message from various error types.
1709
+ */
1710
+ function getErrorMessage(error) {
1711
+ if (typeof error === "string") return error;
1712
+ if (error instanceof Error) return error.message;
1713
+ if (typeof error === "object" && error !== null) {
1714
+ const errorObj = error;
1715
+ if (typeof errorObj.message === "string") return errorObj.message;
1716
+ if (typeof errorObj.shortMessage === "string") return errorObj.shortMessage;
1717
+ }
1718
+ return void 0;
1719
+ }
1720
+ /**
1721
+ * Check if an error is a nonce-related error that can be recovered.
1722
+ *
1723
+ * @param error - The error to check
1724
+ * @returns True if this is a nonce error
1725
+ */
1726
+ function isNonceError(error) {
1727
+ const message = getErrorMessage(error);
1728
+ if (!message) return false;
1729
+ return /nonce too low/i.test(message) || /nonce too high/i.test(message) || /already known/i.test(message);
1730
+ }
1731
+ /**
1732
+ * Check if an error is a gas-related error that can be recovered.
1733
+ *
1734
+ * @param error - The error to check
1735
+ * @returns True if this is a gas error
1736
+ */
1737
+ function isGasError(error) {
1738
+ const message = getErrorMessage(error);
1739
+ if (!message) return false;
1740
+ return /replacement transaction underpriced/i.test(message) || /gas too low/i.test(message) || /intrinsic gas too low/i.test(message) || /max fee per gas less than block base fee/i.test(message);
1741
+ }
1742
+
1743
+ //#endregion
1744
+ //#region src/panoptic/v2/sync/reorgHandling.ts
1745
+ /**
1746
+ * Detect chain reorganization by comparing stored block hash with current chain.
1747
+ *
1748
+ * @param params - Detection parameters
1749
+ * @returns Reorg detection result
1750
+ */
1751
+ async function detectReorg(params) {
1752
+ const { client, chainId, poolAddress, account, storage } = params;
1753
+ const checkpointKey = getSyncCheckpointKey(chainId, poolAddress, account);
1754
+ const checkpointData = await storage.get(checkpointKey);
1755
+ if (!checkpointData) return { detected: false };
1756
+ const checkpoint = jsonSerializer.parse(checkpointData);
1757
+ try {
1758
+ const block = await client.getBlock({ blockNumber: checkpoint.lastBlock });
1759
+ if (block.hash !== checkpoint.lastBlockHash) return {
1760
+ detected: true,
1761
+ reorgBlock: checkpoint.lastBlock,
1762
+ expectedHash: checkpoint.lastBlockHash,
1763
+ actualHash: block.hash,
1764
+ blocksToResync: REORG_DEPTH
1765
+ };
1766
+ return { detected: false };
1767
+ } catch (error) {
1768
+ return {
1769
+ detected: true,
1770
+ reorgBlock: checkpoint.lastBlock,
1771
+ expectedHash: checkpoint.lastBlockHash,
1772
+ actualHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
1773
+ blocksToResync: REORG_DEPTH
1774
+ };
1775
+ }
1776
+ }
1777
+ /**
1778
+ * Calculate the safe resync block after a reorg.
1779
+ *
1780
+ * @param reorgBlock - Block where reorg was detected
1781
+ * @param reorgDepth - Number of blocks to roll back (default: REORG_DEPTH = 128)
1782
+ * @returns Safe block to resync from
1783
+ */
1784
+ function calculateResyncBlock(reorgBlock, reorgDepth = REORG_DEPTH) {
1785
+ if (reorgBlock <= reorgDepth) return 0n;
1786
+ return reorgBlock - reorgDepth;
1787
+ }
1788
+ /**
1789
+ * Save a sync checkpoint.
1790
+ *
1791
+ * @param params - Checkpoint parameters
1792
+ */
1793
+ async function saveCheckpoint(params) {
1794
+ const { storage, chainId, poolAddress, account, lastBlock, lastBlockHash, positionIds } = params;
1795
+ const checkpoint = {
1796
+ chainId,
1797
+ poolAddress,
1798
+ account,
1799
+ lastBlock,
1800
+ lastBlockHash,
1801
+ positionIds,
1802
+ createdAt: BigInt(Math.floor(Date.now() / 1e3))
1803
+ };
1804
+ const key = getSyncCheckpointKey(chainId, poolAddress, account);
1805
+ await storage.set(key, jsonSerializer.stringify(checkpoint));
1806
+ }
1807
+ /**
1808
+ * Load a sync checkpoint.
1809
+ *
1810
+ * @param storage - Storage adapter
1811
+ * @param chainId - Chain ID
1812
+ * @param poolAddress - Pool address
1813
+ * @param account - Account address
1814
+ * @returns Checkpoint or null if not found
1815
+ */
1816
+ async function loadCheckpoint(storage, chainId, poolAddress, account) {
1817
+ const key = getSyncCheckpointKey(chainId, poolAddress, account);
1818
+ const data = await storage.get(key);
1819
+ if (!data) return null;
1820
+ return jsonSerializer.parse(data);
1821
+ }
1822
+ /**
1823
+ * Clear a sync checkpoint.
1824
+ *
1825
+ * @param storage - Storage adapter
1826
+ * @param chainId - Chain ID
1827
+ * @param poolAddress - Pool address
1828
+ * @param account - Account address
1829
+ */
1830
+ async function clearCheckpoint(storage, chainId, poolAddress, account) {
1831
+ const key = getSyncCheckpointKey(chainId, poolAddress, account);
1832
+ await storage.delete(key);
1833
+ }
1834
+ /**
1835
+ * Verify block continuity for a range of blocks.
1836
+ * This checks that each block's parentHash matches the previous block's hash.
1837
+ *
1838
+ * @param client - viem public client
1839
+ * @param fromBlock - Starting block
1840
+ * @param toBlock - Ending block
1841
+ * @returns True if blocks form a valid chain
1842
+ */
1843
+ async function verifyBlockContinuity(client, fromBlock, toBlock) {
1844
+ if (fromBlock >= toBlock) return true;
1845
+ const sampleSize = Math.min(5, Number(toBlock - fromBlock));
1846
+ const step = (toBlock - fromBlock) / BigInt(sampleSize);
1847
+ let previousHash = null;
1848
+ for (let i = 0; i <= sampleSize; i++) {
1849
+ const blockNumber = fromBlock + step * BigInt(i);
1850
+ const block = await client.getBlock({ blockNumber });
1851
+ if (previousHash !== null && i > 0) {}
1852
+ previousHash = block.hash;
1853
+ }
1854
+ return true;
1855
+ }
1856
+
1857
+ //#endregion
1858
+ //#region src/panoptic/v2/sync/syncPositions.ts
1859
+ /**
1860
+ * Synchronize positions for an account.
1861
+ *
1862
+ * This function:
1863
+ * 1. Checks for existing checkpoint
1864
+ * 2. If no checkpoint: recovers from dispatch() calldata or falls back to full event scan
1865
+ * 3. If checkpoint exists: detects reorgs and syncs incrementally
1866
+ * 4. Persists positions and checkpoint to storage
1867
+ *
1868
+ * @param params - Sync parameters
1869
+ * @returns Sync result with position count and sync metadata
1870
+ * @throws SyncTimeoutError if sync exceeds timeout
1871
+ * @throws ProviderLagError if provider is behind minBlockNumber
1872
+ * @throws PositionSnapshotNotFoundError if no positions found and no snapshot available
1873
+ */
1874
+ async function syncPositions(params) {
1875
+ const { client, chainId, poolAddress, account, storage, syncTimeout = 300000n, onUpdate, minBlockNumber, snapshotTxHash } = params;
1876
+ const startTime = Date.now();
1877
+ const checkTimeout = () => {
1878
+ const elapsed = BigInt(Date.now() - startTime);
1879
+ if (elapsed > syncTimeout) throw new SyncTimeoutError(elapsed, toBlock, 0n);
1880
+ };
1881
+ const latestBlock = await client.getBlockNumber();
1882
+ const toBlock = params.toBlock ?? latestBlock;
1883
+ if (minBlockNumber !== void 0 && latestBlock < minBlockNumber) throw new ProviderLagError(minBlockNumber, latestBlock);
1884
+ const checkpoint = await loadCheckpoint(storage, chainId, poolAddress, account);
1885
+ if (checkpoint) {
1886
+ const reorgResult = await detectReorg({
1887
+ client,
1888
+ chainId,
1889
+ poolAddress,
1890
+ account,
1891
+ storage
1892
+ });
1893
+ if (reorgResult.detected) onUpdate?.({
1894
+ type: "reorg-detected",
1895
+ blockNumber: reorgResult.reorgBlock ?? 0n
1896
+ });
1897
+ }
1898
+ checkTimeout();
1899
+ const snapshotResult = await getOpenPositionIds({
1900
+ client,
1901
+ chainId,
1902
+ poolAddress,
1903
+ account,
1904
+ storage,
1905
+ lastDispatchTxHash: snapshotTxHash,
1906
+ fromBlock: checkpoint?.lastBlock ?? params.fromBlock
1907
+ });
1908
+ let positionIds;
1909
+ if (snapshotResult === null && checkpoint && checkpoint.positionIds.length > 0) positionIds = checkpoint.positionIds;
1910
+ else positionIds = snapshotResult ?? [];
1911
+ if (positionIds.length === 0) {
1912
+ const hasAnyEvents = await accountHasPositionEvents({
1913
+ client,
1914
+ poolAddress,
1915
+ account,
1916
+ fromBlock: params.fromBlock,
1917
+ toBlock
1918
+ });
1919
+ if (!hasAnyEvents) {
1920
+ const finalBlock$1 = await client.getBlock({ blockNumber: toBlock });
1921
+ const poolMetaKey$1 = getPoolMetaKey(chainId, poolAddress);
1922
+ const existingPoolMeta$1 = await storage.get(poolMetaKey$1);
1923
+ if (!existingPoolMeta$1) await fetchAndStorePoolMeta(client, poolAddress, poolMetaKey$1, storage);
1924
+ await saveCheckpoint({
1925
+ storage,
1926
+ chainId,
1927
+ poolAddress,
1928
+ account,
1929
+ lastBlock: toBlock,
1930
+ lastBlockHash: finalBlock$1.hash,
1931
+ positionIds: []
1932
+ });
1933
+ const positionsKey$1 = getPositionsKey(chainId, poolAddress, account);
1934
+ await storage.set(positionsKey$1, jsonSerializer.stringify([]));
1935
+ return {
1936
+ lastSyncedBlock: toBlock,
1937
+ lastSyncedBlockHash: finalBlock$1.hash,
1938
+ positionCount: 0n,
1939
+ positionIds: [],
1940
+ incremental: false,
1941
+ durationMs: BigInt(Date.now() - startTime)
1942
+ };
1943
+ }
1944
+ }
1945
+ checkTimeout();
1946
+ const finalBlock = await client.getBlock({ blockNumber: toBlock });
1947
+ const poolMetaKey = getPoolMetaKey(chainId, poolAddress);
1948
+ const existingPoolMeta = await storage.get(poolMetaKey);
1949
+ if (!existingPoolMeta) await fetchAndStorePoolMeta(client, poolAddress, poolMetaKey, storage);
1950
+ if (positionIds.length > 0) {
1951
+ const { positions } = await getPositions({
1952
+ client,
1953
+ poolAddress,
1954
+ owner: account,
1955
+ tokenIds: positionIds,
1956
+ blockNumber: toBlock
1957
+ });
1958
+ for (const pos of positions) {
1959
+ const positionMetaKey = getPositionMetaKey(chainId, poolAddress, pos.tokenId);
1960
+ const storedData = {
1961
+ tokenId: pos.tokenId,
1962
+ positionSize: pos.positionSize,
1963
+ legs: pos.legs,
1964
+ tickAtMint: pos.tickAtMint,
1965
+ poolUtilization0AtMint: pos.poolUtilization0AtMint,
1966
+ poolUtilization1AtMint: pos.poolUtilization1AtMint,
1967
+ timestampAtMint: pos.timestampAtMint,
1968
+ blockNumberAtMint: pos.blockNumberAtMint,
1969
+ swapAtMint: pos.swapAtMint
1970
+ };
1971
+ await storage.set(positionMetaKey, jsonSerializer.stringify(storedData));
1972
+ }
1973
+ }
1974
+ await saveCheckpoint({
1975
+ storage,
1976
+ chainId,
1977
+ poolAddress,
1978
+ account,
1979
+ lastBlock: toBlock,
1980
+ lastBlockHash: finalBlock.hash,
1981
+ positionIds
1982
+ });
1983
+ const positionsKey = getPositionsKey(chainId, poolAddress, account);
1984
+ await storage.set(positionsKey, jsonSerializer.stringify(positionIds));
1985
+ return {
1986
+ lastSyncedBlock: toBlock,
1987
+ lastSyncedBlockHash: finalBlock.hash,
1988
+ positionCount: BigInt(positionIds.length),
1989
+ positionIds,
1990
+ incremental: !!checkpoint,
1991
+ durationMs: BigInt(Date.now() - startTime)
1992
+ };
1993
+ }
1994
+ /**
1995
+ * Quick check if an account has ANY position events.
1996
+ * Checks OptionMinted, OptionBurnt, ForcedExercised (user), and AccountLiquidated (liquidatee).
1997
+ * All checked fields are indexed so this is fast.
1998
+ *
1999
+ * **Warning:** RPC index lag can cause false negatives for recently minted positions.
2000
+ * If the RPC node's event index is behind the chain tip, this function may return
2001
+ * `false` even though the account has just minted a position. Callers should either
2002
+ * wait a few blocks after minting before calling syncPositions, or skip this
2003
+ * optimization (by providing a `snapshotTxHash`) when freshness is critical.
2004
+ *
2005
+ * @param params - Check parameters
2006
+ * @returns true if account has any position events, false otherwise
2007
+ */
2008
+ async function accountHasPositionEvents(params) {
2009
+ const { client, poolAddress, account, fromBlock = 0n, toBlock } = params;
2010
+ const [mintEvents, burnEvents, forceExerciseEvents, liquidationEvents] = await Promise.all([
2011
+ withRetry(() => client.getLogs({
2012
+ address: poolAddress,
2013
+ event: {
2014
+ type: "event",
2015
+ name: "OptionMinted",
2016
+ inputs: [
2017
+ {
2018
+ type: "address",
2019
+ name: "recipient",
2020
+ indexed: true
2021
+ },
2022
+ {
2023
+ type: "uint256",
2024
+ name: "tokenId",
2025
+ indexed: true
2026
+ },
2027
+ {
2028
+ type: "uint256",
2029
+ name: "balanceData",
2030
+ indexed: false
2031
+ }
2032
+ ]
2033
+ },
2034
+ args: { recipient: account },
2035
+ fromBlock,
2036
+ toBlock
2037
+ })),
2038
+ withRetry(() => client.getLogs({
2039
+ address: poolAddress,
2040
+ event: {
2041
+ type: "event",
2042
+ name: "OptionBurnt",
2043
+ inputs: [
2044
+ {
2045
+ type: "address",
2046
+ name: "recipient",
2047
+ indexed: true
2048
+ },
2049
+ {
2050
+ type: "uint256",
2051
+ name: "tokenId",
2052
+ indexed: true
2053
+ },
2054
+ {
2055
+ type: "uint256",
2056
+ name: "positionSize",
2057
+ indexed: false
2058
+ },
2059
+ {
2060
+ type: "int256[4]",
2061
+ name: "premiaByLeg",
2062
+ indexed: false
2063
+ }
2064
+ ]
2065
+ },
2066
+ args: { recipient: account },
2067
+ fromBlock,
2068
+ toBlock
2069
+ })),
2070
+ withRetry(() => client.getLogs({
2071
+ address: poolAddress,
2072
+ event: {
2073
+ type: "event",
2074
+ name: "ForcedExercised",
2075
+ inputs: [
2076
+ {
2077
+ type: "address",
2078
+ name: "exercisor",
2079
+ indexed: true
2080
+ },
2081
+ {
2082
+ type: "address",
2083
+ name: "user",
2084
+ indexed: true
2085
+ },
2086
+ {
2087
+ type: "uint256",
2088
+ name: "tokenId",
2089
+ indexed: true
2090
+ },
2091
+ {
2092
+ type: "int256",
2093
+ name: "exerciseFee",
2094
+ indexed: false
2095
+ }
2096
+ ]
2097
+ },
2098
+ args: { user: account },
2099
+ fromBlock,
2100
+ toBlock
2101
+ })),
2102
+ withRetry(() => client.getLogs({
2103
+ address: poolAddress,
2104
+ event: {
2105
+ type: "event",
2106
+ name: "AccountLiquidated",
2107
+ inputs: [
2108
+ {
2109
+ type: "address",
2110
+ name: "liquidator",
2111
+ indexed: true
2112
+ },
2113
+ {
2114
+ type: "address",
2115
+ name: "liquidatee",
2116
+ indexed: true
2117
+ },
2118
+ {
2119
+ type: "int256",
2120
+ name: "bonusAmounts",
2121
+ indexed: false
2122
+ }
2123
+ ]
2124
+ },
2125
+ args: { liquidatee: account },
2126
+ fromBlock,
2127
+ toBlock
2128
+ }))
2129
+ ]);
2130
+ return mintEvents.length > 0 || burnEvents.length > 0 || forceExerciseEvents.length > 0 || liquidationEvents.length > 0;
2131
+ }
2132
+ /**
2133
+ * Fetch full pool metadata and store it.
2134
+ * Uses getPoolMetadata to fetch all immutable pool data in minimal RPC calls.
2135
+ */
2136
+ async function fetchAndStorePoolMeta(client, poolAddress, poolMetaKey, storage) {
2137
+ const metadata = await getPoolMetadata({
2138
+ client,
2139
+ poolAddress
2140
+ });
2141
+ const poolMeta = {
2142
+ tickSpacing: metadata.tickSpacing,
2143
+ fee: metadata.fee,
2144
+ poolId: metadata.poolId,
2145
+ collateralToken0Address: metadata.collateralToken0Address,
2146
+ collateralToken1Address: metadata.collateralToken1Address,
2147
+ riskEngineAddress: metadata.riskEngineAddress,
2148
+ token0Asset: metadata.token0Asset,
2149
+ token1Asset: metadata.token1Asset,
2150
+ token0Symbol: metadata.token0Symbol,
2151
+ token1Symbol: metadata.token1Symbol,
2152
+ token0Decimals: metadata.token0Decimals,
2153
+ token1Decimals: metadata.token1Decimals
2154
+ };
2155
+ await storage.set(poolMetaKey, jsonSerializer.stringify(poolMeta));
2156
+ }
2157
+ /**
2158
+ * Retry helper with exponential backoff for transient RPC errors.
2159
+ *
2160
+ * @param fn - Async function to retry
2161
+ * @param maxRetries - Maximum number of retries (default: 3)
2162
+ * @returns The result of the function
2163
+ */
2164
+ async function withRetry(fn, maxRetries = 3) {
2165
+ let lastError;
2166
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
2167
+ return await fn();
2168
+ } catch (error) {
2169
+ lastError = error;
2170
+ if (attempt < maxRetries && isRetryableRpcError(error)) {
2171
+ await new Promise((resolve) => setTimeout(resolve, 1e3 * 2 ** attempt));
2172
+ continue;
2173
+ }
2174
+ throw error;
2175
+ }
2176
+ throw lastError;
2177
+ }
2178
+
2179
+ //#endregion
2180
+ //#region src/panoptic/v2/writes/forceExercise.ts
2181
+ /**
2182
+ * Force exercise an ITM (in-the-money) long position.
2183
+ *
2184
+ * The exercisor calls dispatchFrom to exercise another user's ITM long positions,
2185
+ * paying the exercise fee.
2186
+ *
2187
+ * @param params - Force exercise parameters
2188
+ * @returns TxResult
2189
+ *
2190
+ * @example
2191
+ * ```typescript
2192
+ * const result = await forceExercise({
2193
+ * client,
2194
+ * walletClient,
2195
+ * account: exercisorAddress,
2196
+ * poolAddress,
2197
+ * user: targetAccount,
2198
+ * positionIdListFrom: exercisorPositions,
2199
+ * positionIdListTo: targetPositions,
2200
+ * positionIdListToFinal: [], // Exercise all ITM positions
2201
+ * })
2202
+ * const receipt = await result.wait()
2203
+ * ```
2204
+ */
2205
+ async function forceExercise(params) {
2206
+ const { client, walletClient, account, poolAddress, user, positionIdListFrom, positionIdListTo, positionIdListToFinal, usePremiaAsCollateral = 0n, txOverrides } = params;
2207
+ return submitWrite({
2208
+ client,
2209
+ walletClient,
2210
+ account,
2211
+ address: poolAddress,
2212
+ abi: panopticPoolV2Abi,
2213
+ functionName: "dispatchFrom",
2214
+ args: [
2215
+ positionIdListFrom,
2216
+ user,
2217
+ positionIdListTo,
2218
+ positionIdListToFinal,
2219
+ usePremiaAsCollateral
2220
+ ],
2221
+ txOverrides
2222
+ });
2223
+ }
2224
+ /**
2225
+ * Force exercise and wait for confirmation.
2226
+ *
2227
+ * When `storage` and `chainId` are provided, automatically syncs the
2228
+ * exercisor's positions after the transaction confirms.
2229
+ */
2230
+ async function forceExerciseAndWait(params) {
2231
+ const result = await forceExercise(params);
2232
+ const receipt = await result.wait();
2233
+ const { storage, chainId, client, poolAddress, account } = params;
2234
+ if (storage && chainId !== void 0) await syncPositions({
2235
+ client,
2236
+ chainId,
2237
+ poolAddress,
2238
+ account,
2239
+ storage
2240
+ });
2241
+ return receipt;
2242
+ }
2243
+
2244
+ //#endregion
2245
+ //#region src/panoptic/v2/writes/settle.ts
2246
+ /**
2247
+ * Settle accumulated premia on existing positions.
2248
+ *
2249
+ * This function triggers premium collection without changing position size.
2250
+ * It calls dispatch with unchanged position lists.
2251
+ *
2252
+ * @param params - Settlement parameters
2253
+ * @returns TxResult
2254
+ *
2255
+ * @example
2256
+ * ```typescript
2257
+ * const result = await settleAccumulatedPremia({
2258
+ * client,
2259
+ * walletClient,
2260
+ * account,
2261
+ * poolAddress,
2262
+ * positionIdList: existingPositions,
2263
+ * })
2264
+ * const receipt = await result.wait()
2265
+ * ```
2266
+ */
2267
+ async function settleAccumulatedPremia(params) {
2268
+ const { client, walletClient, account, poolAddress, positionIdList, usePremiaAsCollateral = false, builderCode = 0n, txOverrides } = params;
2269
+ const positionSizes = positionIdList.map(() => 0n);
2270
+ const tickAndSpreadLimits = positionIdList.map(() => [
2271
+ -887272n,
2272
+ 887272n,
2273
+ 0n
2274
+ ]);
2275
+ return submitWrite({
2276
+ client,
2277
+ walletClient,
2278
+ account,
2279
+ address: poolAddress,
2280
+ abi: panopticPoolV2Abi,
2281
+ functionName: "dispatch",
2282
+ args: [
2283
+ positionIdList,
2284
+ positionIdList,
2285
+ positionSizes.map((s) => BigInt(s)),
2286
+ tickAndSpreadLimits.map((t) => [
2287
+ Number(t[0]),
2288
+ Number(t[1]),
2289
+ Number(t[2])
2290
+ ]),
2291
+ usePremiaAsCollateral,
2292
+ builderCode
2293
+ ],
2294
+ txOverrides
2295
+ });
2296
+ }
2297
+ /**
2298
+ * Settle premia and wait for confirmation.
2299
+ */
2300
+ async function settleAccumulatedPremiaAndWait(params) {
2301
+ const result = await settleAccumulatedPremia(params);
2302
+ return result.wait();
2303
+ }
2304
+
2305
+ //#endregion
2306
+ //#region src/panoptic/v2/writes/pokeOracle.ts
2307
+ /**
2308
+ * Poke the oracle to update its state.
2309
+ *
2310
+ * This function can be called to advance the oracle epoch.
2311
+ * Note: The oracle can only be poked once per epoch (64 seconds).
2312
+ *
2313
+ * @param params - Poke oracle parameters
2314
+ * @returns TxResult
2315
+ * @throws OracleRateLimitedError if checkRateLimit is true and oracle was recently poked
2316
+ *
2317
+ * @example
2318
+ * ```typescript
2319
+ * const result = await pokeOracle({
2320
+ * client,
2321
+ * walletClient,
2322
+ * account,
2323
+ * poolAddress,
2324
+ * })
2325
+ * const receipt = await result.wait()
2326
+ * ```
2327
+ */
2328
+ async function pokeOracle(params) {
2329
+ const { client, walletClient, account, poolAddress, checkRateLimit = false, txOverrides } = params;
2330
+ if (checkRateLimit) {
2331
+ const [oracleData, block] = await Promise.all([client.readContract({
2332
+ address: poolAddress,
2333
+ abi: panopticPoolV2Abi,
2334
+ functionName: "getOracleTicks"
2335
+ }), client.getBlock()]);
2336
+ const oraclePack = oracleData[4];
2337
+ const epoch = oraclePack >> 232n;
2338
+ const currentEpoch = block.timestamp / 64n;
2339
+ if (currentEpoch <= epoch) {
2340
+ const lastUpdate = epoch * 64n;
2341
+ throw new OracleRateLimitedError(lastUpdate, block.timestamp);
2342
+ }
2343
+ }
2344
+ return submitWrite({
2345
+ client,
2346
+ walletClient,
2347
+ account,
2348
+ address: poolAddress,
2349
+ abi: panopticPoolV2Abi,
2350
+ functionName: "pokeOracle",
2351
+ args: [],
2352
+ txOverrides
2353
+ });
2354
+ }
2355
+ /**
2356
+ * Poke oracle and wait for confirmation.
2357
+ */
2358
+ async function pokeOracleAndWait(params) {
2359
+ const result = await pokeOracle(params);
2360
+ return result.wait();
2361
+ }
2362
+
2363
+ //#endregion
2364
+ //#region src/panoptic/v2/writes/factory.ts
2365
+ /**
2366
+ * Deploy a new Panoptic pool via the factory.
2367
+ *
2368
+ * @param params - Deployment parameters (versioned: 'v3' or 'v4')
2369
+ * @returns Transaction result with hash and wait function
2370
+ */
2371
+ async function deployNewPool(params) {
2372
+ const { client, walletClient, account, factoryAddress, riskEngine, salt, txOverrides } = params;
2373
+ if (params.version === "v3") return submitWrite({
2374
+ client,
2375
+ walletClient,
2376
+ account,
2377
+ address: factoryAddress,
2378
+ abi: panopticFactoryV3Abi,
2379
+ functionName: "deployNewPool",
2380
+ args: [
2381
+ params.token0,
2382
+ params.token1,
2383
+ params.fee,
2384
+ riskEngine,
2385
+ salt
2386
+ ],
2387
+ txOverrides
2388
+ });
2389
+ return submitWrite({
2390
+ client,
2391
+ walletClient,
2392
+ account,
2393
+ address: factoryAddress,
2394
+ abi: panopticFactoryV4Abi,
2395
+ functionName: "deployNewPool",
2396
+ args: [
2397
+ {
2398
+ currency0: params.poolKey.currency0,
2399
+ currency1: params.poolKey.currency1,
2400
+ fee: Number(params.poolKey.fee),
2401
+ tickSpacing: Number(params.poolKey.tickSpacing),
2402
+ hooks: params.poolKey.hooks
2403
+ },
2404
+ riskEngine,
2405
+ salt
2406
+ ],
2407
+ txOverrides
2408
+ });
2409
+ }
2410
+ /**
2411
+ * Deploy a new Panoptic pool and wait for confirmation.
2412
+ */
2413
+ async function deployNewPoolAndWait(params) {
2414
+ const result = await deployNewPool(params);
2415
+ return result.wait();
2416
+ }
2417
+
2418
+ //#endregion
2419
+ //#region src/panoptic/v2/writes/txManagement.ts
2420
+ /** Default gas price multiplier for replacement (12.5% bump = minimum for replacement) */
2421
+ const DEFAULT_GAS_PRICE_MULTIPLIER = 1.125;
2422
+ /**
2423
+ * Apply a multiplier to a bigint gas value.
2424
+ * Uses integer arithmetic to avoid floating point issues.
2425
+ */
2426
+ function applyMultiplier(value, multiplier) {
2427
+ const bps = BigInt(Math.ceil(multiplier * 1e4));
2428
+ return value * bps / 10000n;
2429
+ }
2430
+ /**
2431
+ * Compute bumped gas parameters from the original transaction.
2432
+ */
2433
+ function computeBumpedGas(originalMaxFeePerGas, originalMaxPriorityFeePerGas, explicitMaxFeePerGas, explicitMaxPriorityFeePerGas, multiplier) {
2434
+ const maxFeePerGas = explicitMaxFeePerGas ?? applyMultiplier(originalMaxFeePerGas ?? 0n, multiplier);
2435
+ const maxPriorityFeePerGas = explicitMaxPriorityFeePerGas ?? applyMultiplier(originalMaxPriorityFeePerGas ?? 0n, multiplier);
2436
+ return {
2437
+ maxFeePerGas,
2438
+ maxPriorityFeePerGas
2439
+ };
2440
+ }
2441
+ /**
2442
+ * Speed up a pending transaction by resubmitting with higher gas.
2443
+ *
2444
+ * Fetches the original transaction, extracts its parameters,
2445
+ * bumps the gas price, and resubmits with the same nonce.
2446
+ *
2447
+ * @param params - Speed up parameters
2448
+ * @returns TxResult for the replacement transaction
2449
+ * @throws Error if the original transaction is not found
2450
+ *
2451
+ * @example
2452
+ * ```typescript
2453
+ * // Speed up with default 12.5% bump
2454
+ * const result = await speedUpTransaction({
2455
+ * client,
2456
+ * walletClient,
2457
+ * hash: pendingTxHash,
2458
+ * })
2459
+ *
2460
+ * // Speed up with explicit gas prices
2461
+ * const result = await speedUpTransaction({
2462
+ * client,
2463
+ * walletClient,
2464
+ * hash: pendingTxHash,
2465
+ * maxFeePerGas: 50_000_000_000n, // 50 gwei
2466
+ * maxPriorityFeePerGas: 3_000_000_000n, // 3 gwei
2467
+ * })
2468
+ * ```
2469
+ */
2470
+ async function speedUpTransaction(params) {
2471
+ const { client, walletClient, hash, maxFeePerGas: explicitMaxFee, maxPriorityFeePerGas: explicitMaxPriority, gasPriceMultiplier = DEFAULT_GAS_PRICE_MULTIPLIER, broadcaster } = params;
2472
+ const tx = await client.getTransaction({ hash });
2473
+ const { maxFeePerGas, maxPriorityFeePerGas } = computeBumpedGas(tx.maxFeePerGas ?? void 0, tx.maxPriorityFeePerGas ?? void 0, explicitMaxFee, explicitMaxPriority, gasPriceMultiplier);
2474
+ if (broadcaster) {
2475
+ const request = await walletClient.prepareTransactionRequest({
2476
+ account: tx.from,
2477
+ to: tx.to ?? void 0,
2478
+ data: tx.input,
2479
+ value: tx.value,
2480
+ nonce: tx.nonce,
2481
+ gas: tx.gas,
2482
+ maxFeePerGas,
2483
+ maxPriorityFeePerGas,
2484
+ chain: walletClient.chain
2485
+ });
2486
+ const signedTx = await walletClient.signTransaction({
2487
+ ...request,
2488
+ account: tx.from
2489
+ });
2490
+ const replacementHash$1 = await broadcaster.broadcast(signedTx);
2491
+ return createTxResult(client, replacementHash$1);
2492
+ }
2493
+ const replacementHash = await walletClient.sendTransaction({
2494
+ account: tx.from,
2495
+ to: tx.to ?? void 0,
2496
+ data: tx.input,
2497
+ value: tx.value,
2498
+ nonce: tx.nonce,
2499
+ gas: tx.gas,
2500
+ maxFeePerGas,
2501
+ maxPriorityFeePerGas,
2502
+ chain: walletClient.chain
2503
+ });
2504
+ return createTxResult(client, replacementHash);
2505
+ }
2506
+ /**
2507
+ * Cancel a pending transaction by sending a 0-value self-transfer
2508
+ * with the same nonce and higher gas price.
2509
+ *
2510
+ * @param params - Cancel parameters
2511
+ * @returns TxResult for the cancellation transaction
2512
+ * @throws Error if the original transaction is not found
2513
+ *
2514
+ * @example
2515
+ * ```typescript
2516
+ * const result = await cancelTransaction({
2517
+ * client,
2518
+ * walletClient,
2519
+ * account,
2520
+ * hash: pendingTxHash,
2521
+ * })
2522
+ * await result.wait()
2523
+ * ```
2524
+ */
2525
+ async function cancelTransaction(params) {
2526
+ const { client, walletClient, account, hash, maxFeePerGas: explicitMaxFee, maxPriorityFeePerGas: explicitMaxPriority, gasPriceMultiplier = DEFAULT_GAS_PRICE_MULTIPLIER, broadcaster } = params;
2527
+ const tx = await client.getTransaction({ hash });
2528
+ const { maxFeePerGas, maxPriorityFeePerGas } = computeBumpedGas(tx.maxFeePerGas ?? void 0, tx.maxPriorityFeePerGas ?? void 0, explicitMaxFee, explicitMaxPriority, gasPriceMultiplier);
2529
+ if (broadcaster) {
2530
+ const request = await walletClient.prepareTransactionRequest({
2531
+ account,
2532
+ to: account,
2533
+ value: 0n,
2534
+ nonce: tx.nonce,
2535
+ gas: 21000n,
2536
+ maxFeePerGas,
2537
+ maxPriorityFeePerGas,
2538
+ chain: walletClient.chain
2539
+ });
2540
+ const signedTx = await walletClient.signTransaction({
2541
+ ...request,
2542
+ account
2543
+ });
2544
+ const cancelHash$1 = await broadcaster.broadcast(signedTx);
2545
+ return createTxResult(client, cancelHash$1);
2546
+ }
2547
+ const cancelHash = await walletClient.sendTransaction({
2548
+ account,
2549
+ to: account,
2550
+ value: 0n,
2551
+ nonce: tx.nonce,
2552
+ gas: 21000n,
2553
+ maxFeePerGas,
2554
+ maxPriorityFeePerGas,
2555
+ chain: walletClient.chain
2556
+ });
2557
+ return createTxResult(client, cancelHash);
2558
+ }
2559
+
2560
+ //#endregion
2561
+ //#region src/panoptic/v2/tokenId/encoding.ts
2562
+ /**
2563
+ * Convert a signed strike tick to unsigned representation for encoding.
2564
+ *
2565
+ * @param strike - The signed strike tick
2566
+ * @returns The unsigned representation
2567
+ */
2568
+ function convertStrikeToUnsigned(strike) {
2569
+ if (strike < 0n) return STRIKE_CONVERSION_FACTOR + strike;
2570
+ return strike;
2571
+ }
2572
+ /**
2573
+ * Convert an unsigned encoded strike back to signed representation.
2574
+ *
2575
+ * @param encodedStrike - The unsigned encoded strike
2576
+ * @returns The signed strike tick
2577
+ */
2578
+ function convertStrikeToSigned(encodedStrike) {
2579
+ if (encodedStrike > 2n ** 23n - 1n) return encodedStrike - STRIKE_CONVERSION_FACTOR;
2580
+ return encodedStrike;
2581
+ }
2582
+ /**
2583
+ * Get the bit offset for a leg at the given index.
2584
+ *
2585
+ * @param legIndex - The leg index (0-3)
2586
+ * @returns The bit offset from the start of the legs section
2587
+ */
2588
+ function getLegOffset(legIndex) {
2589
+ return legIndex * TOKEN_ID_BITS.LEG_SIZE;
2590
+ }
2591
+ /**
2592
+ * Encode a pool ID from a Uniswap V3 pool address.
2593
+ *
2594
+ * The encoded PoolId structure: [16-bit tickSpacing][8-bit vegoid][40-bit pool address]
2595
+ *
2596
+ * @param address - The Uniswap V3 pool address
2597
+ * @param tickSpacing - The tick spacing of the pool
2598
+ * @param vegoid - The vegoid value (defaults to 4)
2599
+ * @returns The encoded pool ID
2600
+ */
2601
+ function encodePoolId(address, tickSpacing, vegoid = DEFAULT_VEGOID) {
2602
+ const addressHex = address.slice(2, 12).toLowerCase();
2603
+ const bytes = [];
2604
+ for (let i = 0; i < 10; i += 2) bytes.push(parseInt(addressHex.slice(i, i + 2), 16));
2605
+ bytes.reverse();
2606
+ let poolId = 0n;
2607
+ for (let i = 0; i < bytes.length; i++) poolId |= BigInt(bytes[i]) << BigInt(i * 8);
2608
+ poolId |= (vegoid & 0xffn) << TOKEN_ID_BITS.VEGOID_STARTING_BIT;
2609
+ poolId |= (tickSpacing & 0xffffn) << TOKEN_ID_BITS.TICK_SPACING_STARTING_BIT;
2610
+ return poolId;
2611
+ }
2612
+ /**
2613
+ * Encode a pool ID from a Uniswap V4 pool ID (bytes32).
2614
+ *
2615
+ * @param poolIdHex - The V4 pool ID (bytes32 hex string)
2616
+ * @param tickSpacing - The tick spacing of the pool
2617
+ * @param vegoid - The vegoid value (defaults to 4)
2618
+ * @returns The encoded pool ID
2619
+ */
2620
+ function encodeV4PoolId(poolIdHex, tickSpacing, vegoid = DEFAULT_VEGOID) {
2621
+ const hex = poolIdHex.slice(2);
2622
+ const last5BytesHex = hex.slice(-10).toLowerCase();
2623
+ const bytes = [];
2624
+ for (let i = 0; i < 10; i += 2) bytes.push(parseInt(last5BytesHex.slice(i, i + 2), 16));
2625
+ bytes.reverse();
2626
+ let encodedPoolId = 0n;
2627
+ for (let i = 0; i < bytes.length; i++) encodedPoolId |= BigInt(bytes[i]) << BigInt(i * 8);
2628
+ encodedPoolId |= (vegoid & 0xffn) << TOKEN_ID_BITS.VEGOID_STARTING_BIT;
2629
+ encodedPoolId |= (tickSpacing & 0xffffn) << TOKEN_ID_BITS.TICK_SPACING_STARTING_BIT;
2630
+ return encodedPoolId;
2631
+ }
2632
+ /**
2633
+ * Decode the vegoid from a TokenId.
2634
+ *
2635
+ * @param tokenId - The TokenId to decode
2636
+ * @returns The vegoid value
2637
+ */
2638
+ function decodeVegoid(tokenId) {
2639
+ const poolId = tokenId & (1n << TOKEN_ID_BITS.POOL_ID_SIZE) - 1n;
2640
+ return poolId >> TOKEN_ID_BITS.VEGOID_STARTING_BIT & (1n << TOKEN_ID_BITS.VEGOID_SIZE) - 1n;
2641
+ }
2642
+ /**
2643
+ * Decode the tick spacing from a TokenId.
2644
+ *
2645
+ * @param tokenId - The TokenId to decode
2646
+ * @returns The tick spacing
2647
+ */
2648
+ function decodeTickSpacing(tokenId) {
2649
+ const poolId = tokenId & (1n << TOKEN_ID_BITS.POOL_ID_SIZE) - 1n;
2650
+ return poolId >> TOKEN_ID_BITS.TICK_SPACING_STARTING_BIT;
2651
+ }
2652
+ /**
2653
+ * Decode the pool ID portion from a TokenId.
2654
+ *
2655
+ * @param tokenId - The TokenId to decode
2656
+ * @returns The pool ID as a hex string
2657
+ */
2658
+ function decodePoolId(tokenId) {
2659
+ const poolId = tokenId & (1n << TOKEN_ID_BITS.POOL_ID_SIZE) - 1n;
2660
+ let hex = poolId.toString(16);
2661
+ while (hex.length < 16) hex = "0" + hex;
2662
+ return `0x${hex}`;
2663
+ }
2664
+ /**
2665
+ * Encode a single leg field.
2666
+ *
2667
+ * @param value - The value to encode
2668
+ * @param bitPosition - The bit position within the leg
2669
+ * @param legIndex - The leg index
2670
+ * @returns The encoded value shifted to the correct position
2671
+ */
2672
+ function encodeLegField(value, bitPosition, legIndex) {
2673
+ return value << getLegOffset(legIndex) + bitPosition + TOKEN_ID_BITS.POOL_ID_SIZE;
2674
+ }
2675
+ /**
2676
+ * Encode a single leg into a TokenId.
2677
+ *
2678
+ * @param leg - The leg parameters
2679
+ * @returns The encoded leg value (to be ORed with existing TokenId)
2680
+ */
2681
+ function encodeLeg(leg) {
2682
+ const { index, asset, optionRatio, isLong, tokenType, riskPartner, strike, width } = leg;
2683
+ return encodeLegField(asset & LEG_MASKS.ASSET, LEG_BITS.ASSET_BIT, index) | encodeLegField(optionRatio & LEG_MASKS.RATIO, LEG_BITS.RATIO_BIT, index) | encodeLegField(isLong & LEG_MASKS.IS_LONG, LEG_BITS.IS_LONG_BIT, index) | encodeLegField(tokenType & LEG_MASKS.TOKEN_TYPE, LEG_BITS.TOKEN_TYPE_BIT, index) | encodeLegField(riskPartner & LEG_MASKS.RISK_PARTNER, LEG_BITS.RISK_PARTNER_BIT, index) | encodeLegField(convertStrikeToUnsigned(strike) & LEG_MASKS.STRIKE, LEG_BITS.STRIKE_BIT, index) | encodeLegField(width & LEG_MASKS.WIDTH, LEG_BITS.WIDTH_BIT, index);
2684
+ }
2685
+ /**
2686
+ * Add a leg to an existing TokenId.
2687
+ *
2688
+ * @param tokenId - The existing TokenId (can be just poolId or partial TokenId)
2689
+ * @param leg - The leg parameters to add
2690
+ * @returns The TokenId with the leg added
2691
+ */
2692
+ function addLegToTokenId(tokenId, leg) {
2693
+ return tokenId | encodeLeg(leg);
2694
+ }
2695
+ /**
2696
+ * Decode a single leg from a TokenId.
2697
+ *
2698
+ * @param tokenId - The TokenId to decode
2699
+ * @param legIndex - The leg index (0-3)
2700
+ * @returns The decoded leg data
2701
+ */
2702
+ function decodeLeg(tokenId, legIndex) {
2703
+ const offset = getLegOffset(legIndex) + TOKEN_ID_BITS.POOL_ID_SIZE;
2704
+ const leg = tokenId >> offset & LEG_MASKS.LEG;
2705
+ const asset = leg & LEG_MASKS.ASSET;
2706
+ const optionRatio = leg >> LEG_BITS.RATIO_BIT & LEG_MASKS.RATIO;
2707
+ const isLong = (leg >> LEG_BITS.IS_LONG_BIT & LEG_MASKS.IS_LONG) === 1n;
2708
+ const tokenType = leg >> LEG_BITS.TOKEN_TYPE_BIT & LEG_MASKS.TOKEN_TYPE;
2709
+ const riskPartner = leg >> LEG_BITS.RISK_PARTNER_BIT & LEG_MASKS.RISK_PARTNER;
2710
+ const encodedStrike = leg >> LEG_BITS.STRIKE_BIT & LEG_MASKS.STRIKE;
2711
+ const strike = convertStrikeToSigned(encodedStrike);
2712
+ const width = leg >> LEG_BITS.WIDTH_BIT & LEG_MASKS.WIDTH;
2713
+ return {
2714
+ index: legIndex,
2715
+ asset,
2716
+ optionRatio,
2717
+ isLong,
2718
+ tokenType,
2719
+ riskPartner,
2720
+ strike,
2721
+ width
2722
+ };
2723
+ }
2724
+ /**
2725
+ * Count the number of active legs in a TokenId.
2726
+ * A leg is active if its optionRatio > 0.
2727
+ *
2728
+ * @param tokenId - The TokenId to check
2729
+ * @returns The number of active legs
2730
+ */
2731
+ function countLegs(tokenId) {
2732
+ let count = 0n;
2733
+ for (let i = 0n; i < TOKEN_ID_BITS.MAX_LEGS; i++) {
2734
+ const leg = decodeLeg(tokenId, i);
2735
+ if (leg.optionRatio > 0n) count++;
2736
+ }
2737
+ return count;
2738
+ }
2739
+ /**
2740
+ * Decode all active legs from a TokenId.
2741
+ *
2742
+ * @param tokenId - The TokenId to decode
2743
+ * @returns Array of decoded legs (only active legs with optionRatio > 0)
2744
+ */
2745
+ function decodeAllLegs(tokenId) {
2746
+ const legs = [];
2747
+ for (let i = 0n; i < TOKEN_ID_BITS.MAX_LEGS; i++) {
2748
+ const leg = decodeLeg(tokenId, i);
2749
+ if (leg.optionRatio > 0n) legs.push(leg);
2750
+ }
2751
+ return legs;
2752
+ }
2753
+
2754
+ //#endregion
2755
+ //#region src/panoptic/v2/tokenId/builder.ts
2756
+ /**
2757
+ * Create a TokenId builder from an encoded pool ID.
2758
+ *
2759
+ * Use the 64-bit poolId from `getPool().poolId` or `fetchPoolId()`.
2760
+ * For offline encoding, use `encodePoolId()` or `encodeV4PoolId()` first.
2761
+ *
2762
+ * @param poolId - The encoded 64-bit pool ID
2763
+ * @returns A TokenId builder instance
2764
+ *
2765
+ * @example
2766
+ * ```typescript
2767
+ * const pool = await getPool({ client, poolAddress, chainId })
2768
+ * const tokenId = createTokenIdBuilder(pool.poolId)
2769
+ * .addCall({ strike: 100n, width: 10n, optionRatio: 1n, isLong: false })
2770
+ * .addPut({ strike: -100n, width: 10n, optionRatio: 1n, isLong: false })
2771
+ * .build()
2772
+ * ```
2773
+ */
2774
+ function createTokenIdBuilder(poolId) {
2775
+ let tokenId = poolId;
2776
+ let currentLegIndex = 0n;
2777
+ const validateLegConfig = (config, legIndex) => {
2778
+ if (legIndex >= TOKEN_ID_BITS.MAX_LEGS) throw new InvalidTokenIdParameterError(0n);
2779
+ if (config.optionRatio < 1n || config.optionRatio > LEG_LIMITS.MAX_RATIO) throw new InvalidTokenIdParameterError(1n);
2780
+ if (config.width < 0n || config.width > LEG_LIMITS.MAX_WIDTH) throw new InvalidTokenIdParameterError(2n);
2781
+ if (config.strike < LEG_LIMITS.MIN_STRIKE || config.strike > LEG_LIMITS.MAX_STRIKE) throw new InvalidTokenIdParameterError(3n);
2782
+ if (config.asset !== 0n && config.asset !== 1n) throw new InvalidTokenIdParameterError(4n);
2783
+ if (config.tokenType !== 0n && config.tokenType !== 1n) throw new InvalidTokenIdParameterError(5n);
2784
+ const riskPartner = config.riskPartner ?? legIndex;
2785
+ if (riskPartner < 0n || riskPartner > 3n) throw new InvalidTokenIdParameterError(6n);
2786
+ };
2787
+ const builder = {
2788
+ addLeg(config) {
2789
+ validateLegConfig(config, currentLegIndex);
2790
+ const leg = {
2791
+ index: currentLegIndex,
2792
+ asset: config.asset,
2793
+ optionRatio: config.optionRatio,
2794
+ isLong: config.isLong ? 1n : 0n,
2795
+ tokenType: config.tokenType,
2796
+ riskPartner: config.riskPartner ?? currentLegIndex,
2797
+ strike: config.strike,
2798
+ width: config.width
2799
+ };
2800
+ tokenId = addLegToTokenId(tokenId, leg);
2801
+ currentLegIndex++;
2802
+ return builder;
2803
+ },
2804
+ addCall(config) {
2805
+ const asset = config.asset ?? 0n;
2806
+ return builder.addLeg({
2807
+ ...config,
2808
+ tokenType: asset,
2809
+ asset
2810
+ });
2811
+ },
2812
+ addPut(config) {
2813
+ const asset = config.asset ?? 0n;
2814
+ return builder.addLeg({
2815
+ ...config,
2816
+ tokenType: asset === 0n ? 1n : 0n,
2817
+ asset
2818
+ });
2819
+ },
2820
+ addLoan(config) {
2821
+ return builder.addLeg({
2822
+ asset: config.asset,
2823
+ optionRatio: config.optionRatio ?? 1n,
2824
+ isLong: false,
2825
+ tokenType: config.tokenType,
2826
+ riskPartner: config.riskPartner,
2827
+ strike: config.strike,
2828
+ width: 0n
2829
+ });
2830
+ },
2831
+ addCredit(config) {
2832
+ return builder.addLeg({
2833
+ asset: config.asset,
2834
+ optionRatio: config.optionRatio ?? 1n,
2835
+ isLong: true,
2836
+ tokenType: config.tokenType,
2837
+ riskPartner: config.riskPartner,
2838
+ strike: config.strike,
2839
+ width: 0n
2840
+ });
2841
+ },
2842
+ build() {
2843
+ if (currentLegIndex === 0n) throw new InvalidTokenIdParameterError(7n);
2844
+ return tokenId;
2845
+ },
2846
+ legCount() {
2847
+ return currentLegIndex;
2848
+ },
2849
+ reset() {
2850
+ tokenId = poolId;
2851
+ currentLegIndex = 0n;
2852
+ return builder;
2853
+ }
2854
+ };
2855
+ return builder;
2856
+ }
2857
+
2858
+ //#endregion
2859
+ //#region src/panoptic/v2/writes/loanUtils.ts
2860
+ /** Maximum retry attempts on InputListFail */
2861
+ const MAX_RETRIES = 3;
2862
+ /**
2863
+ * Resolve token index (0 or 1) for the given token address against the pool.
2864
+ * Throws SwapTokenMismatchError if the token isn't in the pool.
2865
+ */
2866
+ function resolveTokenIndex(tokenAddress, token0, token1) {
2867
+ const lower = tokenAddress.toLowerCase();
2868
+ if (lower === token0.toLowerCase()) return 0n;
2869
+ if (lower === token1.toLowerCase()) return 1n;
2870
+ throw new SwapTokenMismatchError(tokenAddress, token0, token1);
2871
+ }
2872
+ /**
2873
+ * Resolve existing position IDs from explicit param or storage.
2874
+ */
2875
+ async function resolvePositionIds(explicit, storage, chainId, poolAddress, account) {
2876
+ if (explicit !== void 0) return explicit;
2877
+ if (storage) return getTrackedPositionIds({
2878
+ chainId,
2879
+ poolAddress,
2880
+ account,
2881
+ storage
2882
+ });
2883
+ throw new MissingPositionIdsError();
2884
+ }
2885
+ /**
2886
+ * Check if a caught error is an InputListFail contract revert.
2887
+ * Works with both raw viem errors and parsed PanopticError instances.
2888
+ */
2889
+ function isInputListFailError(error) {
2890
+ if (error instanceof InputListFailError) return true;
2891
+ const parsed = parsePanopticError(error);
2892
+ return parsed?.errorName === "InputListFail";
2893
+ }
2894
+ /**
2895
+ * Build a unique loan tokenId that doesn't collide with existing positions.
2896
+ * Bumps optionRatio (1-127) and returns adjusted size to maintain equivalent exposure.
2897
+ *
2898
+ * @param asset - Which token denominates the positionSize (0 or 1).
2899
+ */
2900
+ function buildUniqueLoan(poolId, asset, tokenType, currentTick, tickSpacing, existingPositionIds, positionSize) {
2901
+ const mod = currentTick % tickSpacing;
2902
+ let strike = currentTick - (mod + tickSpacing) % tickSpacing;
2903
+ for (let ratio = 1n; ratio <= 127n; ratio++) {
2904
+ const tokenId = createTokenIdBuilder(poolId).addLoan({
2905
+ asset,
2906
+ tokenType,
2907
+ strike,
2908
+ optionRatio: ratio
2909
+ }).build();
2910
+ if (!existingPositionIds.includes(tokenId)) {
2911
+ const adjustedSize = positionSize / ratio;
2912
+ if (adjustedSize === 0n || positionSize % ratio !== 0n) continue;
2913
+ return {
2914
+ tokenId,
2915
+ adjustedSize
2916
+ };
2917
+ }
2918
+ }
2919
+ const mod2 = currentTick % tickSpacing;
2920
+ strike = currentTick - (mod2 + tickSpacing) % tickSpacing + tickSpacing;
2921
+ for (let i = 0; i < 100; i++) {
2922
+ const tokenId = createTokenIdBuilder(poolId).addLoan({
2923
+ asset,
2924
+ tokenType,
2925
+ strike
2926
+ }).build();
2927
+ if (!existingPositionIds.includes(tokenId)) return {
2928
+ tokenId,
2929
+ adjustedSize: positionSize
2930
+ };
2931
+ strike += tickSpacing;
2932
+ }
2933
+ throw new LoanSlotExhaustedError();
2934
+ }
2935
+
2936
+ //#endregion
2937
+ //#region src/panoptic/v2/writes/swap.ts
2938
+ /**
2939
+ * Swap tokens using Panoptic's exact-output mechanism.
2940
+ *
2941
+ * Opens a loan with `swapAtMint=true` and immediately burns it with `swapAtMint=false`.
2942
+ * The user receives exactly `amountOut` of `tokenOut`.
2943
+ *
2944
+ * @param params - Swap parameters
2945
+ * @returns TxResult
2946
+ *
2947
+ * @example
2948
+ * ```typescript
2949
+ * const result = await swapExactOut({
2950
+ * client, walletClient, account, poolAddress,
2951
+ * chainId: 11155111n,
2952
+ * tokenOut: WETH_ADDRESS,
2953
+ * amountOut: 5n * 10n**16n, // 0.05 WETH
2954
+ * slippageBps: 500n, // 5% slippage
2955
+ * })
2956
+ * const receipt = await result.wait()
2957
+ * ```
2958
+ */
2959
+ async function swapExactOut(params) {
2960
+ const { client, walletClient, account, poolAddress, chainId, tokenOut, amountOut, slippageBps, existingPositionIds: explicitIds, storage, txOverrides } = params;
2961
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
2962
+ const [pool, positionIds] = await Promise.all([getPool({
2963
+ client,
2964
+ poolAddress,
2965
+ chainId
2966
+ }), resolvePositionIds(explicitIds, storage, chainId, poolAddress, account)]);
2967
+ const token0 = pool.collateralTracker0.token;
2968
+ const token1 = pool.collateralTracker1.token;
2969
+ const tokenOutIndex = resolveTokenIndex(tokenOut, token0, token1);
2970
+ const tokenType = tokenOutIndex === 0n ? 1n : 0n;
2971
+ const { low: tickLimitLow, high: tickLimitHigh } = tickLimits(pool.currentTick, slippageBps);
2972
+ const { tokenId: loanTokenId, adjustedSize } = buildUniqueLoan(pool.poolId, tokenOutIndex, tokenType, pool.currentTick, pool.tickSpacing, positionIds, amountOut);
2973
+ const mintTickLimits = [
2974
+ Number(tickLimitHigh),
2975
+ Number(tickLimitLow),
2976
+ 0
2977
+ ];
2978
+ const burnTickLimits = [
2979
+ Number(tickLimitLow),
2980
+ Number(tickLimitHigh),
2981
+ 0
2982
+ ];
2983
+ const positionIdList = [loanTokenId, loanTokenId];
2984
+ const finalPositionIdList = [...positionIds];
2985
+ const positionSizes = [adjustedSize, 0n];
2986
+ try {
2987
+ return await submitWrite({
2988
+ client,
2989
+ walletClient,
2990
+ account,
2991
+ address: poolAddress,
2992
+ abi: panopticPoolV2Abi,
2993
+ functionName: "dispatch",
2994
+ args: [
2995
+ positionIdList,
2996
+ finalPositionIdList,
2997
+ positionSizes,
2998
+ [mintTickLimits, burnTickLimits],
2999
+ false,
3000
+ 0n
3001
+ ],
3002
+ txOverrides
3003
+ });
3004
+ } catch (error) {
3005
+ if (isInputListFailError(error) && attempt < MAX_RETRIES - 1) continue;
3006
+ throw error;
3007
+ }
3008
+ }
3009
+ throw new MaxRetriesExceededError("swapExactOut");
3010
+ }
3011
+ /**
3012
+ * Swap exact output and wait for confirmation.
3013
+ */
3014
+ async function swapExactOutAndWait(params) {
3015
+ const result = await swapExactOut(params);
3016
+ return result.wait();
3017
+ }
3018
+ /**
3019
+ * Swap tokens using Panoptic's exact-input mechanism.
3020
+ *
3021
+ * Opens a loan with `swapAtMint=false` (borrows tokenIn to wallet),
3022
+ * then burns with `swapAtMint=true` (swaps tokenIn back to repay).
3023
+ * The user spends exactly `amountIn` of `tokenIn`.
3024
+ *
3025
+ * @param params - Swap parameters
3026
+ * @returns TxResult
3027
+ *
3028
+ * @example
3029
+ * ```typescript
3030
+ * const result = await swapExactIn({
3031
+ * client, walletClient, account, poolAddress,
3032
+ * chainId: 11155111n,
3033
+ * tokenIn: USDC_ADDRESS,
3034
+ * amountIn: 1000n * 10n**6n, // 1000 USDC
3035
+ * slippageBps: 500n,
3036
+ * })
3037
+ * const receipt = await result.wait()
3038
+ * ```
3039
+ */
3040
+ async function swapExactIn(params) {
3041
+ const { client, walletClient, account, poolAddress, chainId, tokenIn, amountIn, slippageBps, existingPositionIds: explicitIds, storage, txOverrides } = params;
3042
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
3043
+ const [pool, positionIds] = await Promise.all([getPool({
3044
+ client,
3045
+ poolAddress,
3046
+ chainId
3047
+ }), resolvePositionIds(explicitIds, storage, chainId, poolAddress, account)]);
3048
+ const token0 = pool.collateralTracker0.token;
3049
+ const token1 = pool.collateralTracker1.token;
3050
+ const tokenInIndex = resolveTokenIndex(tokenIn, token0, token1);
3051
+ const tokenType = tokenInIndex;
3052
+ const { low: tickLimitLow, high: tickLimitHigh } = tickLimits(pool.currentTick, slippageBps);
3053
+ const { tokenId: loanTokenId, adjustedSize } = buildUniqueLoan(pool.poolId, tokenInIndex, tokenType, pool.currentTick, pool.tickSpacing, positionIds, amountIn);
3054
+ const mintTickLimits = [
3055
+ Number(tickLimitLow),
3056
+ Number(tickLimitHigh),
3057
+ 0
3058
+ ];
3059
+ const burnTickLimits = [
3060
+ Number(tickLimitHigh),
3061
+ Number(tickLimitLow),
3062
+ 0
3063
+ ];
3064
+ const positionIdList = [loanTokenId, loanTokenId];
3065
+ const finalPositionIdList = [...positionIds];
3066
+ const positionSizes = [adjustedSize, 0n];
3067
+ try {
3068
+ return await submitWrite({
3069
+ client,
3070
+ walletClient,
3071
+ account,
3072
+ address: poolAddress,
3073
+ abi: panopticPoolV2Abi,
3074
+ functionName: "dispatch",
3075
+ args: [
3076
+ positionIdList,
3077
+ finalPositionIdList,
3078
+ positionSizes,
3079
+ [mintTickLimits, burnTickLimits],
3080
+ false,
3081
+ 0n
3082
+ ],
3083
+ txOverrides
3084
+ });
3085
+ } catch (error) {
3086
+ if (isInputListFailError(error) && attempt < MAX_RETRIES - 1) continue;
3087
+ throw error;
3088
+ }
3089
+ }
3090
+ throw new MaxRetriesExceededError("swapExactIn");
3091
+ }
3092
+ /**
3093
+ * Swap exact input and wait for confirmation.
3094
+ */
3095
+ async function swapExactInAndWait(params) {
3096
+ const result = await swapExactIn(params);
3097
+ return result.wait();
3098
+ }
3099
+
3100
+ //#endregion
3101
+ //#region src/panoptic/v2/writes/wrap.ts
3102
+ /** Minimal ERC4626 surface used by the xStock wrappers. */
3103
+ const xstockWrapperAbi = [
3104
+ {
3105
+ type: "function",
3106
+ name: "deposit",
3107
+ stateMutability: "nonpayable",
3108
+ inputs: [{
3109
+ name: "assets",
3110
+ type: "uint256"
3111
+ }, {
3112
+ name: "receiver",
3113
+ type: "address"
3114
+ }],
3115
+ outputs: [{
3116
+ name: "shares",
3117
+ type: "uint256"
3118
+ }]
3119
+ },
3120
+ {
3121
+ type: "function",
3122
+ name: "redeem",
3123
+ stateMutability: "nonpayable",
3124
+ inputs: [
3125
+ {
3126
+ name: "shares",
3127
+ type: "uint256"
3128
+ },
3129
+ {
3130
+ name: "receiver",
3131
+ type: "address"
3132
+ },
3133
+ {
3134
+ name: "owner",
3135
+ type: "address"
3136
+ }
3137
+ ],
3138
+ outputs: [{
3139
+ name: "assets",
3140
+ type: "uint256"
3141
+ }]
3142
+ },
3143
+ {
3144
+ type: "function",
3145
+ name: "asset",
3146
+ stateMutability: "view",
3147
+ inputs: [],
3148
+ outputs: [{ type: "address" }]
3149
+ },
3150
+ {
3151
+ type: "function",
3152
+ name: "previewDeposit",
3153
+ stateMutability: "view",
3154
+ inputs: [{
3155
+ name: "assets",
3156
+ type: "uint256"
3157
+ }],
3158
+ outputs: [{
3159
+ name: "shares",
3160
+ type: "uint256"
3161
+ }]
3162
+ },
3163
+ {
3164
+ type: "function",
3165
+ name: "previewRedeem",
3166
+ stateMutability: "view",
3167
+ inputs: [{
3168
+ name: "shares",
3169
+ type: "uint256"
3170
+ }],
3171
+ outputs: [{
3172
+ name: "assets",
3173
+ type: "uint256"
3174
+ }]
3175
+ },
3176
+ {
3177
+ type: "function",
3178
+ name: "maxRedeem",
3179
+ stateMutability: "view",
3180
+ inputs: [{
3181
+ name: "owner",
3182
+ type: "address"
3183
+ }],
3184
+ outputs: [{
3185
+ name: "maxShares",
3186
+ type: "uint256"
3187
+ }]
3188
+ }
3189
+ ];
3190
+ /**
3191
+ * Wrap an underlying xStock into its ERC4626 wrapper (`deposit`). Requires a
3192
+ * prior ERC20 approval of the underlying to the wrapper address.
3193
+ *
3194
+ * @returns TxResult with hash and wait function
3195
+ */
3196
+ async function wrapXstock(params) {
3197
+ const { client, walletClient, account, wrapper, assets, receiver = account, txOverrides } = params;
3198
+ return submitWrite({
3199
+ client,
3200
+ walletClient,
3201
+ account,
3202
+ address: wrapper,
3203
+ abi: xstockWrapperAbi,
3204
+ functionName: "deposit",
3205
+ args: [assets, receiver],
3206
+ txOverrides
3207
+ });
3208
+ }
3209
+ /** Wrap and wait for confirmation. */
3210
+ async function wrapXstockAndWait(params) {
3211
+ const result = await wrapXstock(params);
3212
+ return result.wait();
3213
+ }
3214
+ /**
3215
+ * Unwrap wrapper shares back into the underlying xStock (`redeem`). When
3216
+ * `owner` defaults to (or equals) `account` the caller burns their own shares
3217
+ * and no approval is needed. Passing a different `owner` redeems on its behalf
3218
+ * and requires that owner to have granted the caller an ERC-4626 share
3219
+ * allowance (`approve`), or the `redeem` call reverts.
3220
+ *
3221
+ * @returns TxResult with hash and wait function
3222
+ */
3223
+ async function unwrapXstock(params) {
3224
+ const { client, walletClient, account, wrapper, shares, receiver = account, owner = account, txOverrides } = params;
3225
+ return submitWrite({
3226
+ client,
3227
+ walletClient,
3228
+ account,
3229
+ address: wrapper,
3230
+ abi: xstockWrapperAbi,
3231
+ functionName: "redeem",
3232
+ args: [
3233
+ shares,
3234
+ receiver,
3235
+ owner
3236
+ ],
3237
+ txOverrides
3238
+ });
3239
+ }
3240
+ /** Unwrap and wait for confirmation. */
3241
+ async function unwrapXstockAndWait(params) {
3242
+ const result = await unwrapXstock(params);
3243
+ return result.wait();
3244
+ }
3245
+ /**
3246
+ * Preview the wrapper shares minted for a given amount of underlying xStock.
3247
+ */
3248
+ async function previewWrap(params) {
3249
+ const { client, wrapper, amount } = params;
3250
+ return client.readContract({
3251
+ address: wrapper,
3252
+ abi: xstockWrapperAbi,
3253
+ functionName: "previewDeposit",
3254
+ args: [amount]
3255
+ });
3256
+ }
3257
+ /**
3258
+ * Preview the underlying xStock returned for a given amount of wrapper shares.
3259
+ */
3260
+ async function previewUnwrap(params) {
3261
+ const { client, wrapper, amount } = params;
3262
+ return client.readContract({
3263
+ address: wrapper,
3264
+ abi: xstockWrapperAbi,
3265
+ functionName: "previewRedeem",
3266
+ args: [amount]
3267
+ });
3268
+ }
3269
+
3270
+ //#endregion
3271
+ //#region src/panoptic/v2/tokenId/decode.ts
3272
+ /**
3273
+ * Convert a DecodedLeg to TokenIdLeg format.
3274
+ *
3275
+ * @param leg - The decoded leg from encoding utils
3276
+ * @param tickSpacing - The tick spacing for calculating tick bounds
3277
+ * @returns The TokenIdLeg format
3278
+ */
3279
+ function convertToTokenIdLeg(leg, tickSpacing) {
3280
+ const halfWidth = leg.width * tickSpacing / 2n;
3281
+ const tickLower = leg.strike - halfWidth;
3282
+ const tickUpper = leg.strike + halfWidth;
3283
+ return {
3284
+ index: leg.index,
3285
+ asset: leg.asset,
3286
+ optionRatio: leg.optionRatio,
3287
+ isLong: leg.isLong,
3288
+ tokenType: leg.tokenType,
3289
+ riskPartner: leg.riskPartner,
3290
+ strike: leg.strike,
3291
+ width: leg.width,
3292
+ tickLower,
3293
+ tickUpper
3294
+ };
3295
+ }
3296
+ /**
3297
+ * Decode a TokenId into its component parts.
3298
+ *
3299
+ * @param tokenId - The TokenId to decode
3300
+ * @returns The decoded TokenId data
3301
+ *
3302
+ * @example
3303
+ * ```typescript
3304
+ * const decoded = decodeTokenId(tokenId)
3305
+ * console.log(decoded.legs) // Array of legs
3306
+ * console.log(decoded.tickSpacing) // Tick spacing
3307
+ * ```
3308
+ */
3309
+ function decodeTokenId(tokenId) {
3310
+ const poolId = decodePoolId(tokenId);
3311
+ const vegoid = decodeVegoid(tokenId);
3312
+ const tickSpacing = decodeTickSpacing(tokenId);
3313
+ const rawLegs = decodeAllLegs(tokenId);
3314
+ const numLegs = countLegs(tokenId);
3315
+ const legs = rawLegs.map((leg) => convertToTokenIdLeg(leg, tickSpacing));
3316
+ return {
3317
+ tokenId,
3318
+ poolId,
3319
+ vegoid,
3320
+ tickSpacing,
3321
+ legs,
3322
+ legCount: numLegs
3323
+ };
3324
+ }
3325
+ /**
3326
+ * Validate that a TokenId has the expected pool ID.
3327
+ *
3328
+ * @param tokenId - The TokenId to validate
3329
+ * @param expectedPoolId - The expected pool ID
3330
+ * @returns True if the pool IDs match
3331
+ */
3332
+ function validatePoolId(tokenId, expectedPoolId) {
3333
+ const actualPoolId = tokenId & (1n << 64n) - 1n;
3334
+ return actualPoolId === expectedPoolId;
3335
+ }
3336
+ /**
3337
+ * Check if a TokenId represents a long position (any leg is long).
3338
+ *
3339
+ * @param tokenId - The TokenId to check
3340
+ * @returns True if any leg is long
3341
+ */
3342
+ function hasLongLeg(tokenId) {
3343
+ const legs = decodeAllLegs(tokenId);
3344
+ return legs.some((leg) => leg.isLong);
3345
+ }
3346
+ /**
3347
+ * Check if a TokenId represents a short-only position.
3348
+ *
3349
+ * @param tokenId - The TokenId to check
3350
+ * @returns True if all legs are short
3351
+ */
3352
+ function isShortOnly(tokenId) {
3353
+ const legs = decodeAllLegs(tokenId);
3354
+ return legs.length > 0 && legs.every((leg) => !leg.isLong);
3355
+ }
3356
+ /**
3357
+ * Check if a TokenId represents a spread (legs with different risk partners).
3358
+ *
3359
+ * @param tokenId - The TokenId to check
3360
+ * @returns True if any leg has a different risk partner
3361
+ */
3362
+ function isSpread(tokenId) {
3363
+ const legs = decodeAllLegs(tokenId);
3364
+ return legs.some((leg) => leg.riskPartner !== leg.index);
3365
+ }
3366
+ /**
3367
+ * Get the asset index for a TokenId (from the first leg).
3368
+ *
3369
+ * @param tokenId - The TokenId
3370
+ * @returns The asset index (0 or 1), or undefined if no legs
3371
+ */
3372
+ function getAssetIndex(tokenId) {
3373
+ const legs = decodeAllLegs(tokenId);
3374
+ if (legs.length === 0) return void 0;
3375
+ return legs[0].asset;
3376
+ }
3377
+ /**
3378
+ * Check if a leg is a loan (width=0 and isLong=false).
3379
+ *
3380
+ * Loans borrow liquidity from the pool at a specific strike price.
3381
+ *
3382
+ * @param leg - The decoded leg to check
3383
+ * @returns True if the leg is a loan
3384
+ */
3385
+ function isLoanLeg(leg) {
3386
+ return leg.width === 0n && !leg.isLong;
3387
+ }
3388
+ /**
3389
+ * Check if a leg is a credit (width=0 and isLong=true).
3390
+ *
3391
+ * Credits lend liquidity to the pool at a specific strike price.
3392
+ *
3393
+ * @param leg - The decoded leg to check
3394
+ * @returns True if the leg is a credit
3395
+ */
3396
+ function isCreditLeg(leg) {
3397
+ return leg.width === 0n && leg.isLong;
3398
+ }
3399
+ /**
3400
+ * Check if a TokenId is a pure loan (all legs are loans).
3401
+ *
3402
+ * @param tokenId - The TokenId to check
3403
+ * @returns True if all legs are loans
3404
+ */
3405
+ function isLoan(tokenId) {
3406
+ const legs = decodeAllLegs(tokenId);
3407
+ return legs.length > 0 && legs.every(isLoanLeg);
3408
+ }
3409
+ /**
3410
+ * Check if a TokenId is a pure credit (all legs are credits).
3411
+ *
3412
+ * @param tokenId - The TokenId to check
3413
+ * @returns True if all legs are credits
3414
+ */
3415
+ function isCredit(tokenId) {
3416
+ const legs = decodeAllLegs(tokenId);
3417
+ return legs.length > 0 && legs.every(isCreditLeg);
3418
+ }
3419
+ /**
3420
+ * Check if a TokenId contains loan or credit legs (width=0).
3421
+ *
3422
+ * @param tokenId - The TokenId to check
3423
+ * @returns True if any leg has width=0 (loan or credit)
3424
+ */
3425
+ function hasLoanOrCredit(tokenId) {
3426
+ const legs = decodeAllLegs(tokenId);
3427
+ return legs.some((leg) => leg.width === 0n);
3428
+ }
3429
+
3430
+ //#endregion
3431
+ //#region src/panoptic/v2/simulations/tokenFlow.ts
3432
+ /**
3433
+ * PanopticPool getAssetsOf ABI.
3434
+ * Returns collateral assets (shares converted to underlying) for an account.
3435
+ */
3436
+ const getAssetsOfAbi = [{
3437
+ type: "function",
3438
+ name: "getAssetsOf",
3439
+ inputs: [{
3440
+ name: "account",
3441
+ type: "address"
3442
+ }],
3443
+ outputs: [{
3444
+ name: "assets0",
3445
+ type: "uint256"
3446
+ }, {
3447
+ name: "assets1",
3448
+ type: "uint256"
3449
+ }],
3450
+ stateMutability: "view"
3451
+ }];
3452
+ /**
3453
+ * PanopticPool multicall ABI (inherited from Uniswap).
3454
+ * Uses delegatecall, preserving msg.sender throughout the chain.
3455
+ */
3456
+ const multicallAbi = [{
3457
+ type: "function",
3458
+ name: "multicall",
3459
+ inputs: [{
3460
+ name: "data",
3461
+ type: "bytes[]"
3462
+ }],
3463
+ outputs: [{
3464
+ name: "results",
3465
+ type: "bytes[]"
3466
+ }],
3467
+ stateMutability: "nonpayable"
3468
+ }, ...panopticErrorsAbi];
3469
+ /**
3470
+ * Simulate a contract call and measure token flow using PanopticPool.multicall.
3471
+ *
3472
+ * This function uses PanopticPool's inherited multicall (delegatecall-based) to chain:
3473
+ * 1. getAssetsOf(user) - read collateral assets before
3474
+ * 2. getCurrentTick() - read pool tick before
3475
+ * 3. Execute the target call (e.g., dispatch)
3476
+ * 4. getCurrentTick() - read pool tick after
3477
+ * 5. getAssetsOf(user) - read collateral assets after
3478
+ *
3479
+ * ## Why PanopticPool.multicall instead of Multicall3?
3480
+ * - Measures **collateral assets** (shares → underlying), not raw wallet balances
3481
+ * - Uses **delegatecall**, preserving msg.sender throughout the chain
3482
+ * - Single contract interaction with PanopticPool
3483
+ * - Correctly reflects what happens during position operations
3484
+ *
3485
+ * ## Same-Block Guarantee
3486
+ * All operations execute within a single eth_call, ensuring atomic consistency.
3487
+ *
3488
+ * @param params - Simulation parameters
3489
+ * @returns Token flow result
3490
+ *
3491
+ * @example
3492
+ * ```typescript
3493
+ * const callData = encodeFunctionData({
3494
+ * abi: panopticPoolV2Abi,
3495
+ * functionName: 'dispatch',
3496
+ * args: [positionIdList, finalPositionIdList, positionSizes, tickAndSpreadLimits, false, 0n],
3497
+ * })
3498
+ *
3499
+ * const result = await simulateWithTokenFlow({
3500
+ * client,
3501
+ * poolAddress,
3502
+ * user: userAddress,
3503
+ * callData,
3504
+ * })
3505
+ *
3506
+ * if (result.success) {
3507
+ * console.log('Token 0 change:', result.tokenFlow.delta0)
3508
+ * console.log('Token 1 change:', result.tokenFlow.delta1)
3509
+ * }
3510
+ * ```
3511
+ */
3512
+ async function simulateWithTokenFlow(params) {
3513
+ const { client, poolAddress, user, callData, blockNumber, postCallData, preCallData } = params;
3514
+ const getAssetsOfCallData = encodeFunctionData({
3515
+ abi: getAssetsOfAbi,
3516
+ functionName: "getAssetsOf",
3517
+ args: [user]
3518
+ });
3519
+ const getCurrentTickCallData = encodeFunctionData({
3520
+ abi: panopticPoolV2Abi,
3521
+ functionName: "getCurrentTick"
3522
+ });
3523
+ const preLen = preCallData?.length ?? 0;
3524
+ const multicallData = [
3525
+ getAssetsOfCallData,
3526
+ getCurrentTickCallData,
3527
+ ...preCallData ?? [],
3528
+ callData,
3529
+ getCurrentTickCallData,
3530
+ getAssetsOfCallData,
3531
+ ...postCallData ?? []
3532
+ ];
3533
+ try {
3534
+ const { result } = await client.simulateContract({
3535
+ address: poolAddress,
3536
+ abi: multicallAbi,
3537
+ functionName: "multicall",
3538
+ args: [multicallData],
3539
+ account: user,
3540
+ blockNumber
3541
+ });
3542
+ const decodeAssets = (data) => {
3543
+ const decoded = decodeFunctionResult({
3544
+ abi: getAssetsOfAbi,
3545
+ functionName: "getAssetsOf",
3546
+ data
3547
+ });
3548
+ return {
3549
+ assets0: decoded[0],
3550
+ assets1: decoded[1]
3551
+ };
3552
+ };
3553
+ const decodeTick = (data) => {
3554
+ return BigInt(decodeFunctionResult({
3555
+ abi: panopticPoolV2Abi,
3556
+ functionName: "getCurrentTick",
3557
+ data
3558
+ }));
3559
+ };
3560
+ const assetsBefore = decodeAssets(result[0]);
3561
+ const tickBefore = decodeTick(result[1]);
3562
+ const tickAfter = decodeTick(result[3 + preLen]);
3563
+ const assetsAfter = decodeAssets(result[4 + preLen]);
3564
+ const preCallResults = preLen > 0 ? result.slice(2, 2 + preLen) : void 0;
3565
+ const postCallResults = postCallData && postCallData.length > 0 ? result.slice(5 + preLen) : void 0;
3566
+ const delta0 = assetsAfter.assets0 - assetsBefore.assets0;
3567
+ const delta1 = assetsAfter.assets1 - assetsBefore.assets1;
3568
+ let gasEstimate = 0n;
3569
+ try {
3570
+ gasEstimate = await client.estimateGas({
3571
+ account: user,
3572
+ to: poolAddress,
3573
+ data: callData,
3574
+ blockNumber
3575
+ });
3576
+ } catch {}
3577
+ return {
3578
+ success: true,
3579
+ tokenFlow: {
3580
+ delta0,
3581
+ delta1,
3582
+ balanceBefore0: assetsBefore.assets0,
3583
+ balanceBefore1: assetsBefore.assets1,
3584
+ balanceAfter0: assetsAfter.assets0,
3585
+ balanceAfter1: assetsAfter.assets1,
3586
+ tickBefore,
3587
+ tickAfter
3588
+ },
3589
+ gasEstimate,
3590
+ postCallResults,
3591
+ preCallResults
3592
+ };
3593
+ } catch (error) {
3594
+ return {
3595
+ success: false,
3596
+ error: error instanceof Error ? error.message : "Simulation failed",
3597
+ rawError: error instanceof Error ? error : void 0,
3598
+ gasEstimate: 0n
3599
+ };
3600
+ }
3601
+ }
3602
+
3603
+ //#endregion
3604
+ //#region src/panoptic/v2/simulations/simulateOpenPosition.ts
3605
+ /**
3606
+ * Simulate opening a position.
3607
+ *
3608
+ * Uses PanopticPool.multicall with getAssetsOf-dispatch-getAssetsOf pattern
3609
+ * to measure exact collateral asset movements, then enriches with decoded
3610
+ * position data and client-side greeks.
3611
+ *
3612
+ * @param params - Simulation parameters
3613
+ * @returns Simulation result with position data or error
3614
+ *
3615
+ * @example
3616
+ * ```typescript
3617
+ * import { MIN_TICK, MAX_TICK } from '@panoptic/sdk'
3618
+ *
3619
+ * const result = await simulateOpenPosition({
3620
+ * client,
3621
+ * poolAddress,
3622
+ * account,
3623
+ * existingPositionIds: [],
3624
+ * tokenId,
3625
+ * positionSize: 1n,
3626
+ * tickLimitLow: MIN_TICK,
3627
+ * tickLimitHigh: MAX_TICK,
3628
+ * swapAtMint: false,
3629
+ * chainId: 11155111n, // Sepolia
3630
+ * })
3631
+ *
3632
+ * if (result.success) {
3633
+ * console.log('Gas estimate:', result.gasEstimate)
3634
+ * console.log('Token 0 required:', result.data.amount0Required)
3635
+ * console.log('Token 1 required:', result.data.amount1Required)
3636
+ * console.log('Delta:', result.data.greeks.delta)
3637
+ * } else {
3638
+ * console.log('Error:', result.error)
3639
+ * }
3640
+ * ```
3641
+ */
3642
+ async function simulateOpenPosition(params) {
3643
+ const { client, poolAddress, account, existingPositionIds, tokenId, positionSize, tickLimitLow, tickLimitHigh, spreadLimit = 0n, swapAtMint = false, usePremiaAsCollateral = false, builderCode = 0n, chainId, blockNumber } = params;
3644
+ const targetBlockNumber = blockNumber ?? await client.getBlockNumber();
3645
+ const metaPromise = getBlockMeta({
3646
+ client,
3647
+ blockNumber: targetBlockNumber
3648
+ });
3649
+ const tickLimits$1 = swapAtMint ? [
3650
+ Number(tickLimitHigh),
3651
+ Number(tickLimitLow),
3652
+ Number(spreadLimit)
3653
+ ] : [
3654
+ Number(tickLimitLow),
3655
+ Number(tickLimitHigh),
3656
+ Number(spreadLimit)
3657
+ ];
3658
+ try {
3659
+ const positionIdList = [tokenId];
3660
+ const finalPositionIdList = [...existingPositionIds, tokenId];
3661
+ const callData = encodeFunctionData({
3662
+ abi: panopticPoolV2Abi,
3663
+ functionName: "dispatch",
3664
+ args: [
3665
+ positionIdList,
3666
+ finalPositionIdList,
3667
+ [BigInt(positionSize)],
3668
+ [tickLimits$1],
3669
+ usePremiaAsCollateral,
3670
+ builderCode
3671
+ ]
3672
+ });
3673
+ const getFullPositionsDataCallData = encodeFunctionData({
3674
+ abi: panopticPoolV2Abi,
3675
+ functionName: "getFullPositionsData",
3676
+ args: [
3677
+ account,
3678
+ false,
3679
+ finalPositionIdList
3680
+ ]
3681
+ });
3682
+ const flowResult = await simulateWithTokenFlow({
3683
+ client,
3684
+ poolAddress,
3685
+ user: account,
3686
+ callData,
3687
+ blockNumber: targetBlockNumber,
3688
+ postCallData: [getFullPositionsDataCallData]
3689
+ });
3690
+ if (!flowResult.success || !flowResult.tokenFlow) throw flowResult.rawError ?? new PanopticError(flowResult.error || "Token flow simulation failed");
3691
+ const tokenFlow = flowResult.tokenFlow;
3692
+ let postMintCollateralReqToken0 = 0n;
3693
+ let postMintCollateralReqToken1 = 0n;
3694
+ const perPositionCollateralReqs = [];
3695
+ if (flowResult.postCallResults?.[0]) try {
3696
+ const decoded$1 = decodeFunctionResult({
3697
+ abi: panopticPoolV2Abi,
3698
+ functionName: "getFullPositionsData",
3699
+ data: flowResult.postCallResults[0]
3700
+ });
3701
+ const collateralReqs = decoded$1[3];
3702
+ if (collateralReqs.length > 0) for (const packed of collateralReqs) {
3703
+ const req = decodeLeftRightUnsigned(packed);
3704
+ postMintCollateralReqToken0 += req.right;
3705
+ postMintCollateralReqToken1 += req.left;
3706
+ perPositionCollateralReqs.push({
3707
+ token0: req.right,
3708
+ token1: req.left
3709
+ });
3710
+ }
3711
+ } catch {}
3712
+ const decoded = decodeTokenId(tokenId);
3713
+ let currentTick = 0n;
3714
+ let poolTickSpacing = decoded.tickSpacing;
3715
+ let utilization0 = 0n;
3716
+ let utilization1 = 0n;
3717
+ if (chainId !== void 0) try {
3718
+ const pool = await getPool({
3719
+ client,
3720
+ poolAddress,
3721
+ chainId,
3722
+ blockNumber: targetBlockNumber
3723
+ });
3724
+ currentTick = pool.currentTick;
3725
+ poolTickSpacing = pool.tickSpacing;
3726
+ utilization0 = pool.collateralTracker0.utilization;
3727
+ utilization1 = pool.collateralTracker1.utilization;
3728
+ } catch {}
3729
+ const greeks = decoded.legs.length > 0 && (chainId !== void 0 || currentTick !== 0n) ? calculatePositionGreeks({
3730
+ legs: decoded.legs,
3731
+ currentTick,
3732
+ mintTick: currentTick,
3733
+ positionSize,
3734
+ poolTickSpacing
3735
+ }) : {
3736
+ value: 0n,
3737
+ delta: 0n,
3738
+ gamma: 0n
3739
+ };
3740
+ const _meta = await metaPromise;
3741
+ const amount0Required = -tokenFlow.delta0;
3742
+ const amount1Required = -tokenFlow.delta1;
3743
+ const data = {
3744
+ position: {
3745
+ tokenId,
3746
+ positionSize,
3747
+ owner: account,
3748
+ poolAddress,
3749
+ legs: decoded.legs,
3750
+ poolUtilization0AtMint: utilization0,
3751
+ poolUtilization1AtMint: utilization1,
3752
+ tickAtMint: currentTick,
3753
+ timestampAtMint: _meta.blockTimestamp,
3754
+ blockNumberAtMint: _meta.blockNumber,
3755
+ swapAtMint,
3756
+ premiaOwed0: 0n,
3757
+ premiaOwed1: 0n,
3758
+ assetIndex: 0n,
3759
+ _meta
3760
+ },
3761
+ greeks,
3762
+ amount0Required,
3763
+ amount1Required,
3764
+ postCollateral0: tokenFlow.balanceAfter0,
3765
+ postCollateral1: tokenFlow.balanceAfter1,
3766
+ postMintCollateralReqToken0,
3767
+ postMintCollateralReqToken1,
3768
+ perPositionCollateralReqs,
3769
+ commission0: null,
3770
+ commission1: null
3771
+ };
3772
+ return {
3773
+ success: true,
3774
+ data,
3775
+ gasEstimate: flowResult.gasEstimate,
3776
+ tokenFlow,
3777
+ _meta
3778
+ };
3779
+ } catch (error) {
3780
+ const _meta = await metaPromise;
3781
+ return {
3782
+ success: false,
3783
+ error: error instanceof PanopticError ? error : new PanopticError(error instanceof Error ? error.message : "Simulation failed", error instanceof Error ? error : void 0),
3784
+ _meta
3785
+ };
3786
+ }
3787
+ }
3788
+
3789
+ //#endregion
3790
+ //#region src/panoptic/v2/writes/lending.ts
3791
+ /**
3792
+ * Resolve collateral tracker address for a given token in the pool.
3793
+ */
3794
+ async function resolveCollateralTracker(client, poolAddress, chainId, token) {
3795
+ const pool = await getPool({
3796
+ client,
3797
+ poolAddress,
3798
+ chainId
3799
+ });
3800
+ const tokenIndex = resolveTokenIndex(token, pool.collateralTracker0.token, pool.collateralTracker1.token);
3801
+ return tokenIndex === 0n ? pool.collateralTracker0.address : pool.collateralTracker1.address;
3802
+ }
3803
+ /**
3804
+ * Save a position ID list to storage.
3805
+ */
3806
+ async function savePositionIds(storage, chainId, poolAddress, account, positionIds) {
3807
+ const key = getPositionsKey(chainId, poolAddress, account);
3808
+ await storage.set(key, jsonSerializer.stringify(positionIds));
3809
+ }
3810
+ /**
3811
+ * Supply collateral into a pool.
3812
+ *
3813
+ * Resolves the correct collateral tracker from the token address,
3814
+ * then delegates to the low-level `deposit()`.
3815
+ *
3816
+ * @param params - Supply parameters
3817
+ * @returns TxResult
3818
+ *
3819
+ * @example
3820
+ * ```typescript
3821
+ * const result = await supply({
3822
+ * client, walletClient, account, poolAddress,
3823
+ * chainId: 11155111n,
3824
+ * token: WETH_ADDRESS,
3825
+ * amount: 1n * 10n**18n,
3826
+ * })
3827
+ * const receipt = await result.wait()
3828
+ * ```
3829
+ */
3830
+ async function supply(params) {
3831
+ const { client, walletClient, account, poolAddress, chainId, token, amount, isNativeETH, txOverrides } = params;
3832
+ const collateralTrackerAddress = await resolveCollateralTracker(client, poolAddress, chainId, token);
3833
+ return deposit({
3834
+ client,
3835
+ walletClient,
3836
+ account,
3837
+ collateralTrackerAddress,
3838
+ assets: amount,
3839
+ isNativeETH,
3840
+ txOverrides
3841
+ });
3842
+ }
3843
+ /**
3844
+ * Supply collateral and wait for confirmation.
3845
+ */
3846
+ async function supplyAndWait(params) {
3847
+ const result = await supply(params);
3848
+ return result.wait();
3849
+ }
3850
+ /**
3851
+ * Withdraw (unsupply) collateral from a pool.
3852
+ *
3853
+ * Resolves the correct collateral tracker from the token address,
3854
+ * then delegates to the low-level `withdraw()`.
3855
+ *
3856
+ * @param params - Unsupply parameters
3857
+ * @returns TxResult
3858
+ */
3859
+ async function unsupply(params) {
3860
+ const { client, walletClient, account, poolAddress, chainId, token, amount, txOverrides } = params;
3861
+ const collateralTrackerAddress = await resolveCollateralTracker(client, poolAddress, chainId, token);
3862
+ return withdraw({
3863
+ client,
3864
+ walletClient,
3865
+ account,
3866
+ collateralTrackerAddress,
3867
+ assets: amount,
3868
+ txOverrides
3869
+ });
3870
+ }
3871
+ /**
3872
+ * Withdraw collateral and wait for confirmation.
3873
+ */
3874
+ async function unsupplyAndWait(params) {
3875
+ const result = await unsupply(params);
3876
+ return result.wait();
3877
+ }
3878
+ /**
3879
+ * Borrow tokens from a pool via a loan position.
3880
+ *
3881
+ * Creates a loan tokenId and mints it via dispatch. The loan borrows the
3882
+ * specified token without swapping (ascending tick limits = no swap).
3883
+ *
3884
+ * @param params - Borrow parameters
3885
+ * @returns TxResult
3886
+ *
3887
+ * @example
3888
+ * ```typescript
3889
+ * const result = await borrow({
3890
+ * client, walletClient, account, poolAddress,
3891
+ * chainId: 11155111n,
3892
+ * token: USDC_ADDRESS,
3893
+ * amount: 100n * 10n**6n,
3894
+ * slippageBps: 500n,
3895
+ * })
3896
+ * const receipt = await result.wait()
3897
+ * ```
3898
+ */
3899
+ async function borrow(params) {
3900
+ const { client, walletClient, account, poolAddress, chainId, token, amount, slippageBps, existingPositionIds: explicitIds, builderCode = 0n, storage, txOverrides } = params;
3901
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
3902
+ const [pool, positionIds] = await Promise.all([getPool({
3903
+ client,
3904
+ poolAddress,
3905
+ chainId
3906
+ }), resolvePositionIds(explicitIds, storage, chainId, poolAddress, account)]);
3907
+ const token0 = pool.collateralTracker0.token;
3908
+ const token1 = pool.collateralTracker1.token;
3909
+ const tokenIndex = resolveTokenIndex(token, token0, token1);
3910
+ const { tokenId: loanTokenId, adjustedSize } = buildUniqueLoan(pool.poolId, tokenIndex, tokenIndex, pool.currentTick, pool.tickSpacing, positionIds, amount);
3911
+ const { low: tickLimitLow, high: tickLimitHigh } = tickLimits(pool.currentTick, slippageBps);
3912
+ const mintTickLimits = [
3913
+ Number(tickLimitLow),
3914
+ Number(tickLimitHigh),
3915
+ 0
3916
+ ];
3917
+ const positionIdList = [loanTokenId];
3918
+ const finalPositionIdList = [...positionIds, loanTokenId];
3919
+ const positionSizes = [adjustedSize];
3920
+ try {
3921
+ const txResult = await submitWrite({
3922
+ client,
3923
+ walletClient,
3924
+ account,
3925
+ address: poolAddress,
3926
+ abi: panopticPoolV2Abi,
3927
+ functionName: "dispatch",
3928
+ args: [
3929
+ positionIdList,
3930
+ finalPositionIdList,
3931
+ positionSizes,
3932
+ [mintTickLimits],
3933
+ false,
3934
+ builderCode
3935
+ ],
3936
+ txOverrides
3937
+ });
3938
+ return {
3939
+ ...txResult,
3940
+ loanTokenId
3941
+ };
3942
+ } catch (error) {
3943
+ if (isInputListFailError(error) && attempt < MAX_RETRIES - 1) continue;
3944
+ throw error;
3945
+ }
3946
+ }
3947
+ throw new MaxRetriesExceededError("borrow");
3948
+ }
3949
+ /**
3950
+ * Borrow tokens and wait for confirmation.
3951
+ * If storage is provided, auto-tracks the new loan position.
3952
+ */
3953
+ async function borrowAndWait(params) {
3954
+ const result = await borrow(params);
3955
+ const receipt = await result.wait();
3956
+ if (params.storage && params.chainId !== void 0) {
3957
+ const { poolAddress, chainId, storage, account, existingPositionIds: explicitIds } = params;
3958
+ const positionIds = explicitIds ?? await getTrackedPositionIds({
3959
+ chainId,
3960
+ poolAddress,
3961
+ account,
3962
+ storage
3963
+ });
3964
+ await savePositionIds(storage, chainId, poolAddress, account, [...positionIds, result.loanTokenId]);
3965
+ }
3966
+ return receipt;
3967
+ }
3968
+ /**
3969
+ * Preview a borrow without executing it.
3970
+ *
3971
+ * Builds the loan tokenId (same logic as `borrow`) and runs a
3972
+ * dry-run `simulateOpenPosition` to check feasibility and return
3973
+ * post-mint collateral requirements.
3974
+ *
3975
+ * @param params - Preview parameters
3976
+ * @returns Preview result with tokenId, size, and simulation data
3977
+ */
3978
+ async function previewBorrow(params) {
3979
+ const { client, poolAddress, chainId, token, amount, slippageBps, account, existingPositionIds } = params;
3980
+ const pool = await getPool({
3981
+ client,
3982
+ poolAddress,
3983
+ chainId
3984
+ });
3985
+ const token0 = pool.collateralTracker0.token;
3986
+ const token1 = pool.collateralTracker1.token;
3987
+ const tokenIndex = resolveTokenIndex(token, token0, token1);
3988
+ const { tokenId: loanTokenId, adjustedSize } = buildUniqueLoan(pool.poolId, tokenIndex, tokenIndex, pool.currentTick, pool.tickSpacing, existingPositionIds, amount);
3989
+ const { low: tickLimitLow, high: tickLimitHigh } = tickLimits(pool.currentTick, slippageBps);
3990
+ const simulation = await simulateOpenPosition({
3991
+ client,
3992
+ poolAddress,
3993
+ account,
3994
+ existingPositionIds,
3995
+ tokenId: loanTokenId,
3996
+ positionSize: adjustedSize,
3997
+ tickLimitLow: BigInt(tickLimitLow),
3998
+ tickLimitHigh: BigInt(tickLimitHigh),
3999
+ chainId
4000
+ });
4001
+ let collateralReqToken0 = null;
4002
+ let collateralReqToken1 = null;
4003
+ if (simulation.success) {
4004
+ const reqs = simulation.data.perPositionCollateralReqs;
4005
+ if (reqs.length > 0) {
4006
+ const loanReq = reqs[reqs.length - 1];
4007
+ collateralReqToken0 = loanReq.token0;
4008
+ collateralReqToken1 = loanReq.token1;
4009
+ }
4010
+ }
4011
+ return {
4012
+ loanTokenId,
4013
+ adjustedSize,
4014
+ simulation,
4015
+ collateralReqToken0,
4016
+ collateralReqToken1
4017
+ };
4018
+ }
4019
+ /**
4020
+ * Repay a loan position (full or partial).
4021
+ *
4022
+ * - Full repay (`amount >= currentSize`): Burns the loan position entirely.
4023
+ * - Partial repay (`amount < currentSize`): Uses roll-same-tokenId pattern
4024
+ * to reduce the position size.
4025
+ *
4026
+ * @param params - Repay parameters
4027
+ * @returns TxResult
4028
+ *
4029
+ * @example
4030
+ * ```typescript
4031
+ * // Full repay
4032
+ * await repay({
4033
+ * client, walletClient, account, poolAddress,
4034
+ * chainId: 11155111n,
4035
+ * loanTokenId,
4036
+ * amount: MAX_UINT128, // repay all
4037
+ * slippageBps: 500n,
4038
+ * })
4039
+ *
4040
+ * // Partial repay
4041
+ * await repay({
4042
+ * client, walletClient, account, poolAddress,
4043
+ * chainId: 11155111n,
4044
+ * loanTokenId,
4045
+ * amount: 50n * 10n**6n, // repay 50 USDC
4046
+ * slippageBps: 500n,
4047
+ * })
4048
+ * ```
4049
+ */
4050
+ async function repay(params) {
4051
+ const { client, walletClient, account, poolAddress, chainId, loanTokenId, amount, slippageBps, existingPositionIds: explicitIds, builderCode = 0n, storage, txOverrides } = params;
4052
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
4053
+ const [positionIds, fullDataResult] = await Promise.all([resolvePositionIds(explicitIds, storage, chainId, poolAddress, account), client.readContract({
4054
+ address: poolAddress,
4055
+ abi: panopticPoolV2Abi,
4056
+ functionName: "getFullPositionsData",
4057
+ args: [
4058
+ account,
4059
+ false,
4060
+ [loanTokenId]
4061
+ ]
4062
+ })]);
4063
+ const balanceRaw = fullDataResult[2][0];
4064
+ const currentSize = balanceRaw & (1n << 128n) - 1n;
4065
+ const pool = await getPool({
4066
+ client,
4067
+ poolAddress,
4068
+ chainId
4069
+ });
4070
+ const { low: tickLimitLow, high: tickLimitHigh } = tickLimits(pool.currentTick, slippageBps);
4071
+ const ascendingLimits = [
4072
+ Number(tickLimitLow),
4073
+ Number(tickLimitHigh),
4074
+ 0
4075
+ ];
4076
+ if (amount >= currentSize) {
4077
+ const finalPositionIdList = positionIds.filter((id) => id !== loanTokenId);
4078
+ try {
4079
+ return await submitWrite({
4080
+ client,
4081
+ walletClient,
4082
+ account,
4083
+ address: poolAddress,
4084
+ abi: panopticPoolV2Abi,
4085
+ functionName: "dispatch",
4086
+ args: [
4087
+ [loanTokenId],
4088
+ finalPositionIdList,
4089
+ [0n],
4090
+ [ascendingLimits],
4091
+ false,
4092
+ builderCode
4093
+ ],
4094
+ txOverrides
4095
+ });
4096
+ } catch (error) {
4097
+ if (isInputListFailError(error) && attempt < MAX_RETRIES - 1) continue;
4098
+ throw error;
4099
+ }
4100
+ } else {
4101
+ const newSize = currentSize - amount;
4102
+ const finalPositionIdList = positionIds.filter((id) => id !== loanTokenId).concat([loanTokenId]);
4103
+ try {
4104
+ return await submitWrite({
4105
+ client,
4106
+ walletClient,
4107
+ account,
4108
+ address: poolAddress,
4109
+ abi: panopticPoolV2Abi,
4110
+ functionName: "dispatch",
4111
+ args: [
4112
+ [loanTokenId, loanTokenId],
4113
+ finalPositionIdList,
4114
+ [currentSize, newSize],
4115
+ [ascendingLimits, ascendingLimits],
4116
+ false,
4117
+ builderCode
4118
+ ],
4119
+ txOverrides
4120
+ });
4121
+ } catch (error) {
4122
+ if (isInputListFailError(error) && attempt < MAX_RETRIES - 1) continue;
4123
+ throw error;
4124
+ }
4125
+ }
4126
+ }
4127
+ throw new MaxRetriesExceededError("repay");
4128
+ }
4129
+ /**
4130
+ * Repay a loan and wait for confirmation.
4131
+ * If storage is provided, auto-updates the tracked positions (removes on full repay).
4132
+ */
4133
+ async function repayAndWait(params) {
4134
+ const result = await repay(params);
4135
+ const receipt = await result.wait();
4136
+ if (params.storage && params.chainId !== void 0) {
4137
+ const { client, account, poolAddress, chainId, loanTokenId, storage, existingPositionIds: explicitIds } = params;
4138
+ const fullDataResult = await client.readContract({
4139
+ address: poolAddress,
4140
+ abi: panopticPoolV2Abi,
4141
+ functionName: "getFullPositionsData",
4142
+ args: [
4143
+ account,
4144
+ false,
4145
+ [loanTokenId]
4146
+ ]
4147
+ });
4148
+ const balanceRaw = fullDataResult[2][0];
4149
+ const currentSize = balanceRaw & (1n << 128n) - 1n;
4150
+ if (currentSize === 0n) {
4151
+ const positionIds = explicitIds ?? await getTrackedPositionIds({
4152
+ chainId,
4153
+ poolAddress,
4154
+ account,
4155
+ storage
4156
+ });
4157
+ await savePositionIds(storage, chainId, poolAddress, account, positionIds.filter((id) => id !== loanTokenId));
4158
+ }
4159
+ }
4160
+ return receipt;
4161
+ }
4162
+ /**
4163
+ * Identify loan tokenIds for a specific token and return their sizes.
4164
+ */
4165
+ async function getLoanPositionsForToken(client, poolAddress, account, existingPositionIds, tokenIndex) {
4166
+ const loanIds = existingPositionIds.filter((id) => {
4167
+ if (!isLoan(id)) return false;
4168
+ const legs = decodeAllLegs(id);
4169
+ return legs.length > 0 && legs[0].tokenType === tokenIndex;
4170
+ });
4171
+ if (loanIds.length === 0) return [];
4172
+ const fullData = await client.readContract({
4173
+ address: poolAddress,
4174
+ abi: panopticPoolV2Abi,
4175
+ functionName: "getFullPositionsData",
4176
+ args: [
4177
+ account,
4178
+ false,
4179
+ loanIds
4180
+ ]
4181
+ });
4182
+ const balances = fullData[2];
4183
+ return loanIds.map((tokenId, i) => {
4184
+ const positionSize = balances[i] & (1n << 128n) - 1n;
4185
+ const legs = decodeAllLegs(tokenId);
4186
+ const optionRatio = legs[0].optionRatio;
4187
+ const tokenAmount = positionSize * optionRatio;
4188
+ return {
4189
+ tokenId,
4190
+ positionSize,
4191
+ optionRatio,
4192
+ tokenAmount
4193
+ };
4194
+ });
4195
+ }
4196
+ /**
4197
+ * Smart repay: burns all loan positions for a token and optionally re-opens a smaller one.
4198
+ *
4199
+ * Unlike `repay()` which targets a single loanTokenId, this function handles
4200
+ * multiple loans transparently. The user specifies a repay amount and the SDK:
4201
+ * 1. Finds all loan positions for the specified token
4202
+ * 2. Burns them all
4203
+ * 3. If repayAmount < totalDebt, opens a new loan for the remainder
4204
+ *
4205
+ * @param params - Smart repay parameters
4206
+ * @returns TxResult
4207
+ */
4208
+ async function smartRepay(params) {
4209
+ const { client, walletClient, account, poolAddress, chainId, token, amount, slippageBps, existingPositionIds, builderCode = 0n, txOverrides } = params;
4210
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
4211
+ const pool = await getPool({
4212
+ client,
4213
+ poolAddress,
4214
+ chainId
4215
+ });
4216
+ const tokenIndex = resolveTokenIndex(token, pool.collateralTracker0.token, pool.collateralTracker1.token);
4217
+ const loans = await getLoanPositionsForToken(client, poolAddress, account, existingPositionIds, tokenIndex);
4218
+ if (loans.length === 0) throw new NoLoanPositionsError(token);
4219
+ const totalDebt = loans.reduce((sum, l) => sum + l.tokenAmount, 0n);
4220
+ const loanIds = loans.map((l) => l.tokenId);
4221
+ const remainder = totalDebt > amount ? totalDebt - amount : 0n;
4222
+ const { low: tickLimitLow, high: tickLimitHigh } = tickLimits(pool.currentTick, slippageBps);
4223
+ const ascendingLimits = [
4224
+ Number(tickLimitLow),
4225
+ Number(tickLimitHigh),
4226
+ 0
4227
+ ];
4228
+ const nonLoanIds = existingPositionIds.filter((id) => !loanIds.includes(id));
4229
+ const opsPositionIds = [...loanIds];
4230
+ const opsSizes = loanIds.map(() => 0n);
4231
+ const opsLimits = loanIds.map(() => ascendingLimits);
4232
+ let finalPositionIdList;
4233
+ if (remainder > 0n) {
4234
+ const { tokenId: newLoanId, adjustedSize } = buildUniqueLoan(pool.poolId, tokenIndex, tokenIndex, pool.currentTick, pool.tickSpacing, nonLoanIds, remainder);
4235
+ opsPositionIds.push(newLoanId);
4236
+ opsSizes.push(adjustedSize);
4237
+ opsLimits.push(ascendingLimits);
4238
+ finalPositionIdList = [...nonLoanIds, newLoanId];
4239
+ } else finalPositionIdList = nonLoanIds;
4240
+ try {
4241
+ return await submitWrite({
4242
+ client,
4243
+ walletClient,
4244
+ account,
4245
+ address: poolAddress,
4246
+ abi: panopticPoolV2Abi,
4247
+ functionName: "dispatch",
4248
+ args: [
4249
+ opsPositionIds,
4250
+ finalPositionIdList,
4251
+ opsSizes,
4252
+ opsLimits,
4253
+ false,
4254
+ builderCode
4255
+ ],
4256
+ txOverrides
4257
+ });
4258
+ } catch (error) {
4259
+ if (isInputListFailError(error) && attempt < MAX_RETRIES - 1) continue;
4260
+ throw error;
4261
+ }
4262
+ }
4263
+ throw new MaxRetriesExceededError("smartRepay");
4264
+ }
4265
+ /**
4266
+ * Smart repay and wait for confirmation.
4267
+ */
4268
+ async function smartRepayAndWait(params) {
4269
+ const result = await smartRepay(params);
4270
+ const receipt = await result.wait();
4271
+ if (params.storage && params.chainId !== void 0) {
4272
+ const { client, account, poolAddress, chainId, token, amount, storage, existingPositionIds } = params;
4273
+ const pool = await getPool({
4274
+ client,
4275
+ poolAddress,
4276
+ chainId
4277
+ });
4278
+ const tokenIndex = resolveTokenIndex(token, pool.collateralTracker0.token, pool.collateralTracker1.token);
4279
+ const loans = await getLoanPositionsForToken(client, poolAddress, account, existingPositionIds, tokenIndex);
4280
+ const loanIds = loans.map((l) => l.tokenId);
4281
+ const totalDebt = loans.reduce((sum, l) => sum + l.tokenAmount, 0n);
4282
+ const remainder = totalDebt > amount ? totalDebt - amount : 0n;
4283
+ const positionIds = existingPositionIds ?? await getTrackedPositionIds({
4284
+ chainId,
4285
+ poolAddress,
4286
+ account,
4287
+ storage
4288
+ });
4289
+ let updatedIds = positionIds.filter((id) => !loanIds.includes(id));
4290
+ if (remainder > 0n) {
4291
+ const nonLoanIds = existingPositionIds.filter((id) => !loanIds.includes(id));
4292
+ const newLoan = buildUniqueLoan(pool.poolId, tokenIndex, tokenIndex, pool.currentTick, pool.tickSpacing, nonLoanIds, remainder);
4293
+ updatedIds = [...updatedIds, newLoan.tokenId];
4294
+ }
4295
+ await savePositionIds(storage, chainId, poolAddress, account, updatedIds);
4296
+ }
4297
+ return receipt;
4298
+ }
4299
+
4300
+ //#endregion
4301
+ export { addLegToTokenId, approve, approveAndWait, approvePool, assertCanBurn, assertCanForceExercise, assertCanLiquidate, assertCanMint, assertFresh, assertHealthy, assertTradeable, borrow, borrowAndWait, buildBatchDispatchArgs, buildOpenPositionCalldata, buildUniqueLoan, calculateResyncBlock, cancelTransaction, checkApproval, clearCheckpoint, clearTrackedPositions, closePosition, closePositionAndWait, countLegs, createFileStorage, createMemoryStorage, createNonceManager, createTokenIdBuilder, decodeAllLegs, decodeDispatchCalldata, decodeLeg, decodePoolId, decodeTickSpacing as decodeTickSpacing$1, decodeTokenId, decodeVegoid, deployNewPool, deployNewPoolAndWait, deposit, depositAndWait, detectReorg, dispatch, dispatchAndWait, encodeLeg, encodePoolId, encodeV4PoolId, executeBatchDispatch, executeBatchDispatchAndWait, forceExercise, forceExerciseAndWait, getAssetIndex, getClosedPositionsKey, getOpenPositionIds, getPendingPositionsKey, getPoolMetaKey, getPoolPrefix, getPositionMetaKey, getPositionsKey, getSchemaVersionKey, getSyncCheckpointKey, getTrackedChunksKey, getTrackedPositionIds, hasLoanOrCredit, hasLongLeg, isCredit, isCreditLeg, isGasError, isInputListFailError, isLoan, isLoanLeg, isNonceError, isPositionTracked, isRetryableRpcError, isShortOnly, isSpread, jsonSerializer, liquidate, liquidateAndWait, loadCheckpoint, mint, mintAndWait, openPosition, openPositionAndWait, pokeOracle, pokeOracleAndWait, previewBorrow, previewUnwrap, previewWrap, publicBroadcaster, recoverSnapshot, recoverSnapshotFromTx, redeem, redeemAndWait, repay, repayAndWait, resolveTokenIndex, rollPosition, rollPositionAndWait, saveCheckpoint, settleAccumulatedPremia, settleAccumulatedPremiaAndWait, simulateOpenPosition, simulateWithTokenFlow, smartRepay, smartRepayAndWait, speedUpTransaction, supply, supplyAndWait, swapExactIn, swapExactInAndWait, swapExactOut, swapExactOutAndWait, syncPositions, unsupply, unsupplyAndWait, unwrapXstock, unwrapXstockAndWait, validateBatch, validatePoolId, verifyBlockContinuity, withdraw, withdrawAndWait, withdrawWithPositions, withdrawWithPositionsAndWait, wrapXstock, wrapXstockAndWait, xstockWrapperAbi };
4302
+ //# sourceMappingURL=writes-CVCD6Ers.js.map