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