@panoptic-eng/mcp 0.1.1 → 0.1.2
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/README.md +22 -1
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +19 -10
- package/dist/index.js +1 -1
- package/dist/{server--YF2LHqv.js → server-CabyB7Fb.js} +821 -177
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DEFAULT_VEGOID, MAX_TICK, MIN_TICK, STANDARD_TICK_WIDTHS, WAD, createMemoryStorage, createTokenIdBuilder, decodeTokenId, getAccountBuyingPower, getAccountCollateral, getAccountGreeks, getAccountHistory, getAccountPremia, getChainDeployment, getDeltaHedgeParams, getLegDelta, getMarginBuffer, getNetLiquidationValues, getOpenPositionPreview, getPool, getPoolDeploymentBlock, getPoolLiquidities, getPositions, getTrackedPositionIds, hasLoanOrCredit, hasLongLeg, isCall, isCreditLeg, isLiquidatable, isLoanLeg, isShortOnly, isSpread, scanChunks, simulateClosePosition, syncPositions } from "@panoptic-eng/sdk/v2";
|
|
1
|
+
import { DEFAULT_VEGOID, MAX_TICK, MIN_TICK, STANDARD_TICK_WIDTHS, WAD, createMemoryStorage, createTokenIdBuilder, decodeTokenId, getAccountBuyingPower, getAccountCollateral, getAccountGreeks, getAccountHistory, getAccountPremia, getChainDeployment, getDeltaHedgeParams, getLegDelta, getMarginBuffer, getNetLiquidationValues, getOpenPositionPreview, getPool, getPoolDeploymentBlock, getPoolLiquidities, getPositions, getTrackedPositionIds, hasLoanOrCredit, hasLongLeg, isCall, isCreditLeg, isLiquidatable, isLoanLeg, isShortOnly, isSpread, roundToTickSpacing, scanChunks, simulateClosePosition, syncPositions, tickToPriceDecimalScaled } from "@panoptic-eng/sdk/v2";
|
|
2
2
|
import { createPublicClient, getAddress, http, isAddress, keccak256, toBytes, toHex } from "viem";
|
|
3
|
-
import {
|
|
3
|
+
import { mainnet, sepolia } from "viem/chains";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -9,27 +9,29 @@ import { z } from "zod";
|
|
|
9
9
|
/** viem chain definitions (carry the Multicall3 address the SDK reads rely on). */
|
|
10
10
|
const VIEM_CHAINS = {
|
|
11
11
|
[mainnet.id]: mainnet,
|
|
12
|
-
[base.id]: base,
|
|
13
12
|
[sepolia.id]: sepolia
|
|
14
13
|
};
|
|
15
|
-
|
|
16
|
-
const KNOWN_CHAIN_IDS = [
|
|
17
|
-
mainnet.id,
|
|
18
|
-
base.id,
|
|
19
|
-
sepolia.id
|
|
20
|
-
];
|
|
14
|
+
const KNOWN_CHAIN_IDS = [mainnet.id, sepolia.id];
|
|
21
15
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* decimal form only (never Number(), so "1e0" is rejected)
|
|
25
|
-
*
|
|
16
|
+
* Validate a user-supplied chain-id string against the trusted known-chain
|
|
17
|
+
* constants AND the SDK deployment registry, returning the matched id. Matches
|
|
18
|
+
* on canonical decimal form only (never Number(), so "1e0" is rejected). This is
|
|
19
|
+
* the single validation path shared by resolveChainId (per-call args) and
|
|
20
|
+
* resolveConfig (startup) so the two can't drift.
|
|
26
21
|
*/
|
|
27
|
-
function
|
|
28
|
-
if (rawChainId === void 0 || rawChainId.trim() === "") return fallback;
|
|
22
|
+
function toSupportedChainId(rawChainId) {
|
|
29
23
|
const chainId = KNOWN_CHAIN_IDS.find((id) => id.toString() === rawChainId.trim());
|
|
30
|
-
if (chainId === void 0) throw new Error(`Unsupported chain id ${rawChainId}. Supported: ${KNOWN_CHAIN_IDS.join(", ")}.`);
|
|
24
|
+
if (chainId === void 0 || getChainDeployment(chainId) === void 0) throw new Error(`Unsupported chain id ${rawChainId}. Supported: ${KNOWN_CHAIN_IDS.join(", ")}.`);
|
|
31
25
|
return chainId;
|
|
32
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve an optional user-supplied chain-id string to a trusted, deployed
|
|
29
|
+
* known-chain constant, falling back to `fallback` when omitted/blank.
|
|
30
|
+
*/
|
|
31
|
+
function resolveChainId(rawChainId, fallback) {
|
|
32
|
+
if (rawChainId === void 0 || rawChainId.trim() === "") return fallback;
|
|
33
|
+
return toSupportedChainId(rawChainId);
|
|
34
|
+
}
|
|
33
35
|
var ConfigError = class extends Error {};
|
|
34
36
|
/**
|
|
35
37
|
* Resolve server config from CLI flags (`--rpc-url`, `--chain-id`,
|
|
@@ -43,8 +45,12 @@ function resolveConfig(argv, env) {
|
|
|
43
45
|
if (!rpcUrl) throw new ConfigError("Missing RPC URL. Set PANOPTIC_RPC_URL or pass --rpc-url <url>.");
|
|
44
46
|
const rawChainId = flags["chain-id"] ?? env.PANOPTIC_CHAIN_ID;
|
|
45
47
|
if (!rawChainId) throw new ConfigError("Missing chain id. Set PANOPTIC_CHAIN_ID or pass --chain-id <id>.");
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
let chainId;
|
|
49
|
+
try {
|
|
50
|
+
chainId = toSupportedChainId(rawChainId);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new ConfigError(err instanceof Error ? err.message : String(err));
|
|
53
|
+
}
|
|
48
54
|
const subgraphUrl = flags["subgraph-url"] ?? env.PANOPTIC_SUBGRAPH_URL ?? void 0;
|
|
49
55
|
return {
|
|
50
56
|
rpcUrl,
|
|
@@ -65,7 +71,8 @@ async function createContext(config) {
|
|
|
65
71
|
return {
|
|
66
72
|
config,
|
|
67
73
|
client,
|
|
68
|
-
deployment
|
|
74
|
+
deployment,
|
|
75
|
+
session: {}
|
|
69
76
|
};
|
|
70
77
|
}
|
|
71
78
|
function parseFlags(argv) {
|
|
@@ -94,6 +101,46 @@ function parseAccountAddress(address) {
|
|
|
94
101
|
return getAddress(address);
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/numeric.ts
|
|
106
|
+
/**
|
|
107
|
+
* Shared parsing for the decimal-/hex-string numeric inputs every tool accepts.
|
|
108
|
+
* Centralizes the validation so malformed input fails early with a consistent,
|
|
109
|
+
* readable message instead of a raw `BigInt()` "Cannot convert" throw (or, for
|
|
110
|
+
* `Number()`, silent coercion of forms like "1e9").
|
|
111
|
+
*/
|
|
112
|
+
const UNSIGNED = /^\d+$/;
|
|
113
|
+
const SIGNED = /^-?\d+$/;
|
|
114
|
+
/**
|
|
115
|
+
* Parse a required decimal integer string to a bigint. Set `signed` to allow a
|
|
116
|
+
* leading `-` (e.g. ticks, target delta); otherwise only non-negative integers
|
|
117
|
+
* are accepted (blocks, epochs, sizes).
|
|
118
|
+
*/
|
|
119
|
+
function parseDecimalBigInt(raw, label, opts = {}) {
|
|
120
|
+
const trimmed = raw.trim();
|
|
121
|
+
const pattern = opts.signed ? SIGNED : UNSIGNED;
|
|
122
|
+
if (!pattern.test(trimmed)) throw new Error(`${label} must be a ${opts.signed ? "decimal" : "non-negative decimal"} integer string.`);
|
|
123
|
+
return BigInt(trimmed);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Like {@link parseDecimalBigInt} but treats `undefined` and blank/whitespace
|
|
127
|
+
* strings as "not provided", returning `undefined`.
|
|
128
|
+
*/
|
|
129
|
+
function parseOptionalDecimalBigInt(raw, label, opts = {}) {
|
|
130
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
131
|
+
return parseDecimalBigInt(raw, label, opts);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Parse a TokenId supplied as either a 0x-hex or a decimal string. TokenIds are
|
|
135
|
+
* unsigned, so both forms map straight through `BigInt`, with a readable error
|
|
136
|
+
* on anything else.
|
|
137
|
+
*/
|
|
138
|
+
function parseTokenId(raw, label = "tokenId") {
|
|
139
|
+
const trimmed = raw.trim();
|
|
140
|
+
if (!/^(0x[0-9a-fA-F]+|\d+)$/.test(trimmed)) throw new Error(`${label} must be a 0x-hex or decimal string.`);
|
|
141
|
+
return BigInt(trimmed);
|
|
142
|
+
}
|
|
143
|
+
|
|
97
144
|
//#endregion
|
|
98
145
|
//#region src/serialize.ts
|
|
99
146
|
/**
|
|
@@ -166,42 +213,82 @@ function buildEnvelope(input) {
|
|
|
166
213
|
//#region src/tools/positions.ts
|
|
167
214
|
const STABLE_SYMBOL = /^(usdc|usdt|usdbc|usds|usde|dai|frax|lusd|gusd|tusd|pyusd)$/i;
|
|
168
215
|
/**
|
|
169
|
-
*
|
|
170
|
-
* override wins
|
|
171
|
-
*
|
|
216
|
+
* Choose the frame in which to report an account's aggregate net delta. An
|
|
217
|
+
* explicit caller override always wins. Absent that, the stablecoin side (if
|
|
218
|
+
* any) is treated as the numeraire so the other token becomes the reporting
|
|
219
|
+
* frame — a convenience default only, surfaced with `source`/`reason` so callers
|
|
220
|
+
* can see it was guessed and re-request a different frame.
|
|
172
221
|
*/
|
|
173
|
-
function
|
|
222
|
+
function suggestDeltaFrame(pool, override) {
|
|
174
223
|
if (override !== void 0) return {
|
|
175
|
-
|
|
176
|
-
|
|
224
|
+
frame: override === 0 ? 0n : 1n,
|
|
225
|
+
source: "override",
|
|
226
|
+
reason: "Caller-specified reporting frame."
|
|
177
227
|
};
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
228
|
+
const sym0 = pool.collateralTracker0.symbol;
|
|
229
|
+
const sym1 = pool.collateralTracker1.symbol;
|
|
230
|
+
if (STABLE_SYMBOL.test(sym0)) return {
|
|
231
|
+
frame: 1n,
|
|
232
|
+
source: "heuristic",
|
|
233
|
+
reason: `token0 (${sym0}) looks like a stablecoin numeraire, so net delta is reported in token1 (${sym1}) units.`
|
|
181
234
|
};
|
|
182
|
-
if (STABLE_SYMBOL.test(
|
|
183
|
-
|
|
184
|
-
|
|
235
|
+
if (STABLE_SYMBOL.test(sym1)) return {
|
|
236
|
+
frame: 0n,
|
|
237
|
+
source: "heuristic",
|
|
238
|
+
reason: `token1 (${sym1}) looks like a stablecoin numeraire, so net delta is reported in token0 (${sym0}) units.`
|
|
185
239
|
};
|
|
186
240
|
return {
|
|
187
|
-
|
|
188
|
-
|
|
241
|
+
frame: 0n,
|
|
242
|
+
source: "default",
|
|
243
|
+
reason: `Neither token (${sym0}/${sym1}) looks like a stablecoin; defaulting the reporting frame to token0. Pass assetIndex to choose the frame explicitly.`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Resolve the delta reporting frame with precedence: explicit tool argument →
|
|
248
|
+
* session preference (set via configure_session) → heuristic guess. Only the
|
|
249
|
+
* last case sets `needsUserChoice`, signalling the caller to ask the user which
|
|
250
|
+
* token they want their net delta denominated in.
|
|
251
|
+
*/
|
|
252
|
+
function resolveDeltaFrame(pool, argIndex, sessionPreference) {
|
|
253
|
+
if (argIndex !== void 0) return {
|
|
254
|
+
...suggestDeltaFrame(pool, argIndex),
|
|
255
|
+
source: "override",
|
|
256
|
+
needsUserChoice: false
|
|
257
|
+
};
|
|
258
|
+
if (sessionPreference !== void 0) {
|
|
259
|
+
const symbol = sessionPreference === 0n ? pool.collateralTracker0.symbol : pool.collateralTracker1.symbol;
|
|
260
|
+
return {
|
|
261
|
+
frame: sessionPreference,
|
|
262
|
+
source: "session",
|
|
263
|
+
reason: `Using the session's preferred reporting frame (${symbol}); set via configure_session.`,
|
|
264
|
+
needsUserChoice: false
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
...suggestDeltaFrame(pool),
|
|
269
|
+
needsUserChoice: true
|
|
189
270
|
};
|
|
190
271
|
}
|
|
272
|
+
/** Explanatory note attached to every account-level net-delta figure. */
|
|
273
|
+
const DELTA_FRAME_NOTE = "Net delta is a single figure expressed in the chosen reporting frame (assetIndex). It is not a claim about a canonical pool asset: each position leg carries its own asset, and selecting a frame overrides every leg into that frame. The sign and magnitude depend on the frame — pass assetIndex to report in the other token.";
|
|
191
274
|
/**
|
|
192
275
|
* Discover an account's open position ids on the configured pool via the SDK's
|
|
193
276
|
* event-scan sync into a fresh in-memory store — no subgraph required. Returns
|
|
194
277
|
* the populated storage so greeks reads (which read synced positions from
|
|
195
278
|
* storage) can reuse it.
|
|
196
279
|
*/
|
|
280
|
+
const SYNC_TTL_MS = 15e3;
|
|
281
|
+
const SYNC_CACHE_MAX = 64;
|
|
197
282
|
const syncCache = new Map();
|
|
198
283
|
async function syncAccountPositions(ctx, account, fromBlock) {
|
|
199
284
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
200
285
|
const chainId = BigInt(ctx.config.chainId);
|
|
201
286
|
const cacheKey = `${chainId}:${poolAddress}:${account}:${fromBlock ?? "default"}`;
|
|
287
|
+
const now = Date.now();
|
|
202
288
|
const cached = syncCache.get(cacheKey);
|
|
203
|
-
if (cached) return cached;
|
|
204
|
-
|
|
289
|
+
if (cached && cached.expiresAt > now) return cached.promise;
|
|
290
|
+
if (cached) syncCache.delete(cacheKey);
|
|
291
|
+
const promise = (async () => {
|
|
205
292
|
const storage = createMemoryStorage();
|
|
206
293
|
await syncPositions({
|
|
207
294
|
client: ctx.client,
|
|
@@ -224,9 +311,19 @@ async function syncAccountPositions(ctx, account, fromBlock) {
|
|
|
224
311
|
storage
|
|
225
312
|
};
|
|
226
313
|
})();
|
|
227
|
-
syncCache.
|
|
228
|
-
|
|
229
|
-
|
|
314
|
+
if (syncCache.size >= SYNC_CACHE_MAX) {
|
|
315
|
+
const oldest = syncCache.keys().next().value;
|
|
316
|
+
if (oldest !== void 0) syncCache.delete(oldest);
|
|
317
|
+
}
|
|
318
|
+
const entry = {
|
|
319
|
+
promise,
|
|
320
|
+
expiresAt: now + SYNC_TTL_MS
|
|
321
|
+
};
|
|
322
|
+
syncCache.set(cacheKey, entry);
|
|
323
|
+
promise.catch(() => {
|
|
324
|
+
if (syncCache.get(cacheKey) === entry) syncCache.delete(cacheKey);
|
|
325
|
+
});
|
|
326
|
+
return promise;
|
|
230
327
|
}
|
|
231
328
|
|
|
232
329
|
//#endregion
|
|
@@ -242,7 +339,7 @@ function registerAccountHealth(server, ctx) {
|
|
|
242
339
|
}, async ({ address, fromBlock }) => {
|
|
243
340
|
const account = parseAccountAddress(address);
|
|
244
341
|
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
245
|
-
const { poolAddress, tokenIds } = await syncAccountPositions(ctx, account, fromBlock
|
|
342
|
+
const { poolAddress, tokenIds } = await syncAccountPositions(ctx, account, parseOptionalDecimalBigInt(fromBlock, "fromBlock"));
|
|
246
343
|
const [buyingPower, marginBuffer, liquidation] = await Promise.all([
|
|
247
344
|
getAccountBuyingPower({
|
|
248
345
|
client: ctx.client,
|
|
@@ -270,11 +367,50 @@ function registerAccountHealth(server, ctx) {
|
|
|
270
367
|
openTokenIds: tokenIds,
|
|
271
368
|
buyingPower,
|
|
272
369
|
marginBuffer,
|
|
273
|
-
liquidation
|
|
370
|
+
liquidation,
|
|
371
|
+
...tokenIds.length === 0 ? { note: "No open positions found on this pool for this account; margin buffer/liquidation reflect an unlevered account. If positions are expected, widen the scan with fromBlock — public RPCs can truncate large eth_getLogs ranges." } : {}
|
|
274
372
|
});
|
|
275
373
|
});
|
|
276
374
|
}
|
|
277
375
|
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/tools/pool.ts
|
|
378
|
+
const POOL_TTL_MS = 6e3;
|
|
379
|
+
const POOL_CACHE_MAX = 8;
|
|
380
|
+
const poolCache = new Map();
|
|
381
|
+
/**
|
|
382
|
+
* Read the configured pool's live state, reusing a recent read when one is in
|
|
383
|
+
* flight or fresh. Failures are not cached, so the next call retries.
|
|
384
|
+
*/
|
|
385
|
+
async function getCachedPool(ctx) {
|
|
386
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
387
|
+
const chainId = BigInt(ctx.config.chainId);
|
|
388
|
+
const cacheKey = `${chainId}:${poolAddress}`;
|
|
389
|
+
const now = Date.now();
|
|
390
|
+
const cached = poolCache.get(cacheKey);
|
|
391
|
+
if (cached && cached.expiresAt > now) return cached.promise;
|
|
392
|
+
if (cached) poolCache.delete(cacheKey);
|
|
393
|
+
const promise = getPool({
|
|
394
|
+
client: ctx.client,
|
|
395
|
+
poolAddress,
|
|
396
|
+
chainId,
|
|
397
|
+
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
398
|
+
});
|
|
399
|
+
if (poolCache.size >= POOL_CACHE_MAX) {
|
|
400
|
+
const oldest = poolCache.keys().next().value;
|
|
401
|
+
if (oldest !== void 0) poolCache.delete(oldest);
|
|
402
|
+
}
|
|
403
|
+
const entry = {
|
|
404
|
+
promise,
|
|
405
|
+
expiresAt: now + POOL_TTL_MS
|
|
406
|
+
};
|
|
407
|
+
poolCache.set(cacheKey, entry);
|
|
408
|
+
promise.catch(() => {
|
|
409
|
+
if (poolCache.get(cacheKey) === entry) poolCache.delete(cacheKey);
|
|
410
|
+
});
|
|
411
|
+
return promise;
|
|
412
|
+
}
|
|
413
|
+
|
|
278
414
|
//#endregion
|
|
279
415
|
//#region src/tools/advisePosition.ts
|
|
280
416
|
const NEAR_LIQUIDATION_TICKS = 500n;
|
|
@@ -319,26 +455,14 @@ function registerAdvisePosition(server, ctx) {
|
|
|
319
455
|
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
320
456
|
}
|
|
321
457
|
}, async ({ tokenId, address, fromBlock }) => {
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
let id;
|
|
325
|
-
try {
|
|
326
|
-
id = BigInt(tokenId.trim());
|
|
327
|
-
} catch {
|
|
328
|
-
throw new Error(`Invalid tokenId (expected a 0x-hex or decimal string): ${tokenId}`);
|
|
329
|
-
}
|
|
458
|
+
const account = parseAccountAddress(address);
|
|
459
|
+
const id = parseTokenId(tokenId);
|
|
330
460
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
331
461
|
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
332
|
-
const
|
|
333
|
-
const pool = await getPool({
|
|
334
|
-
client: ctx.client,
|
|
335
|
-
poolAddress,
|
|
336
|
-
chainId,
|
|
337
|
-
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
338
|
-
});
|
|
462
|
+
const pool = await getCachedPool(ctx);
|
|
339
463
|
const decoded = decodeTokenId(id);
|
|
340
464
|
const legs = decoded.legs.map((leg) => analyzeLeg(leg, pool.currentTick));
|
|
341
|
-
const { tokenIds } = await syncAccountPositions(ctx, account, fromBlock
|
|
465
|
+
const { tokenIds } = await syncAccountPositions(ctx, account, parseOptionalDecimalBigInt(fromBlock, "fromBlock"));
|
|
342
466
|
const owned = tokenIds.some((t) => t === id);
|
|
343
467
|
const idList = owned ? tokenIds : [...tokenIds, id];
|
|
344
468
|
const [{ positions }, premia, marginBuffer, liquidation] = await Promise.all([
|
|
@@ -435,6 +559,35 @@ function registerAdvisePosition(server, ctx) {
|
|
|
435
559
|
});
|
|
436
560
|
}
|
|
437
561
|
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/tools/tickMath.ts
|
|
564
|
+
/**
|
|
565
|
+
* Clamp a tick into the valid Uniswap range [MIN_TICK, MAX_TICK]. The SDK
|
|
566
|
+
* provides tick↔price math and roundToTickSpacing but no plain clamp, so this
|
|
567
|
+
* one small helper lives here and is shared by the tools that build tick bands.
|
|
568
|
+
*/
|
|
569
|
+
function clampTick(tick) {
|
|
570
|
+
if (tick < MIN_TICK) return MIN_TICK;
|
|
571
|
+
if (tick > MAX_TICK) return MAX_TICK;
|
|
572
|
+
return tick;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Round a tick to the nearest `spacing` multiple, guaranteed to stay within the
|
|
576
|
+
* valid range. roundToTickSpacing rounds to the *nearest* multiple, so a value
|
|
577
|
+
* at the range edge can round outward past MIN_TICK/MAX_TICK when spacing does
|
|
578
|
+
* not divide them; this clamps the result to the inward-aligned multiples
|
|
579
|
+
* (largest ≤ MAX_TICK, smallest ≥ MIN_TICK) so the output is always both a
|
|
580
|
+
* `spacing` multiple AND in range — required by callers like scanChunks.
|
|
581
|
+
*/
|
|
582
|
+
function roundToSpacingInRange(tick, spacing) {
|
|
583
|
+
const rounded = roundToTickSpacing(clampTick(tick), spacing);
|
|
584
|
+
const maxAligned = MAX_TICK - MAX_TICK % spacing;
|
|
585
|
+
const minAligned = MIN_TICK - MIN_TICK % spacing;
|
|
586
|
+
if (rounded > maxAligned) return maxAligned;
|
|
587
|
+
if (rounded < minAligned) return minAligned;
|
|
588
|
+
return rounded;
|
|
589
|
+
}
|
|
590
|
+
|
|
438
591
|
//#endregion
|
|
439
592
|
//#region src/tools/chunkSpread.ts
|
|
440
593
|
const DEFAULT_TIMESCALE = "1D";
|
|
@@ -443,15 +596,6 @@ const DEFAULT_SCAN_HALFSPAN = 6000n;
|
|
|
443
596
|
function toMultiplier(spreadWad) {
|
|
444
597
|
return Number(spreadWad * 10000n / WAD) / 1e4;
|
|
445
598
|
}
|
|
446
|
-
function clampTick$1(tick) {
|
|
447
|
-
if (tick < MIN_TICK) return MIN_TICK;
|
|
448
|
-
if (tick > MAX_TICK) return MAX_TICK;
|
|
449
|
-
return tick;
|
|
450
|
-
}
|
|
451
|
-
/** Floor a tick to the nearest lower multiple of `step`. */
|
|
452
|
-
function alignDown(tick, step) {
|
|
453
|
-
return tick - (tick % step + step) % step;
|
|
454
|
-
}
|
|
455
599
|
function registerChunkSpread(server, ctx) {
|
|
456
600
|
server.registerTool("chunk_spread", {
|
|
457
601
|
title: "Chunk spread multipliers",
|
|
@@ -472,21 +616,16 @@ function registerChunkSpread(server, ctx) {
|
|
|
472
616
|
}, async ({ timescale, width, tickLower, tickUpper, vegoid }) => {
|
|
473
617
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
474
618
|
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
475
|
-
const pool = await
|
|
476
|
-
client: ctx.client,
|
|
477
|
-
poolAddress,
|
|
478
|
-
chainId: BigInt(ctx.config.chainId),
|
|
479
|
-
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
480
|
-
});
|
|
619
|
+
const pool = await getCachedPool(ctx);
|
|
481
620
|
const resolvedTimescale = timescale ?? DEFAULT_TIMESCALE;
|
|
482
621
|
const rawWidth = width !== void 0 ? BigInt(width) : STANDARD_TICK_WIDTHS[resolvedTimescale];
|
|
483
622
|
const chunkWidth = rawWidth < pool.tickSpacing ? pool.tickSpacing : rawWidth - rawWidth % pool.tickSpacing;
|
|
484
623
|
const vg = vegoid !== void 0 ? BigInt(vegoid) : DEFAULT_VEGOID;
|
|
485
624
|
const halfWidthsRaw = DEFAULT_SCAN_HALFSPAN / chunkWidth;
|
|
486
625
|
const halfWidths = halfWidthsRaw < 2n ? 2n : halfWidthsRaw;
|
|
487
|
-
const center =
|
|
488
|
-
const lower = tickLower !== void 0 ?
|
|
489
|
-
const upper = tickUpper !== void 0 ?
|
|
626
|
+
const center = roundToSpacingInRange(pool.currentTick, chunkWidth);
|
|
627
|
+
const lower = tickLower !== void 0 ? roundToSpacingInRange(parseDecimalBigInt(tickLower, "tickLower", { signed: true }), chunkWidth) : roundToSpacingInRange(center - halfWidths * chunkWidth, chunkWidth);
|
|
628
|
+
const upper = tickUpper !== void 0 ? roundToSpacingInRange(parseDecimalBigInt(tickUpper, "tickUpper", { signed: true }), chunkWidth) : roundToSpacingInRange(center + halfWidths * chunkWidth, chunkWidth);
|
|
490
629
|
const { chunks, _meta } = await scanChunks({
|
|
491
630
|
client: ctx.client,
|
|
492
631
|
poolAddress,
|
|
@@ -587,7 +726,7 @@ function registerGetPortfolio(server, ctx) {
|
|
|
587
726
|
}
|
|
588
727
|
}, async ({ address, fromBlock }) => {
|
|
589
728
|
const account = parseAccountAddress(address);
|
|
590
|
-
const { poolAddress, chainId, tokenIds, storage } = await syncAccountPositions(ctx, account, fromBlock
|
|
729
|
+
const { poolAddress, chainId, tokenIds, storage } = await syncAccountPositions(ctx, account, parseOptionalDecimalBigInt(fromBlock, "fromBlock"));
|
|
591
730
|
const [positions, collateral, greeks] = await Promise.all([
|
|
592
731
|
getPositions({
|
|
593
732
|
client: ctx.client,
|
|
@@ -614,7 +753,55 @@ function registerGetPortfolio(server, ctx) {
|
|
|
614
753
|
openTokenIds: tokenIds,
|
|
615
754
|
positions,
|
|
616
755
|
collateral,
|
|
617
|
-
greeks
|
|
756
|
+
greeks,
|
|
757
|
+
...tokenIds.length === 0 ? { note: "No open positions found on this pool for this account (positions/greeks reflect collateral only). If positions are expected, widen the scan with fromBlock — public RPCs can truncate large eth_getLogs ranges." } : {}
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
//#endregion
|
|
763
|
+
//#region src/tools/session.ts
|
|
764
|
+
/**
|
|
765
|
+
* Build the response a delta tool returns when it cannot pick a reporting frame
|
|
766
|
+
* on its own. Rather than silently guessing which token the user wants their net
|
|
767
|
+
* delta denominated in, the tool surfaces a question the agent should put to the
|
|
768
|
+
* user, plus the exact follow-up call to make. `suggestion` carries the
|
|
769
|
+
* heuristic guess so the agent can offer a sensible default.
|
|
770
|
+
*/
|
|
771
|
+
function assetPreferenceRequired(params) {
|
|
772
|
+
const { token0, token1, suggestedAssetIndex, suggestionReason } = params;
|
|
773
|
+
const suggestedSymbol = suggestedAssetIndex === 0n ? token0 : token1;
|
|
774
|
+
return jsonResult({
|
|
775
|
+
status: "needs_asset_preference",
|
|
776
|
+
question: `Which token should net delta be reported in for this pool — ${token0} (assetIndex 0) or ${token1} (assetIndex 1)? There is no canonical answer: it is a denomination choice (e.g. some traders treat ETH as the numeraire, others USDC).`,
|
|
777
|
+
suggestion: {
|
|
778
|
+
assetIndex: suggestedAssetIndex,
|
|
779
|
+
symbol: suggestedSymbol,
|
|
780
|
+
reason: suggestionReason
|
|
781
|
+
},
|
|
782
|
+
howToProceed: "Ask the user which token they prefer, then either pass `assetIndex` (0 or 1) directly to this tool for a one-off, or call `configure_session` with `assetIndex` to set it for the rest of the session so you are not asked again.",
|
|
783
|
+
tokens: {
|
|
784
|
+
token0,
|
|
785
|
+
token1
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
function registerConfigureSession(server, ctx) {
|
|
790
|
+
server.registerTool("configure_session", {
|
|
791
|
+
title: "Configure the session",
|
|
792
|
+
description: "Set (or read) preferences for this MCP session. Currently records the user's preferred delta reporting frame (`assetIndex`) so delta tools (suggest_delta_hedge, hedge_params) report net delta in the chosen token's units without asking again. There is no canonical pool asset — this is a per-user denomination choice, so ask the user before setting it. Call with no arguments to read the current session preferences.",
|
|
793
|
+
inputSchema: {
|
|
794
|
+
assetIndex: z.union([z.literal(0), z.literal(1)]).optional().describe("Preferred delta reporting frame for the session: 0 = report net delta in token0 units, 1 = token1 units. Omit to leave unchanged."),
|
|
795
|
+
clearAssetIndex: z.boolean().optional().describe("Set true to clear a previously set preferred assetIndex.")
|
|
796
|
+
}
|
|
797
|
+
}, async ({ assetIndex, clearAssetIndex }) => {
|
|
798
|
+
if (clearAssetIndex === true) ctx.session.preferredAssetIndex = void 0;
|
|
799
|
+
else if (assetIndex !== void 0) ctx.session.preferredAssetIndex = assetIndex === 0 ? 0n : 1n;
|
|
800
|
+
const current = ctx.session.preferredAssetIndex;
|
|
801
|
+
return jsonResult({
|
|
802
|
+
chainId: ctx.config.chainId,
|
|
803
|
+
preferences: { preferredAssetIndex: current },
|
|
804
|
+
note: current === void 0 ? "No preferred delta reporting frame is set. Delta tools will ask the user (or you can pass assetIndex per call)." : `Net delta will be reported in token${current} units for this session unless a call overrides it with an explicit assetIndex.`
|
|
618
805
|
});
|
|
619
806
|
});
|
|
620
807
|
}
|
|
@@ -630,32 +817,17 @@ function registerHedgeParams(server, ctx) {
|
|
|
630
817
|
tokenId: z.string().optional().describe("TokenId (0x-hex or decimal) for single-position hedging. Requires positionSize."),
|
|
631
818
|
positionSize: z.string().optional().describe("Position size (decimal string). Required with tokenId."),
|
|
632
819
|
targetDelta: z.string().optional().describe("Target delta in asset smallest units. Defaults to 0 (delta-neutral)."),
|
|
633
|
-
assetIndex: z.union([z.literal(0), z.literal(1)]).optional().describe("
|
|
820
|
+
assetIndex: z.union([z.literal(0), z.literal(1)]).optional().describe("Reporting frame for whole-portfolio net delta: 0 = token0 units, 1 = token1 units. A denomination choice that overrides each leg's own asset when aggregating, NOT a canonical pool asset. Ignored in single-position (tokenId) mode, where the leg's own asset is used. If omitted and no session preference is set (via configure_session), the tool asks the user which token to use instead of guessing."),
|
|
634
821
|
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
635
822
|
}
|
|
636
823
|
}, async ({ address, tokenId, positionSize, targetDelta, assetIndex, fromBlock }) => {
|
|
637
824
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
638
825
|
const chainId = BigInt(ctx.config.chainId);
|
|
639
|
-
|
|
640
|
-
try {
|
|
641
|
-
target = targetDelta !== void 0 ? BigInt(targetDelta) : 0n;
|
|
642
|
-
} catch {
|
|
643
|
-
throw new Error(`Invalid targetDelta (expected a decimal string): ${targetDelta}`);
|
|
644
|
-
}
|
|
826
|
+
const target = targetDelta !== void 0 ? parseDecimalBigInt(targetDelta, "targetDelta", { signed: true }) : 0n;
|
|
645
827
|
if (tokenId !== void 0) {
|
|
646
828
|
if (positionSize === void 0) throw new Error("positionSize is required when hedging a single tokenId.");
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
try {
|
|
650
|
-
id = BigInt(tokenId.trim());
|
|
651
|
-
} catch {
|
|
652
|
-
throw new Error(`Invalid tokenId (expected a 0x-hex or decimal string): ${tokenId}`);
|
|
653
|
-
}
|
|
654
|
-
try {
|
|
655
|
-
size = BigInt(positionSize);
|
|
656
|
-
} catch {
|
|
657
|
-
throw new Error(`Invalid positionSize (expected a decimal string): ${positionSize}`);
|
|
658
|
-
}
|
|
829
|
+
const id = parseTokenId(tokenId);
|
|
830
|
+
const size = parseDecimalBigInt(positionSize, "positionSize");
|
|
659
831
|
const hedge$1 = await getDeltaHedgeParams({
|
|
660
832
|
client: ctx.client,
|
|
661
833
|
poolAddress,
|
|
@@ -673,21 +845,17 @@ function registerHedgeParams(server, ctx) {
|
|
|
673
845
|
});
|
|
674
846
|
}
|
|
675
847
|
if (address === void 0) throw new Error("Provide either `address` (whole-portfolio) or `tokenId` + `positionSize` (single).");
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
848
|
+
const account = parseAccountAddress(address);
|
|
849
|
+
const pool = await getCachedPool(ctx);
|
|
850
|
+
const frameResolution = resolveDeltaFrame(pool, assetIndex, ctx.session.preferredAssetIndex);
|
|
851
|
+
if (frameResolution.needsUserChoice) return assetPreferenceRequired({
|
|
852
|
+
token0: pool.collateralTracker0.symbol,
|
|
853
|
+
token1: pool.collateralTracker1.symbol,
|
|
854
|
+
suggestedAssetIndex: frameResolution.frame,
|
|
855
|
+
suggestionReason: frameResolution.reason
|
|
683
856
|
});
|
|
684
|
-
const { asset,
|
|
685
|
-
|
|
686
|
-
try {
|
|
687
|
-
scanFromBlock = fromBlock ? BigInt(fromBlock) : void 0;
|
|
688
|
-
} catch {
|
|
689
|
-
throw new Error(`Invalid fromBlock (expected a decimal string): ${fromBlock}`);
|
|
690
|
-
}
|
|
857
|
+
const { frame: asset, source: assetFrameSource, reason: assetFrameReason } = frameResolution;
|
|
858
|
+
const scanFromBlock = parseOptionalDecimalBigInt(fromBlock, "fromBlock");
|
|
691
859
|
const { tokenIds, storage } = await syncAccountPositions(ctx, account, scanFromBlock);
|
|
692
860
|
const greeks = await getAccountGreeks({
|
|
693
861
|
client: ctx.client,
|
|
@@ -712,7 +880,13 @@ function registerHedgeParams(server, ctx) {
|
|
|
712
880
|
pool: poolAddress,
|
|
713
881
|
assetIndex: asset,
|
|
714
882
|
assetSymbol: asset === 0n ? pool.collateralTracker0.symbol : pool.collateralTracker1.symbol,
|
|
715
|
-
|
|
883
|
+
deltaFrame: {
|
|
884
|
+
assetIndex: asset,
|
|
885
|
+
symbol: asset === 0n ? pool.collateralTracker0.symbol : pool.collateralTracker1.symbol,
|
|
886
|
+
source: assetFrameSource,
|
|
887
|
+
reason: assetFrameReason
|
|
888
|
+
},
|
|
889
|
+
deltaFrameNote: DELTA_FRAME_NOTE,
|
|
716
890
|
openTokenIds: tokenIds,
|
|
717
891
|
currentDelta: greeks.totalDelta,
|
|
718
892
|
targetDelta: target,
|
|
@@ -721,6 +895,486 @@ function registerHedgeParams(server, ctx) {
|
|
|
721
895
|
});
|
|
722
896
|
}
|
|
723
897
|
|
|
898
|
+
//#endregion
|
|
899
|
+
//#region src/tools/vaults.ts
|
|
900
|
+
const HYPO_VAULT_READ_ABI = [
|
|
901
|
+
{
|
|
902
|
+
type: "function",
|
|
903
|
+
name: "accountant",
|
|
904
|
+
inputs: [],
|
|
905
|
+
outputs: [{ type: "address" }],
|
|
906
|
+
stateMutability: "view"
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
type: "function",
|
|
910
|
+
name: "balanceOf",
|
|
911
|
+
inputs: [{ type: "address" }],
|
|
912
|
+
outputs: [{ type: "uint256" }],
|
|
913
|
+
stateMutability: "view"
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
type: "function",
|
|
917
|
+
name: "decimals",
|
|
918
|
+
inputs: [],
|
|
919
|
+
outputs: [{ type: "uint8" }],
|
|
920
|
+
stateMutability: "view"
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
type: "function",
|
|
924
|
+
name: "depositEpoch",
|
|
925
|
+
inputs: [],
|
|
926
|
+
outputs: [{ type: "uint128" }],
|
|
927
|
+
stateMutability: "view"
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
type: "function",
|
|
931
|
+
name: "depositEpochState",
|
|
932
|
+
inputs: [{ type: "uint256" }],
|
|
933
|
+
outputs: [
|
|
934
|
+
{ type: "uint128" },
|
|
935
|
+
{ type: "uint128" },
|
|
936
|
+
{ type: "uint128" }
|
|
937
|
+
],
|
|
938
|
+
stateMutability: "view"
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
type: "function",
|
|
942
|
+
name: "feeWallet",
|
|
943
|
+
inputs: [],
|
|
944
|
+
outputs: [{ type: "address" }],
|
|
945
|
+
stateMutability: "view"
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
type: "function",
|
|
949
|
+
name: "manager",
|
|
950
|
+
inputs: [],
|
|
951
|
+
outputs: [{ type: "address" }],
|
|
952
|
+
stateMutability: "view"
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
type: "function",
|
|
956
|
+
name: "name",
|
|
957
|
+
inputs: [],
|
|
958
|
+
outputs: [{ type: "string" }],
|
|
959
|
+
stateMutability: "view"
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
type: "function",
|
|
963
|
+
name: "performanceFeeBps",
|
|
964
|
+
inputs: [],
|
|
965
|
+
outputs: [{ type: "uint256" }],
|
|
966
|
+
stateMutability: "view"
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
type: "function",
|
|
970
|
+
name: "queuedDeposit",
|
|
971
|
+
inputs: [{ type: "address" }, { type: "uint256" }],
|
|
972
|
+
outputs: [{ type: "uint128" }],
|
|
973
|
+
stateMutability: "view"
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
type: "function",
|
|
977
|
+
name: "queuedWithdrawal",
|
|
978
|
+
inputs: [{ type: "address" }, { type: "uint256" }],
|
|
979
|
+
outputs: [
|
|
980
|
+
{ type: "uint128" },
|
|
981
|
+
{ type: "uint128" },
|
|
982
|
+
{ type: "bool" }
|
|
983
|
+
],
|
|
984
|
+
stateMutability: "view"
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
type: "function",
|
|
988
|
+
name: "reservedWithdrawalAssets",
|
|
989
|
+
inputs: [],
|
|
990
|
+
outputs: [{ type: "uint256" }],
|
|
991
|
+
stateMutability: "view"
|
|
992
|
+
},
|
|
993
|
+
{
|
|
994
|
+
type: "function",
|
|
995
|
+
name: "symbol",
|
|
996
|
+
inputs: [],
|
|
997
|
+
outputs: [{ type: "string" }],
|
|
998
|
+
stateMutability: "view"
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
type: "function",
|
|
1002
|
+
name: "totalSupply",
|
|
1003
|
+
inputs: [],
|
|
1004
|
+
outputs: [{ type: "uint256" }],
|
|
1005
|
+
stateMutability: "view"
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
type: "function",
|
|
1009
|
+
name: "underlyingToken",
|
|
1010
|
+
inputs: [],
|
|
1011
|
+
outputs: [{ type: "address" }],
|
|
1012
|
+
stateMutability: "view"
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
type: "function",
|
|
1016
|
+
name: "userBasis",
|
|
1017
|
+
inputs: [{ type: "address" }],
|
|
1018
|
+
outputs: [{ type: "uint256" }],
|
|
1019
|
+
stateMutability: "view"
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
type: "function",
|
|
1023
|
+
name: "withdrawalEpoch",
|
|
1024
|
+
inputs: [],
|
|
1025
|
+
outputs: [{ type: "uint128" }],
|
|
1026
|
+
stateMutability: "view"
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
type: "function",
|
|
1030
|
+
name: "withdrawalEpochState",
|
|
1031
|
+
inputs: [{ type: "uint256" }],
|
|
1032
|
+
outputs: [
|
|
1033
|
+
{ type: "uint128" },
|
|
1034
|
+
{ type: "uint128" },
|
|
1035
|
+
{ type: "uint128" }
|
|
1036
|
+
],
|
|
1037
|
+
stateMutability: "view"
|
|
1038
|
+
}
|
|
1039
|
+
];
|
|
1040
|
+
const VAULTS = [{
|
|
1041
|
+
key: "wethPlpVault",
|
|
1042
|
+
managerKey: "wethPlpVaultManager",
|
|
1043
|
+
underlyingKey: "weth",
|
|
1044
|
+
label: "WETH PLP Vault",
|
|
1045
|
+
strategy: "Passive liquidity provider"
|
|
1046
|
+
}, {
|
|
1047
|
+
key: "usdcPlpVault",
|
|
1048
|
+
managerKey: "usdcPlpVaultManager",
|
|
1049
|
+
underlyingKey: "usdc",
|
|
1050
|
+
label: "USDC PLP Vault",
|
|
1051
|
+
strategy: "Passive liquidity provider"
|
|
1052
|
+
}];
|
|
1053
|
+
const UNDERLYINGS = {
|
|
1054
|
+
1: {
|
|
1055
|
+
weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|
1056
|
+
usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
|
1057
|
+
},
|
|
1058
|
+
11155111: {
|
|
1059
|
+
weth: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14",
|
|
1060
|
+
usdc: "0xFFFeD8254566B7F800f6D8CDb843ec75AE49B07A"
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
function readAddress(value) {
|
|
1064
|
+
return typeof value === "string" && isAddress(value) ? getAddress(value) : void 0;
|
|
1065
|
+
}
|
|
1066
|
+
function getVaultRegistry(chainId) {
|
|
1067
|
+
const dep = getChainDeployment(chainId);
|
|
1068
|
+
if (dep === void 0) return [];
|
|
1069
|
+
return getVaultRegistryFromDeployment(chainId, dep);
|
|
1070
|
+
}
|
|
1071
|
+
function getVaultRegistryFromDeployment(chainId, dep) {
|
|
1072
|
+
const underlyings = UNDERLYINGS[chainId];
|
|
1073
|
+
if (underlyings === void 0) return [];
|
|
1074
|
+
return VAULTS.flatMap((vault) => {
|
|
1075
|
+
const vaultAddress = readAddress(dep.hypovault.vaults[vault.key]);
|
|
1076
|
+
const managerAddress = readAddress(dep.hypovault.managers[vault.managerKey]);
|
|
1077
|
+
const accountantAddress = readAddress(dep.hypovault.core.accountant);
|
|
1078
|
+
const underlyingToken = readAddress(underlyings[vault.underlyingKey]);
|
|
1079
|
+
const panopticPool = readAddress(dep.panoptic.pool.panopticPool);
|
|
1080
|
+
const collateralTracker0 = readAddress(dep.panoptic.pool.collateralTracker0);
|
|
1081
|
+
const collateralTracker1 = readAddress(dep.panoptic.pool.collateralTracker1);
|
|
1082
|
+
if (vaultAddress === void 0 || managerAddress === void 0 || accountantAddress === void 0 || underlyingToken === void 0 || panopticPool === void 0 || collateralTracker0 === void 0 || collateralTracker1 === void 0) return [];
|
|
1083
|
+
return [{
|
|
1084
|
+
chainId,
|
|
1085
|
+
key: vault.key,
|
|
1086
|
+
label: vault.label,
|
|
1087
|
+
strategy: vault.strategy,
|
|
1088
|
+
vaultAddress,
|
|
1089
|
+
managerAddress,
|
|
1090
|
+
accountantAddress,
|
|
1091
|
+
underlyingToken,
|
|
1092
|
+
panopticPool,
|
|
1093
|
+
collateralTracker0,
|
|
1094
|
+
collateralTracker1
|
|
1095
|
+
}];
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
function findVaultByAddress(address, chainId) {
|
|
1099
|
+
return getVaultRegistry(chainId).find((vault) => vault.vaultAddress === address);
|
|
1100
|
+
}
|
|
1101
|
+
function getVaultAddressRoles(address, chainId) {
|
|
1102
|
+
const roles = [];
|
|
1103
|
+
const registry = getVaultRegistry(chainId);
|
|
1104
|
+
for (const vault of registry) {
|
|
1105
|
+
if (vault.vaultAddress === address) roles.push(`hypovault:${vault.key}`);
|
|
1106
|
+
if (vault.managerAddress === address) roles.push(`hypovault:${vault.key}:manager`);
|
|
1107
|
+
if (vault.accountantAddress === address) roles.push(`hypovault:${vault.key}:accountant`);
|
|
1108
|
+
if (vault.underlyingToken === address) roles.push(`hypovault:${vault.key}:underlyingToken`);
|
|
1109
|
+
}
|
|
1110
|
+
const dep = getChainDeployment(chainId);
|
|
1111
|
+
if (dep !== void 0) {
|
|
1112
|
+
const core = dep.hypovault.core;
|
|
1113
|
+
const coreRoles = {
|
|
1114
|
+
hypovaultFactory: core.factory,
|
|
1115
|
+
hypovaultImplementation: core.hypoVaultImplementation,
|
|
1116
|
+
hypovaultAccountant: core.accountant,
|
|
1117
|
+
hypovaultRolesAuthority: core.rolesAuthority,
|
|
1118
|
+
hypovaultCollateralTrackerDecoderAndSanitizer: core.collateralTrackerDecoderAndSanitizer
|
|
1119
|
+
};
|
|
1120
|
+
for (const [role, value] of Object.entries(coreRoles)) {
|
|
1121
|
+
const candidate = readAddress(value);
|
|
1122
|
+
if (candidate === address && !roles.includes(role)) roles.push(role);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return roles;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Batch several HypoVault view reads into a single Multicall3 round-trip (all
|
|
1129
|
+
* resolved at one block, so the returned fields are mutually consistent). Uses
|
|
1130
|
+
* allowFailure:false so any reverting read throws, matching the previous
|
|
1131
|
+
* per-call behavior. Returns results positionally in the order of `calls`.
|
|
1132
|
+
*/
|
|
1133
|
+
async function multicallVault(ctx, vaultAddress, calls) {
|
|
1134
|
+
const multicall = ctx.client.multicall;
|
|
1135
|
+
return multicall({
|
|
1136
|
+
contracts: calls.map((call) => ({
|
|
1137
|
+
address: vaultAddress,
|
|
1138
|
+
abi: HYPO_VAULT_READ_ABI,
|
|
1139
|
+
functionName: call.functionName,
|
|
1140
|
+
args: call.args ?? []
|
|
1141
|
+
})),
|
|
1142
|
+
allowFailure: false
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
function depositEpochStateTuple([assetsDeposited, sharesReceived, assetsFulfilled]) {
|
|
1146
|
+
return {
|
|
1147
|
+
assetsDeposited,
|
|
1148
|
+
sharesReceived,
|
|
1149
|
+
assetsFulfilled
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
function withdrawalEpochStateTuple([sharesWithdrawn, assetsReceived, sharesFulfilled]) {
|
|
1153
|
+
return {
|
|
1154
|
+
sharesWithdrawn,
|
|
1155
|
+
assetsReceived,
|
|
1156
|
+
sharesFulfilled
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function queuedWithdrawalTuple([amount, basis, shouldRedeposit]) {
|
|
1160
|
+
return {
|
|
1161
|
+
amount,
|
|
1162
|
+
basis,
|
|
1163
|
+
shouldRedeposit
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
async function fetchVaultSummary(ctx, vaultAddress) {
|
|
1167
|
+
const registry = findVaultByAddress(vaultAddress, ctx.config.chainId);
|
|
1168
|
+
const [name, symbol, decimals, totalSupply, underlyingToken, manager, accountant, feeWallet, performanceFeeBps, depositEpoch, withdrawalEpoch, reservedWithdrawalAssets] = await multicallVault(ctx, vaultAddress, [
|
|
1169
|
+
{ functionName: "name" },
|
|
1170
|
+
{ functionName: "symbol" },
|
|
1171
|
+
{ functionName: "decimals" },
|
|
1172
|
+
{ functionName: "totalSupply" },
|
|
1173
|
+
{ functionName: "underlyingToken" },
|
|
1174
|
+
{ functionName: "manager" },
|
|
1175
|
+
{ functionName: "accountant" },
|
|
1176
|
+
{ functionName: "feeWallet" },
|
|
1177
|
+
{ functionName: "performanceFeeBps" },
|
|
1178
|
+
{ functionName: "depositEpoch" },
|
|
1179
|
+
{ functionName: "withdrawalEpoch" },
|
|
1180
|
+
{ functionName: "reservedWithdrawalAssets" }
|
|
1181
|
+
]);
|
|
1182
|
+
const [depositEpochState, withdrawalEpochState] = await multicallVault(ctx, vaultAddress, [{
|
|
1183
|
+
functionName: "depositEpochState",
|
|
1184
|
+
args: [depositEpoch]
|
|
1185
|
+
}, {
|
|
1186
|
+
functionName: "withdrawalEpochState",
|
|
1187
|
+
args: [withdrawalEpoch]
|
|
1188
|
+
}]);
|
|
1189
|
+
return {
|
|
1190
|
+
chainId: ctx.config.chainId,
|
|
1191
|
+
vaultAddress,
|
|
1192
|
+
registry,
|
|
1193
|
+
metadata: {
|
|
1194
|
+
name,
|
|
1195
|
+
symbol,
|
|
1196
|
+
decimals
|
|
1197
|
+
},
|
|
1198
|
+
totalSupply,
|
|
1199
|
+
underlyingToken,
|
|
1200
|
+
manager,
|
|
1201
|
+
accountant,
|
|
1202
|
+
feeWallet,
|
|
1203
|
+
performanceFeeBps,
|
|
1204
|
+
depositEpoch,
|
|
1205
|
+
withdrawalEpoch,
|
|
1206
|
+
reservedWithdrawalAssets,
|
|
1207
|
+
currentDepositEpochState: depositEpochStateTuple(depositEpochState),
|
|
1208
|
+
currentWithdrawalEpochState: withdrawalEpochStateTuple(withdrawalEpochState),
|
|
1209
|
+
navAndSharePrice: "Not returned by this v1 tool: HypoVault totalAssets/convertToAssets require managerInput-aware accounting."
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
async function fetchVaultAccount(ctx, vaultAddress, account, requestedDepositEpoch, requestedWithdrawalEpoch) {
|
|
1213
|
+
const [currentDepositEpoch, currentWithdrawalEpoch, shareBalance, userBasis] = await multicallVault(ctx, vaultAddress, [
|
|
1214
|
+
{ functionName: "depositEpoch" },
|
|
1215
|
+
{ functionName: "withdrawalEpoch" },
|
|
1216
|
+
{
|
|
1217
|
+
functionName: "balanceOf",
|
|
1218
|
+
args: [account]
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
functionName: "userBasis",
|
|
1222
|
+
args: [account]
|
|
1223
|
+
}
|
|
1224
|
+
]);
|
|
1225
|
+
const depositEpoch = requestedDepositEpoch ?? currentDepositEpoch;
|
|
1226
|
+
const withdrawalEpoch = requestedWithdrawalEpoch ?? currentWithdrawalEpoch;
|
|
1227
|
+
const [queuedDeposit, queuedWithdrawal, depositEpochState, withdrawalEpochState] = await multicallVault(ctx, vaultAddress, [
|
|
1228
|
+
{
|
|
1229
|
+
functionName: "queuedDeposit",
|
|
1230
|
+
args: [account, depositEpoch]
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
functionName: "queuedWithdrawal",
|
|
1234
|
+
args: [account, withdrawalEpoch]
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
functionName: "depositEpochState",
|
|
1238
|
+
args: [depositEpoch]
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
functionName: "withdrawalEpochState",
|
|
1242
|
+
args: [withdrawalEpoch]
|
|
1243
|
+
}
|
|
1244
|
+
]);
|
|
1245
|
+
const withdrawal = queuedWithdrawalTuple(queuedWithdrawal);
|
|
1246
|
+
const depositEpochStateResult = depositEpochStateTuple(depositEpochState);
|
|
1247
|
+
const withdrawalEpochStateResult = withdrawalEpochStateTuple(withdrawalEpochState);
|
|
1248
|
+
return {
|
|
1249
|
+
chainId: ctx.config.chainId,
|
|
1250
|
+
vaultAddress,
|
|
1251
|
+
account,
|
|
1252
|
+
currentDepositEpoch,
|
|
1253
|
+
currentWithdrawalEpoch,
|
|
1254
|
+
shareBalance,
|
|
1255
|
+
userBasis,
|
|
1256
|
+
deposit: {
|
|
1257
|
+
epoch: depositEpoch,
|
|
1258
|
+
queuedAssets: queuedDeposit,
|
|
1259
|
+
epochState: depositEpochStateResult,
|
|
1260
|
+
executable: depositEpoch < currentDepositEpoch && queuedDeposit > 0n && depositEpochStateResult.assetsFulfilled > 0n && depositEpochStateResult.sharesReceived > 0n
|
|
1261
|
+
},
|
|
1262
|
+
withdrawal: {
|
|
1263
|
+
epoch: withdrawalEpoch,
|
|
1264
|
+
queued: withdrawal,
|
|
1265
|
+
epochState: withdrawalEpochStateResult,
|
|
1266
|
+
executable: withdrawalEpoch < currentWithdrawalEpoch && withdrawal.amount > 0n && withdrawalEpochStateResult.sharesFulfilled > 0n && withdrawalEpochStateResult.assetsReceived > 0n
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function registerListVaults(server, ctx) {
|
|
1271
|
+
server.registerTool("list_vaults", {
|
|
1272
|
+
title: "List HypoVaults",
|
|
1273
|
+
description: "Known HypoVault deployments for a chain — vaults, managers, accountant, underlying token, pool, collateral trackers, and strategy labels.",
|
|
1274
|
+
inputSchema: { chainId: z.string().optional().describe("Chain id (decimal string) to look up. Defaults to the configured chain.") }
|
|
1275
|
+
}, async ({ chainId }) => {
|
|
1276
|
+
const target = resolveChainId(chainId, ctx.config.chainId);
|
|
1277
|
+
const vaults = getVaultRegistry(target);
|
|
1278
|
+
return jsonResult({
|
|
1279
|
+
chainId: target,
|
|
1280
|
+
vaults,
|
|
1281
|
+
liveState: target === ctx.config.chainId ? "Use vault_summary or vault_account for live vault state." : `Live state unavailable: this server is configured for chain ${ctx.config.chainId}.`
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
function registerVaultSummary(server, ctx) {
|
|
1286
|
+
server.registerTool("vault_summary", {
|
|
1287
|
+
title: "Summarize a HypoVault",
|
|
1288
|
+
description: "Read live HypoVault state: supply, underlying, manager, accountant, fee wallet, fee bps, epochs, reserved withdrawal assets, and current epoch totals. Does not compute NAV/share price.",
|
|
1289
|
+
inputSchema: { vaultAddress: z.string().describe("The HypoVault address on the configured chain.") }
|
|
1290
|
+
}, async ({ vaultAddress }) => {
|
|
1291
|
+
const vault = parseAccountAddress(vaultAddress);
|
|
1292
|
+
return jsonResult(await fetchVaultSummary(ctx, vault));
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
function registerVaultAccount(server, ctx) {
|
|
1296
|
+
server.registerTool("vault_account", {
|
|
1297
|
+
title: "Inspect a HypoVault account",
|
|
1298
|
+
description: "A user's HypoVault share balance, basis, queued deposit, queued withdrawal, redeposit flag, and whether selected epochs appear executable.",
|
|
1299
|
+
inputSchema: {
|
|
1300
|
+
vaultAddress: z.string().describe("The HypoVault address on the configured chain."),
|
|
1301
|
+
address: z.string().describe("The user/account address."),
|
|
1302
|
+
depositEpoch: z.string().optional().describe("Optional deposit epoch (decimal string). Defaults to current depositEpoch."),
|
|
1303
|
+
withdrawalEpoch: z.string().optional().describe("Optional withdrawal epoch (decimal string). Defaults to current withdrawalEpoch.")
|
|
1304
|
+
}
|
|
1305
|
+
}, async ({ vaultAddress, address, depositEpoch, withdrawalEpoch }) => {
|
|
1306
|
+
const vault = parseAccountAddress(vaultAddress);
|
|
1307
|
+
const account = parseAccountAddress(address);
|
|
1308
|
+
return jsonResult(await fetchVaultAccount(ctx, vault, account, parseOptionalDecimalBigInt(depositEpoch, "depositEpoch"), parseOptionalDecimalBigInt(withdrawalEpoch, "withdrawalEpoch")));
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
function registerVaultExposure(server, ctx) {
|
|
1312
|
+
server.registerTool("vault_exposure", {
|
|
1313
|
+
title: "Inspect HypoVault Panoptic exposure",
|
|
1314
|
+
description: "Treat a HypoVault as the Panoptic account and return open positions, collateral, greeks, buying power, margin buffer, and liquidation status.",
|
|
1315
|
+
inputSchema: {
|
|
1316
|
+
vaultAddress: z.string().describe("The HypoVault address on the configured chain."),
|
|
1317
|
+
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
1318
|
+
}
|
|
1319
|
+
}, async ({ vaultAddress, fromBlock }) => {
|
|
1320
|
+
const vault = parseAccountAddress(vaultAddress);
|
|
1321
|
+
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
1322
|
+
const fromBlockNumber = parseOptionalDecimalBigInt(fromBlock, "fromBlock");
|
|
1323
|
+
const { poolAddress, chainId, tokenIds, storage } = await syncAccountPositions(ctx, vault, fromBlockNumber);
|
|
1324
|
+
const [positions, collateral, greeks, buyingPower, marginBuffer, liquidation] = await Promise.all([
|
|
1325
|
+
getPositions({
|
|
1326
|
+
client: ctx.client,
|
|
1327
|
+
poolAddress,
|
|
1328
|
+
owner: vault,
|
|
1329
|
+
tokenIds
|
|
1330
|
+
}),
|
|
1331
|
+
getAccountCollateral({
|
|
1332
|
+
client: ctx.client,
|
|
1333
|
+
poolAddress,
|
|
1334
|
+
account: vault
|
|
1335
|
+
}),
|
|
1336
|
+
getAccountGreeks({
|
|
1337
|
+
client: ctx.client,
|
|
1338
|
+
chainId,
|
|
1339
|
+
poolAddress,
|
|
1340
|
+
account: vault,
|
|
1341
|
+
storage
|
|
1342
|
+
}),
|
|
1343
|
+
getAccountBuyingPower({
|
|
1344
|
+
client: ctx.client,
|
|
1345
|
+
poolAddress,
|
|
1346
|
+
account: vault,
|
|
1347
|
+
tokenIds
|
|
1348
|
+
}),
|
|
1349
|
+
getMarginBuffer({
|
|
1350
|
+
client: ctx.client,
|
|
1351
|
+
poolAddress,
|
|
1352
|
+
account: vault,
|
|
1353
|
+
tokenIds,
|
|
1354
|
+
queryAddress
|
|
1355
|
+
}),
|
|
1356
|
+
isLiquidatable({
|
|
1357
|
+
client: ctx.client,
|
|
1358
|
+
poolAddress,
|
|
1359
|
+
account: vault,
|
|
1360
|
+
tokenIds
|
|
1361
|
+
})
|
|
1362
|
+
]);
|
|
1363
|
+
return jsonResult({
|
|
1364
|
+
vaultAddress: vault,
|
|
1365
|
+
registry: findVaultByAddress(vault, ctx.config.chainId),
|
|
1366
|
+
pool: poolAddress,
|
|
1367
|
+
openTokenIds: tokenIds,
|
|
1368
|
+
positions,
|
|
1369
|
+
collateral,
|
|
1370
|
+
greeks,
|
|
1371
|
+
buyingPower,
|
|
1372
|
+
marginBuffer,
|
|
1373
|
+
liquidation
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
|
|
724
1378
|
//#endregion
|
|
725
1379
|
//#region src/tools/identifyAddress.ts
|
|
726
1380
|
function registerIdentifyAddress(server, ctx) {
|
|
@@ -745,7 +1399,7 @@ function registerIdentifyAddress(server, ctx) {
|
|
|
745
1399
|
poolManager: dep.infrastructure.poolManager,
|
|
746
1400
|
stateView: dep.infrastructure.stateView
|
|
747
1401
|
};
|
|
748
|
-
const roles = Object.entries(candidates).filter(([, addr]) => isAddress(addr) && getAddress(addr) === needle).map(([role]) => role);
|
|
1402
|
+
const roles = [...Object.entries(candidates).filter(([, addr]) => isAddress(addr) && getAddress(addr) === needle).map(([role]) => role), ...getVaultAddressRoles(needle, target)];
|
|
749
1403
|
return jsonResult({
|
|
750
1404
|
address: needle,
|
|
751
1405
|
chainId: target,
|
|
@@ -768,15 +1422,9 @@ function registerLiquidityMap(server, ctx) {
|
|
|
768
1422
|
}, async ({ startTick, nTicks }) => {
|
|
769
1423
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
770
1424
|
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
771
|
-
const
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
client: ctx.client,
|
|
775
|
-
poolAddress,
|
|
776
|
-
chainId,
|
|
777
|
-
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
778
|
-
});
|
|
779
|
-
const start = startTick !== void 0 && startTick.trim() !== "" ? BigInt(startTick) : pool.currentTick - count / 2n * pool.tickSpacing;
|
|
1425
|
+
const count = parseOptionalDecimalBigInt(nTicks, "nTicks") ?? DEFAULT_N_TICKS;
|
|
1426
|
+
const pool = await getCachedPool(ctx);
|
|
1427
|
+
const start = parseOptionalDecimalBigInt(startTick, "startTick", { signed: true }) ?? pool.currentTick - count / 2n * pool.tickSpacing;
|
|
780
1428
|
const liquidities = await getPoolLiquidities({
|
|
781
1429
|
client: ctx.client,
|
|
782
1430
|
poolAddress,
|
|
@@ -834,14 +1482,14 @@ function registerListPools(server, ctx) {
|
|
|
834
1482
|
//#region src/tools/positionScenario.ts
|
|
835
1483
|
const DEFAULT_SPAN = 2500n;
|
|
836
1484
|
const DEFAULT_POINTS = 25;
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1485
|
+
const PRICE_PRECISION = 18n;
|
|
1486
|
+
/**
|
|
1487
|
+
* Human token1-per-token0 price at a tick, as a decimal string. Uses the SDK's
|
|
1488
|
+
* exact BigInt tick math (no Math.pow float precision loss / over/underflow at
|
|
1489
|
+
* extreme ticks) and applies the token-decimal scale.
|
|
1490
|
+
*/
|
|
843
1491
|
function priceAtTick(tick, decimals0, decimals1) {
|
|
844
|
-
return
|
|
1492
|
+
return tickToPriceDecimalScaled(tick, decimals0, decimals1, PRICE_PRECISION);
|
|
845
1493
|
}
|
|
846
1494
|
function registerPositionScenario(server, ctx) {
|
|
847
1495
|
server.registerTool("position_scenario", {
|
|
@@ -856,17 +1504,11 @@ function registerPositionScenario(server, ctx) {
|
|
|
856
1504
|
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
857
1505
|
}
|
|
858
1506
|
}, async ({ address, startTick, endTick, points, includePendingPremium, fromBlock }) => {
|
|
859
|
-
|
|
860
|
-
const account = getAddress(address);
|
|
1507
|
+
const account = parseAccountAddress(address);
|
|
861
1508
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
862
1509
|
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
863
|
-
const pool = await
|
|
864
|
-
|
|
865
|
-
poolAddress,
|
|
866
|
-
chainId: BigInt(ctx.config.chainId),
|
|
867
|
-
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
868
|
-
});
|
|
869
|
-
const { tokenIds } = await syncAccountPositions(ctx, account, fromBlock ? BigInt(fromBlock) : void 0);
|
|
1510
|
+
const pool = await getCachedPool(ctx);
|
|
1511
|
+
const { tokenIds } = await syncAccountPositions(ctx, account, parseOptionalDecimalBigInt(fromBlock, "fromBlock"));
|
|
870
1512
|
if (tokenIds.length === 0) return jsonResult({
|
|
871
1513
|
account,
|
|
872
1514
|
pool: poolAddress,
|
|
@@ -875,8 +1517,10 @@ function registerPositionScenario(server, ctx) {
|
|
|
875
1517
|
note: "Account has no open positions on this pool; nothing to evaluate."
|
|
876
1518
|
});
|
|
877
1519
|
const n = points ?? DEFAULT_POINTS;
|
|
878
|
-
const
|
|
879
|
-
const
|
|
1520
|
+
const startTickValue = parseOptionalDecimalBigInt(startTick, "startTick", { signed: true });
|
|
1521
|
+
const endTickValue = parseOptionalDecimalBigInt(endTick, "endTick", { signed: true });
|
|
1522
|
+
const start = clampTick(startTickValue ?? pool.currentTick - DEFAULT_SPAN);
|
|
1523
|
+
const end = clampTick(endTickValue ?? pool.currentTick + DEFAULT_SPAN);
|
|
880
1524
|
const span = Number(end - start);
|
|
881
1525
|
const atTicks = [];
|
|
882
1526
|
for (let i = 0; i < n; i++) atTicks.push(clampTick(start + BigInt(Math.round(i * span / (n - 1)))));
|
|
@@ -928,23 +1572,12 @@ function registerPreviewPosition(server, ctx) {
|
|
|
928
1572
|
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
929
1573
|
}
|
|
930
1574
|
}, async ({ address, tokenId, positionSize, tickLimitLow, tickLimitHigh, swapAtMint, usePremiaAsCollateral, fromBlock }) => {
|
|
931
|
-
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
let size;
|
|
935
|
-
try {
|
|
936
|
-
id = BigInt(tokenId.trim());
|
|
937
|
-
} catch {
|
|
938
|
-
throw new Error(`Invalid tokenId (expected a 0x-hex or decimal string): ${tokenId}`);
|
|
939
|
-
}
|
|
940
|
-
try {
|
|
941
|
-
size = BigInt(positionSize);
|
|
942
|
-
} catch {
|
|
943
|
-
throw new Error(`Invalid positionSize (expected a decimal string): ${positionSize}`);
|
|
944
|
-
}
|
|
1575
|
+
const account = parseAccountAddress(address);
|
|
1576
|
+
const id = parseTokenId(tokenId);
|
|
1577
|
+
const size = parseDecimalBigInt(positionSize, "positionSize");
|
|
945
1578
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
946
1579
|
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
947
|
-
const { tokenIds } = await syncAccountPositions(ctx, account, fromBlock
|
|
1580
|
+
const { tokenIds } = await syncAccountPositions(ctx, account, parseOptionalDecimalBigInt(fromBlock, "fromBlock"));
|
|
948
1581
|
const preview = await getOpenPositionPreview({
|
|
949
1582
|
client: ctx.client,
|
|
950
1583
|
poolAddress,
|
|
@@ -953,8 +1586,8 @@ function registerPreviewPosition(server, ctx) {
|
|
|
953
1586
|
tokenId: id,
|
|
954
1587
|
positionSize: size,
|
|
955
1588
|
queryAddress,
|
|
956
|
-
tickLimitLow: tickLimitLow
|
|
957
|
-
tickLimitHigh: tickLimitHigh
|
|
1589
|
+
tickLimitLow: parseOptionalDecimalBigInt(tickLimitLow, "tickLimitLow", { signed: true }) ?? MIN_TICK,
|
|
1590
|
+
tickLimitHigh: parseOptionalDecimalBigInt(tickLimitHigh, "tickLimitHigh", { signed: true }) ?? MAX_TICK,
|
|
958
1591
|
swapAtMint: swapAtMint ?? false,
|
|
959
1592
|
usePremiaAsCollateral: usePremiaAsCollateral ?? false
|
|
960
1593
|
});
|
|
@@ -982,34 +1615,30 @@ function registerPreviewPosition(server, ctx) {
|
|
|
982
1615
|
|
|
983
1616
|
//#endregion
|
|
984
1617
|
//#region src/tools/suggestDeltaHedge.ts
|
|
985
|
-
/** Align a tick down to the nearest multiple of tickSpacing (matches buildUniqueLoan). */
|
|
986
|
-
function alignStrike(tick, tickSpacing) {
|
|
987
|
-
const mod = (tick % tickSpacing + tickSpacing) % tickSpacing;
|
|
988
|
-
return tick - mod;
|
|
989
|
-
}
|
|
990
1618
|
function registerSuggestDeltaHedge(server, ctx) {
|
|
991
1619
|
server.registerTool("suggest_delta_hedge", {
|
|
992
1620
|
title: "Suggest a delta-neutral hedge",
|
|
993
1621
|
description: "Compute an account's net delta on the configured pool (option positions + deposited collateral) and suggest a Panoptic width=0 loan/credit leg that neutralizes it. A width=0 leg carries pure, spot-like directional delta (delta = ±size, no gamma): a LOAN borrowing the asset adds short delta, a CREDIT lending the asset adds long delta. If the account is net LONG the asset, the tool suggests a loan; if net SHORT, a credit. Returns an inspectable explicit-leg suggestion (tokenId + size) that can be handed to build_position; it does not execute anything.",
|
|
994
1622
|
inputSchema: {
|
|
995
1623
|
address: z.string().describe("The account address."),
|
|
996
|
-
assetIndex: z.union([z.literal(0), z.literal(1)]).optional().describe("
|
|
1624
|
+
assetIndex: z.union([z.literal(0), z.literal(1)]).optional().describe("Reporting frame for the aggregate net delta: 0 = token0 units, 1 = token1 units. This is a denomination choice (it overrides each leg's own asset when aggregating), NOT a canonical pool asset. If omitted and no session preference is set (via configure_session), the tool asks the user which token to use instead of guessing."),
|
|
997
1625
|
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
998
1626
|
}
|
|
999
1627
|
}, async ({ address, assetIndex, fromBlock }) => {
|
|
1000
|
-
|
|
1001
|
-
const account = getAddress(address);
|
|
1628
|
+
const account = parseAccountAddress(address);
|
|
1002
1629
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
1003
1630
|
const chainId = BigInt(ctx.config.chainId);
|
|
1004
|
-
const pool = await
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1631
|
+
const pool = await getCachedPool(ctx);
|
|
1632
|
+
const frameResolution = resolveDeltaFrame(pool, assetIndex, ctx.session.preferredAssetIndex);
|
|
1633
|
+
if (frameResolution.needsUserChoice) return assetPreferenceRequired({
|
|
1634
|
+
token0: pool.collateralTracker0.symbol,
|
|
1635
|
+
token1: pool.collateralTracker1.symbol,
|
|
1636
|
+
suggestedAssetIndex: frameResolution.frame,
|
|
1637
|
+
suggestionReason: frameResolution.reason
|
|
1009
1638
|
});
|
|
1010
|
-
const { asset,
|
|
1639
|
+
const { frame: asset, source: assetFrameSource, reason: assetFrameReason } = frameResolution;
|
|
1011
1640
|
const assetSymbol = asset === 0n ? pool.collateralTracker0.symbol : pool.collateralTracker1.symbol;
|
|
1012
|
-
const { tokenIds, storage } = await syncAccountPositions(ctx, account, fromBlock
|
|
1641
|
+
const { tokenIds, storage } = await syncAccountPositions(ctx, account, parseOptionalDecimalBigInt(fromBlock, "fromBlock"));
|
|
1013
1642
|
const greeks = await getAccountGreeks({
|
|
1014
1643
|
client: ctx.client,
|
|
1015
1644
|
chainId,
|
|
@@ -1025,12 +1654,17 @@ function registerSuggestDeltaHedge(server, ctx) {
|
|
|
1025
1654
|
pool: poolAddress,
|
|
1026
1655
|
assetIndex: asset,
|
|
1027
1656
|
assetSymbol,
|
|
1028
|
-
|
|
1657
|
+
deltaFrame: {
|
|
1658
|
+
assetIndex: asset,
|
|
1659
|
+
symbol: assetSymbol,
|
|
1660
|
+
source: assetFrameSource,
|
|
1661
|
+
reason: assetFrameReason
|
|
1662
|
+
},
|
|
1029
1663
|
openTokenIds: tokenIds,
|
|
1030
1664
|
positionsDelta: greeks.positionsDelta,
|
|
1031
1665
|
collateralDelta: greeks.collateralDelta,
|
|
1032
1666
|
netDelta,
|
|
1033
|
-
netDeltaNote: `Net delta is expressed in ${assetSymbol} smallest units (positive = net long
|
|
1667
|
+
netDeltaNote: `Net delta is expressed in ${assetSymbol} smallest units (positive = net long ${assetSymbol}). ${DELTA_FRAME_NOTE}`
|
|
1034
1668
|
};
|
|
1035
1669
|
if (netDelta === 0n) return jsonResult({
|
|
1036
1670
|
...facts,
|
|
@@ -1039,7 +1673,7 @@ function registerSuggestDeltaHedge(server, ctx) {
|
|
|
1039
1673
|
});
|
|
1040
1674
|
const isLoan = netDelta > 0n;
|
|
1041
1675
|
const size = netDelta > 0n ? netDelta : -netDelta;
|
|
1042
|
-
const strike =
|
|
1676
|
+
const strike = roundToTickSpacing(pool.currentTick, pool.tickSpacing);
|
|
1043
1677
|
const builder = createTokenIdBuilder(pool.poolId);
|
|
1044
1678
|
const legConfig = {
|
|
1045
1679
|
asset,
|
|
@@ -1084,20 +1718,25 @@ function registerTradeHistory(server, ctx) {
|
|
|
1084
1718
|
}, async ({ address, fromBlock, toBlock }) => {
|
|
1085
1719
|
const account = parseAccountAddress(address);
|
|
1086
1720
|
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
1087
|
-
const
|
|
1088
|
-
const
|
|
1089
|
-
const
|
|
1721
|
+
const fromBlockArg = parseOptionalDecimalBigInt(fromBlock, "fromBlock");
|
|
1722
|
+
const toBlockArg = parseOptionalDecimalBigInt(toBlock, "toBlock");
|
|
1723
|
+
const deploymentBlock = fromBlockArg !== void 0 ? void 0 : await getPoolDeploymentBlock(ctx.client, poolAddress);
|
|
1724
|
+
const start = fromBlockArg ?? deploymentBlock ?? 0n;
|
|
1725
|
+
const fromBlockSource = fromBlockArg !== void 0 ? "caller" : deploymentBlock !== void 0 && deploymentBlock !== null ? "pool-deployment-block" : "fallback-zero";
|
|
1090
1726
|
const history = await getAccountHistory({
|
|
1091
1727
|
client: ctx.client,
|
|
1092
1728
|
poolAddress,
|
|
1093
1729
|
account,
|
|
1094
1730
|
fromBlock: start,
|
|
1095
|
-
toBlock:
|
|
1731
|
+
toBlock: toBlockArg
|
|
1096
1732
|
});
|
|
1097
1733
|
return jsonResult({
|
|
1098
1734
|
account,
|
|
1099
1735
|
pool: poolAddress,
|
|
1100
1736
|
fromBlock: start,
|
|
1737
|
+
fromBlockSource,
|
|
1738
|
+
toBlock: toBlockArg,
|
|
1739
|
+
...fromBlockSource === "fallback-zero" ? { scanWarning: "Pool deployment block could not be resolved; scanning from block 0. History may be incomplete or truncated by the RPC provider. Pass fromBlock to bound the scan." } : {},
|
|
1101
1740
|
tradeCount: history.trades.length,
|
|
1102
1741
|
trades: history.trades,
|
|
1103
1742
|
_meta: history._meta
|
|
@@ -1120,6 +1759,7 @@ function createServer(ctx) {
|
|
|
1120
1759
|
name: SERVER_NAME,
|
|
1121
1760
|
version: SERVER_VERSION
|
|
1122
1761
|
});
|
|
1762
|
+
registerConfigureSession(server, ctx);
|
|
1123
1763
|
registerListPools(server, ctx);
|
|
1124
1764
|
registerExplainPosition(server, ctx);
|
|
1125
1765
|
registerIdentifyAddress(server, ctx);
|
|
@@ -1133,6 +1773,10 @@ function createServer(ctx) {
|
|
|
1133
1773
|
registerPositionScenario(server, ctx);
|
|
1134
1774
|
registerHedgeParams(server, ctx);
|
|
1135
1775
|
registerTradeHistory(server, ctx);
|
|
1776
|
+
registerListVaults(server, ctx);
|
|
1777
|
+
registerVaultSummary(server, ctx);
|
|
1778
|
+
registerVaultAccount(server, ctx);
|
|
1779
|
+
registerVaultExposure(server, ctx);
|
|
1136
1780
|
return server;
|
|
1137
1781
|
}
|
|
1138
1782
|
|