@panoptic-eng/sdk 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cow/index.d.ts +5 -0
- package/dist/cow/index.js +5 -0
- package/dist/cow/types.d.ts +4 -0
- package/dist/cow/types.js +0 -0
- package/dist/cow-Dy-Cd09v.js +1405 -0
- package/dist/cow-Dy-Cd09v.js.map +1 -0
- package/dist/index-BuJcj5aO.d.ts +275 -0
- package/dist/index-BuJcj5aO.d.ts.map +1 -0
- package/dist/index-DVMjZi1E.d.ts +1801 -0
- package/dist/index-DVMjZi1E.d.ts.map +1 -0
- package/dist/index.d.ts +7537 -5094
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3101 -5726
- package/dist/index.js.map +1 -1
- package/dist/irm-CBrX8bjH.js +2375 -0
- package/dist/irm-CBrX8bjH.js.map +1 -0
- package/dist/irm-CGykVo3q.d.ts +32 -0
- package/dist/irm-CGykVo3q.d.ts.map +1 -0
- package/dist/irm-zWjtffWA.d.ts +85 -0
- package/dist/irm-zWjtffWA.d.ts.map +1 -0
- package/dist/panoptic/v2/index.d.ts +3972 -4004
- package/dist/panoptic/v2/index.d.ts.map +1 -1
- package/dist/panoptic/v2/index.js +7074 -9212
- package/dist/panoptic/v2/index.js.map +1 -1
- package/dist/panoptic/v2/types/index.d.ts +3 -0
- package/dist/panoptic/v2/types/index.js +0 -0
- package/dist/{rates-D-7EWaPS.js → position-FxaZi608.js} +7246 -5375
- package/dist/position-FxaZi608.js.map +1 -0
- package/dist/router-gzYGM1OO.js +1012 -0
- package/dist/router-gzYGM1OO.js.map +1 -0
- package/dist/types-BQejAFnu.d.ts +245 -0
- package/dist/types-BQejAFnu.d.ts.map +1 -0
- package/dist/types-CRvvn2ce.d.ts +128 -0
- package/dist/types-CRvvn2ce.d.ts.map +1 -0
- package/dist/uniswap/index.d.ts +291 -0
- package/dist/uniswap/index.d.ts.map +1 -0
- package/dist/uniswap/index.js +5 -0
- package/dist/writes-CVCD6Ers.js +4302 -0
- package/dist/writes-CVCD6Ers.js.map +1 -0
- package/package.json +17 -5
- package/dist/rates-BQ91Bbn6.d.ts +0 -38
- package/dist/rates-BQ91Bbn6.d.ts.map +0 -1
- package/dist/rates-D-7EWaPS.js.map +0 -1
|
@@ -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
|