@panoptic-eng/mcp 0.1.0
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 +86 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +20 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +3 -0
- package/dist/server--YF2LHqv.js +1140 -0
- package/package.json +54 -0
|
@@ -0,0 +1,1140 @@
|
|
|
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";
|
|
2
|
+
import { createPublicClient, getAddress, http, isAddress, keccak256, toBytes, toHex } from "viem";
|
|
3
|
+
import { base, mainnet, sepolia } from "viem/chains";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
//#region src/config.ts
|
|
9
|
+
/** viem chain definitions (carry the Multicall3 address the SDK reads rely on). */
|
|
10
|
+
const VIEM_CHAINS = {
|
|
11
|
+
[mainnet.id]: mainnet,
|
|
12
|
+
[base.id]: base,
|
|
13
|
+
[sepolia.id]: sepolia
|
|
14
|
+
};
|
|
15
|
+
/** Chain ids Panoptic has deployments for (mainnet, Base, Sepolia). */
|
|
16
|
+
const KNOWN_CHAIN_IDS = [
|
|
17
|
+
mainnet.id,
|
|
18
|
+
base.id,
|
|
19
|
+
sepolia.id
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Resolve an optional user-supplied chain-id string to a trusted known-chain
|
|
23
|
+
* constant, falling back to `fallback` when omitted/blank. Matches on canonical
|
|
24
|
+
* decimal form only (never Number(), so "1e0" is rejected) and throws on
|
|
25
|
+
* unsupported ids.
|
|
26
|
+
*/
|
|
27
|
+
function resolveChainId(rawChainId, fallback) {
|
|
28
|
+
if (rawChainId === void 0 || rawChainId.trim() === "") return fallback;
|
|
29
|
+
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(", ")}.`);
|
|
31
|
+
return chainId;
|
|
32
|
+
}
|
|
33
|
+
var ConfigError = class extends Error {};
|
|
34
|
+
/**
|
|
35
|
+
* Resolve server config from CLI flags (`--rpc-url`, `--chain-id`,
|
|
36
|
+
* `--subgraph-url`) layered over env (`PANOPTIC_RPC_URL`, `PANOPTIC_CHAIN_ID`,
|
|
37
|
+
* `PANOPTIC_SUBGRAPH_URL`). CLI flags win. Throws a readable error when RPC URL
|
|
38
|
+
* or chain id is missing or unsupported.
|
|
39
|
+
*/
|
|
40
|
+
function resolveConfig(argv, env) {
|
|
41
|
+
const flags = parseFlags(argv);
|
|
42
|
+
const rpcUrl = flags["rpc-url"] ?? env.PANOPTIC_RPC_URL;
|
|
43
|
+
if (!rpcUrl) throw new ConfigError("Missing RPC URL. Set PANOPTIC_RPC_URL or pass --rpc-url <url>.");
|
|
44
|
+
const rawChainId = flags["chain-id"] ?? env.PANOPTIC_CHAIN_ID;
|
|
45
|
+
if (!rawChainId) throw new ConfigError("Missing chain id. Set PANOPTIC_CHAIN_ID or pass --chain-id <id>.");
|
|
46
|
+
const chainId = KNOWN_CHAIN_IDS.find((id) => id.toString() === rawChainId.trim());
|
|
47
|
+
if (chainId === void 0 || getChainDeployment(chainId) === void 0) throw new ConfigError(`Unsupported chain id ${rawChainId}. Supported: ${KNOWN_CHAIN_IDS.join(", ")}.`);
|
|
48
|
+
const subgraphUrl = flags["subgraph-url"] ?? env.PANOPTIC_SUBGRAPH_URL ?? void 0;
|
|
49
|
+
return {
|
|
50
|
+
rpcUrl,
|
|
51
|
+
chainId,
|
|
52
|
+
subgraphUrl
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Create the shared context (viem client + resolved deployment) for tools. */
|
|
56
|
+
async function createContext(config) {
|
|
57
|
+
const deployment = getChainDeployment(config.chainId);
|
|
58
|
+
if (deployment === void 0) throw new ConfigError(`Unsupported chain id ${config.chainId}`);
|
|
59
|
+
const client = createPublicClient({
|
|
60
|
+
chain: VIEM_CHAINS[config.chainId],
|
|
61
|
+
transport: http(config.rpcUrl)
|
|
62
|
+
});
|
|
63
|
+
const rpcChainId = await client.getChainId();
|
|
64
|
+
if (rpcChainId !== config.chainId) throw new ConfigError(`RPC chain id ${rpcChainId} does not match configured chain id ${config.chainId}. Point PANOPTIC_RPC_URL at a chain-${config.chainId} endpoint or set PANOPTIC_CHAIN_ID to ${rpcChainId}.`);
|
|
65
|
+
return {
|
|
66
|
+
config,
|
|
67
|
+
client,
|
|
68
|
+
deployment
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseFlags(argv) {
|
|
72
|
+
const flags = {};
|
|
73
|
+
for (let i = 0; i < argv.length; i++) {
|
|
74
|
+
const arg = argv[i];
|
|
75
|
+
if (!arg.startsWith("--")) continue;
|
|
76
|
+
const key = arg.slice(2);
|
|
77
|
+
const eq = key.indexOf("=");
|
|
78
|
+
if (eq !== -1) flags[key.slice(0, eq)] = key.slice(eq + 1);
|
|
79
|
+
else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) flags[key] = argv[++i];
|
|
80
|
+
else flags[key] = "true";
|
|
81
|
+
}
|
|
82
|
+
return flags;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/address.ts
|
|
87
|
+
/**
|
|
88
|
+
* Validate and checksum-normalize a user-supplied account address, throwing the
|
|
89
|
+
* shared "Invalid address" error on malformed input. Centralizes the identical
|
|
90
|
+
* isAddress/getAddress dance every account-scoped tool performs.
|
|
91
|
+
*/
|
|
92
|
+
function parseAccountAddress(address) {
|
|
93
|
+
if (!isAddress(address)) throw new Error(`Invalid address: ${address}`);
|
|
94
|
+
return getAddress(address);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/serialize.ts
|
|
99
|
+
/**
|
|
100
|
+
* Recursively convert a value into a JSON-safe form. The only Panoptic-specific
|
|
101
|
+
* invariant here: **bigints become decimal strings** (never JS numbers), so
|
|
102
|
+
* on-chain quantities survive serialization without precision loss. Functions
|
|
103
|
+
* are dropped; everything else passes through. `_meta` blocks are ordinary
|
|
104
|
+
* nested objects and are handled by the recursion.
|
|
105
|
+
*/
|
|
106
|
+
function toJsonSafe(value) {
|
|
107
|
+
if (typeof value === "bigint") return value.toString();
|
|
108
|
+
if (typeof value === "function") return void 0;
|
|
109
|
+
if (Array.isArray(value)) return value.map((v) => {
|
|
110
|
+
const serialized = toJsonSafe(v);
|
|
111
|
+
return serialized === void 0 ? null : serialized;
|
|
112
|
+
});
|
|
113
|
+
if (value !== null && typeof value === "object") {
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const [k, v] of Object.entries(value)) {
|
|
116
|
+
const serialized = toJsonSafe(v);
|
|
117
|
+
if (serialized !== void 0) out[k] = serialized;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
/** MCP tool result wrapping a serialized value as pretty JSON text. */
|
|
124
|
+
function jsonResult(value) {
|
|
125
|
+
const text = JSON.stringify(toJsonSafe(value), null, 2) ?? "null";
|
|
126
|
+
return { content: [{
|
|
127
|
+
type: "text",
|
|
128
|
+
text
|
|
129
|
+
}] };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Deterministic JSON with sorted object keys, so an envelope always hashes to
|
|
133
|
+
* the same value regardless of field insertion order.
|
|
134
|
+
*/
|
|
135
|
+
function canonicalStringify(value) {
|
|
136
|
+
if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(",")}]`;
|
|
137
|
+
if (value !== null && typeof value === "object") {
|
|
138
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonicalStringify(v)}`);
|
|
139
|
+
return `{${entries.join(",")}}`;
|
|
140
|
+
}
|
|
141
|
+
return JSON.stringify(value);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Build the unsigned transaction envelope every build/write-like tool returns.
|
|
145
|
+
* The server never signs; it hands back this object plus `envelopeHash` so a
|
|
146
|
+
* downstream signer can verify it signs exactly what was simulated and shown.
|
|
147
|
+
*/
|
|
148
|
+
function buildEnvelope(input) {
|
|
149
|
+
const canonical = {
|
|
150
|
+
summary: input.summary,
|
|
151
|
+
risk: input.risk,
|
|
152
|
+
chainId: input.chainId.toString(),
|
|
153
|
+
to: input.to,
|
|
154
|
+
value: input.value.toString(),
|
|
155
|
+
data: input.data,
|
|
156
|
+
simulation: toJsonSafe(input.simulation)
|
|
157
|
+
};
|
|
158
|
+
const envelopeHash = keccak256(toBytes(canonicalStringify(canonical)));
|
|
159
|
+
return {
|
|
160
|
+
...canonical,
|
|
161
|
+
envelopeHash
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/tools/positions.ts
|
|
167
|
+
const STABLE_SYMBOL = /^(usdc|usdt|usdbc|usds|usde|dai|frax|lusd|gusd|tusd|pyusd)$/i;
|
|
168
|
+
/**
|
|
169
|
+
* Resolve which token is the directional "asset" (non-numeraire). An explicit
|
|
170
|
+
* override wins; otherwise the stablecoin side is treated as the numeraire and
|
|
171
|
+
* the other token as the asset (defaulting to token0 if neither looks stable).
|
|
172
|
+
*/
|
|
173
|
+
function inferAssetIndex(pool, override) {
|
|
174
|
+
if (override !== void 0) return {
|
|
175
|
+
asset: override === 0 ? 0n : 1n,
|
|
176
|
+
inferred: false
|
|
177
|
+
};
|
|
178
|
+
if (STABLE_SYMBOL.test(pool.collateralTracker0.symbol)) return {
|
|
179
|
+
asset: 1n,
|
|
180
|
+
inferred: true
|
|
181
|
+
};
|
|
182
|
+
if (STABLE_SYMBOL.test(pool.collateralTracker1.symbol)) return {
|
|
183
|
+
asset: 0n,
|
|
184
|
+
inferred: true
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
asset: 0n,
|
|
188
|
+
inferred: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Discover an account's open position ids on the configured pool via the SDK's
|
|
193
|
+
* event-scan sync into a fresh in-memory store — no subgraph required. Returns
|
|
194
|
+
* the populated storage so greeks reads (which read synced positions from
|
|
195
|
+
* storage) can reuse it.
|
|
196
|
+
*/
|
|
197
|
+
const syncCache = new Map();
|
|
198
|
+
async function syncAccountPositions(ctx, account, fromBlock) {
|
|
199
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
200
|
+
const chainId = BigInt(ctx.config.chainId);
|
|
201
|
+
const cacheKey = `${chainId}:${poolAddress}:${account}:${fromBlock ?? "default"}`;
|
|
202
|
+
const cached = syncCache.get(cacheKey);
|
|
203
|
+
if (cached) return cached;
|
|
204
|
+
const pending = (async () => {
|
|
205
|
+
const storage = createMemoryStorage();
|
|
206
|
+
await syncPositions({
|
|
207
|
+
client: ctx.client,
|
|
208
|
+
chainId,
|
|
209
|
+
poolAddress,
|
|
210
|
+
account,
|
|
211
|
+
storage,
|
|
212
|
+
fromBlock
|
|
213
|
+
});
|
|
214
|
+
const tokenIds = await getTrackedPositionIds({
|
|
215
|
+
chainId,
|
|
216
|
+
poolAddress,
|
|
217
|
+
account,
|
|
218
|
+
storage
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
poolAddress,
|
|
222
|
+
chainId,
|
|
223
|
+
tokenIds,
|
|
224
|
+
storage
|
|
225
|
+
};
|
|
226
|
+
})();
|
|
227
|
+
syncCache.set(cacheKey, pending);
|
|
228
|
+
pending.catch(() => syncCache.delete(cacheKey));
|
|
229
|
+
return pending;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/tools/accountHealth.ts
|
|
234
|
+
function registerAccountHealth(server, ctx) {
|
|
235
|
+
server.registerTool("account_health", {
|
|
236
|
+
title: "Assess Panoptic account health",
|
|
237
|
+
description: "An account's margin status on the configured pool — buying power, margin buffer / distance to liquidation, and whether the account is currently liquidatable. Discovers positions via an on-chain event scan (no subgraph).",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
address: z.string().describe("The account address."),
|
|
240
|
+
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
241
|
+
}
|
|
242
|
+
}, async ({ address, fromBlock }) => {
|
|
243
|
+
const account = parseAccountAddress(address);
|
|
244
|
+
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
245
|
+
const { poolAddress, tokenIds } = await syncAccountPositions(ctx, account, fromBlock ? BigInt(fromBlock) : void 0);
|
|
246
|
+
const [buyingPower, marginBuffer, liquidation] = await Promise.all([
|
|
247
|
+
getAccountBuyingPower({
|
|
248
|
+
client: ctx.client,
|
|
249
|
+
poolAddress,
|
|
250
|
+
account,
|
|
251
|
+
tokenIds
|
|
252
|
+
}),
|
|
253
|
+
getMarginBuffer({
|
|
254
|
+
client: ctx.client,
|
|
255
|
+
poolAddress,
|
|
256
|
+
account,
|
|
257
|
+
tokenIds,
|
|
258
|
+
queryAddress
|
|
259
|
+
}),
|
|
260
|
+
isLiquidatable({
|
|
261
|
+
client: ctx.client,
|
|
262
|
+
poolAddress,
|
|
263
|
+
account,
|
|
264
|
+
tokenIds
|
|
265
|
+
})
|
|
266
|
+
]);
|
|
267
|
+
return jsonResult({
|
|
268
|
+
account,
|
|
269
|
+
pool: poolAddress,
|
|
270
|
+
openTokenIds: tokenIds,
|
|
271
|
+
buyingPower,
|
|
272
|
+
marginBuffer,
|
|
273
|
+
liquidation
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/tools/advisePosition.ts
|
|
280
|
+
const NEAR_LIQUIDATION_TICKS = 500n;
|
|
281
|
+
function analyzeLeg(leg, currentTick) {
|
|
282
|
+
const side = leg.isLong ? "long" : "short";
|
|
283
|
+
if (leg.width === 0n) return {
|
|
284
|
+
index: leg.index,
|
|
285
|
+
kind: isLoanLeg(leg) ? "loan" : isCreditLeg(leg) ? "credit" : "loan",
|
|
286
|
+
side,
|
|
287
|
+
strike: leg.strike,
|
|
288
|
+
width: leg.width,
|
|
289
|
+
tickLower: leg.tickLower,
|
|
290
|
+
tickUpper: leg.tickUpper,
|
|
291
|
+
inRange: null,
|
|
292
|
+
moneyness: null
|
|
293
|
+
};
|
|
294
|
+
const call = isCall(leg.tokenType, leg.asset === 0n);
|
|
295
|
+
const inRange = currentTick >= leg.tickLower && currentTick <= leg.tickUpper;
|
|
296
|
+
let moneyness;
|
|
297
|
+
if (currentTick === leg.strike) moneyness = "ATM";
|
|
298
|
+
else if (call) moneyness = currentTick > leg.strike ? "ITM" : "OTM";
|
|
299
|
+
else moneyness = currentTick < leg.strike ? "ITM" : "OTM";
|
|
300
|
+
return {
|
|
301
|
+
index: leg.index,
|
|
302
|
+
kind: call ? "call" : "put",
|
|
303
|
+
side,
|
|
304
|
+
strike: leg.strike,
|
|
305
|
+
width: leg.width,
|
|
306
|
+
tickLower: leg.tickLower,
|
|
307
|
+
tickUpper: leg.tickUpper,
|
|
308
|
+
inRange,
|
|
309
|
+
moneyness
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function registerAdvisePosition(server, ctx) {
|
|
313
|
+
server.registerTool("advise_position", {
|
|
314
|
+
title: "Advise on managing a position",
|
|
315
|
+
description: "Given a position TokenId and its owner, pull live on-chain state and advise on managing it. Covers: short options that have gone ITM/underwater (roll vs close vs hold), long options (streamia decay, out-of-range/force-exercise exposure), whether the position can be closed right now (simulated, with the decoded reason if not), and collateral/liquidation risk. Always returns the grounded facts; adds a recommendation only when the situation is clear-cut.",
|
|
316
|
+
inputSchema: {
|
|
317
|
+
tokenId: z.string().describe("The position TokenId, as a 0x-hex or decimal string. Pass it verbatim."),
|
|
318
|
+
address: z.string().describe("The position owner address."),
|
|
319
|
+
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
320
|
+
}
|
|
321
|
+
}, async ({ tokenId, address, fromBlock }) => {
|
|
322
|
+
if (!isAddress(address)) throw new Error(`Invalid address: ${address}`);
|
|
323
|
+
const account = getAddress(address);
|
|
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
|
+
}
|
|
330
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
331
|
+
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
332
|
+
const chainId = BigInt(ctx.config.chainId);
|
|
333
|
+
const pool = await getPool({
|
|
334
|
+
client: ctx.client,
|
|
335
|
+
poolAddress,
|
|
336
|
+
chainId,
|
|
337
|
+
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
338
|
+
});
|
|
339
|
+
const decoded = decodeTokenId(id);
|
|
340
|
+
const legs = decoded.legs.map((leg) => analyzeLeg(leg, pool.currentTick));
|
|
341
|
+
const { tokenIds } = await syncAccountPositions(ctx, account, fromBlock ? BigInt(fromBlock) : void 0);
|
|
342
|
+
const owned = tokenIds.some((t) => t === id);
|
|
343
|
+
const idList = owned ? tokenIds : [...tokenIds, id];
|
|
344
|
+
const [{ positions }, premia, marginBuffer, liquidation] = await Promise.all([
|
|
345
|
+
getPositions({
|
|
346
|
+
client: ctx.client,
|
|
347
|
+
poolAddress,
|
|
348
|
+
owner: account,
|
|
349
|
+
tokenIds: [id]
|
|
350
|
+
}),
|
|
351
|
+
getAccountPremia({
|
|
352
|
+
client: ctx.client,
|
|
353
|
+
poolAddress,
|
|
354
|
+
account,
|
|
355
|
+
tokenIds: [id]
|
|
356
|
+
}),
|
|
357
|
+
getMarginBuffer({
|
|
358
|
+
client: ctx.client,
|
|
359
|
+
poolAddress,
|
|
360
|
+
account,
|
|
361
|
+
tokenIds: idList,
|
|
362
|
+
queryAddress
|
|
363
|
+
}),
|
|
364
|
+
isLiquidatable({
|
|
365
|
+
client: ctx.client,
|
|
366
|
+
poolAddress,
|
|
367
|
+
account,
|
|
368
|
+
tokenIds: idList
|
|
369
|
+
})
|
|
370
|
+
]);
|
|
371
|
+
const position = positions[0] ?? null;
|
|
372
|
+
let closeable = null;
|
|
373
|
+
let closeBlockedReason = null;
|
|
374
|
+
if (position) {
|
|
375
|
+
const sim = await simulateClosePosition({
|
|
376
|
+
client: ctx.client,
|
|
377
|
+
poolAddress,
|
|
378
|
+
account,
|
|
379
|
+
positionIdList: idList,
|
|
380
|
+
tokenId: id,
|
|
381
|
+
positionSize: position.positionSize,
|
|
382
|
+
tickLimitLow: MIN_TICK,
|
|
383
|
+
tickLimitHigh: MAX_TICK
|
|
384
|
+
});
|
|
385
|
+
closeable = sim.success;
|
|
386
|
+
if (!sim.success && sim.error) closeBlockedReason = {
|
|
387
|
+
name: sim.error.name,
|
|
388
|
+
message: sim.error.message
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const recommendations = [];
|
|
392
|
+
if (!owned) recommendations.push(`Address ${account} does not appear to hold this TokenId (not in its open-position scan). Facts below reflect the decoded TokenId and pool, but position-level data may be empty.`);
|
|
393
|
+
if (liquidation.isLiquidatable) recommendations.push("Account is liquidatable RIGHT NOW — add collateral or reduce position size immediately to avoid liquidation.");
|
|
394
|
+
else if (marginBuffer.liquidationDistance !== null && marginBuffer.liquidationDistance < NEAR_LIQUIDATION_TICKS && marginBuffer.liquidationDistance > -NEAR_LIQUIDATION_TICKS) recommendations.push(`Close to a liquidation boundary (~${marginBuffer.liquidationDistance} ticks away). Consider adding collateral or trimming size before the price moves further.`);
|
|
395
|
+
if (closeable === false) recommendations.push(`This position cannot be closed at the moment${closeBlockedReason ? ` (${closeBlockedReason.name})` : ""}. For a long leg this usually means the in-range liquidity needed to unwind it has been withdrawn — check chunk_spread / liquidity_map and retry when liquidity returns; otherwise the pool may be in a restricted safety mode.`);
|
|
396
|
+
for (const leg of legs) {
|
|
397
|
+
if (leg.kind === "loan" || leg.kind === "credit") continue;
|
|
398
|
+
if (leg.side === "short" && leg.moneyness === "ITM") recommendations.push(`Leg ${leg.index}: your short ${leg.kind} is ITM (price moved through strike ${leg.strike}). You are paying out streamia and carry pin/assignment risk — consider rolling the strike out or closing if you don't want continued directional exposure.`);
|
|
399
|
+
else if (leg.side === "long" && leg.inRange === false) recommendations.push(`Leg ${leg.index}: your long ${leg.kind} is out-of-range/OTM. It is decaying (you pay streamia) and out-of-range longs can be force-exercised by others — consider closing to reclaim capital.`);
|
|
400
|
+
}
|
|
401
|
+
return jsonResult({
|
|
402
|
+
tokenId: id,
|
|
403
|
+
tokenIdHex: toHex(id),
|
|
404
|
+
owner: account,
|
|
405
|
+
owned,
|
|
406
|
+
pool: {
|
|
407
|
+
address: poolAddress,
|
|
408
|
+
currentTick: pool.currentTick,
|
|
409
|
+
tickSpacing: pool.tickSpacing,
|
|
410
|
+
health: pool.healthStatus,
|
|
411
|
+
token0: pool.collateralTracker0.symbol,
|
|
412
|
+
token1: pool.collateralTracker1.symbol
|
|
413
|
+
},
|
|
414
|
+
legs,
|
|
415
|
+
position,
|
|
416
|
+
streamia: {
|
|
417
|
+
shortPremium0: premia.shortPremium0,
|
|
418
|
+
shortPremium1: premia.shortPremium1,
|
|
419
|
+
longPremium0: premia.longPremium0,
|
|
420
|
+
longPremium1: premia.longPremium1,
|
|
421
|
+
note: "shortPremium = streamia earned by short legs; longPremium = streamia paid by long legs (token0/token1)."
|
|
422
|
+
},
|
|
423
|
+
health: {
|
|
424
|
+
isLiquidatable: liquidation.isLiquidatable,
|
|
425
|
+
liquidationDistanceTicks: marginBuffer.liquidationDistance,
|
|
426
|
+
lowerLiquidationTick: marginBuffer.lowerLiquidationTick,
|
|
427
|
+
upperLiquidationTick: marginBuffer.upperLiquidationTick,
|
|
428
|
+
buffer0: marginBuffer.buffer0,
|
|
429
|
+
buffer1: marginBuffer.buffer1
|
|
430
|
+
},
|
|
431
|
+
closeable,
|
|
432
|
+
closeBlockedReason,
|
|
433
|
+
recommendations
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/tools/chunkSpread.ts
|
|
440
|
+
const DEFAULT_TIMESCALE = "1D";
|
|
441
|
+
const DEFAULT_SCAN_HALFSPAN = 6000n;
|
|
442
|
+
/** WAD-scaled spread → a plain multiplier rounded to 4 dp (1e18 → 1.0). */
|
|
443
|
+
function toMultiplier(spreadWad) {
|
|
444
|
+
return Number(spreadWad * 10000n / WAD) / 1e4;
|
|
445
|
+
}
|
|
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
|
+
function registerChunkSpread(server, ctx) {
|
|
456
|
+
server.registerTool("chunk_spread", {
|
|
457
|
+
title: "Chunk spread multipliers",
|
|
458
|
+
description: "Per-chunk spread multiplier across a tick range of the configured pool, via the SDK's scanChunks. The spread multiplier is the premium multiplier applied to a chunk: 1.0x means no liquidity has been removed (lowest implied vol) — those chunks are the CHEAPEST to BUY. A higher multiplier means more liquidity is already utilized, so the chunk pays a richer premium — those chunks are the most PROFITABLE to SELL. Reported per token type (0 and 1). Chunk width defaults to a standard timescale width (1H/1D/1W/1M/1Y ≈ ±1%/±4%/±13%/±27%/±112% price range).",
|
|
459
|
+
inputSchema: {
|
|
460
|
+
timescale: z.enum([
|
|
461
|
+
"1H",
|
|
462
|
+
"1D",
|
|
463
|
+
"1W",
|
|
464
|
+
"1M",
|
|
465
|
+
"1Y"
|
|
466
|
+
]).optional().describe(`Standard chunk width by expiry profile (STANDARD_TICK_WIDTHS). Defaults to ${DEFAULT_TIMESCALE}. Ignored if 'width' is given.`),
|
|
467
|
+
width: z.number().int().positive().safe().optional().describe("Explicit chunk width in ticks (rounded down to a multiple of tick spacing). Overrides timescale."),
|
|
468
|
+
tickLower: z.string().optional().describe("Lower tick bound (decimal string). Defaults to a band below the current tick."),
|
|
469
|
+
tickUpper: z.string().optional().describe("Upper tick bound (decimal string). Defaults to a band above the current tick."),
|
|
470
|
+
vegoid: z.number().int().positive().optional().describe(`VEGOID constant used in the spread formula. Defaults to ${DEFAULT_VEGOID}.`)
|
|
471
|
+
}
|
|
472
|
+
}, async ({ timescale, width, tickLower, tickUpper, vegoid }) => {
|
|
473
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
474
|
+
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
475
|
+
const pool = await getPool({
|
|
476
|
+
client: ctx.client,
|
|
477
|
+
poolAddress,
|
|
478
|
+
chainId: BigInt(ctx.config.chainId),
|
|
479
|
+
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
480
|
+
});
|
|
481
|
+
const resolvedTimescale = timescale ?? DEFAULT_TIMESCALE;
|
|
482
|
+
const rawWidth = width !== void 0 ? BigInt(width) : STANDARD_TICK_WIDTHS[resolvedTimescale];
|
|
483
|
+
const chunkWidth = rawWidth < pool.tickSpacing ? pool.tickSpacing : rawWidth - rawWidth % pool.tickSpacing;
|
|
484
|
+
const vg = vegoid !== void 0 ? BigInt(vegoid) : DEFAULT_VEGOID;
|
|
485
|
+
const halfWidthsRaw = DEFAULT_SCAN_HALFSPAN / chunkWidth;
|
|
486
|
+
const halfWidths = halfWidthsRaw < 2n ? 2n : halfWidthsRaw;
|
|
487
|
+
const center = alignDown(pool.currentTick, chunkWidth);
|
|
488
|
+
const lower = tickLower !== void 0 ? alignDown(clampTick$1(BigInt(tickLower)), chunkWidth) : alignDown(clampTick$1(center - halfWidths * chunkWidth), chunkWidth);
|
|
489
|
+
const upper = tickUpper !== void 0 ? alignDown(clampTick$1(BigInt(tickUpper)), chunkWidth) : alignDown(clampTick$1(center + halfWidths * chunkWidth), chunkWidth);
|
|
490
|
+
const { chunks, _meta } = await scanChunks({
|
|
491
|
+
client: ctx.client,
|
|
492
|
+
poolAddress,
|
|
493
|
+
queryAddress,
|
|
494
|
+
tickLower: lower,
|
|
495
|
+
tickUpper: upper,
|
|
496
|
+
width: chunkWidth,
|
|
497
|
+
vegoid: vg
|
|
498
|
+
});
|
|
499
|
+
const rows = chunks.map((c) => ({
|
|
500
|
+
strike: c.strike,
|
|
501
|
+
distanceTicksFromCurrent: c.strike - pool.currentTick,
|
|
502
|
+
spreadMultiplier0: toMultiplier(c.spreadWad0),
|
|
503
|
+
spreadMultiplier1: toMultiplier(c.spreadWad1),
|
|
504
|
+
spreadWad0: c.spreadWad0,
|
|
505
|
+
spreadWad1: c.spreadWad1,
|
|
506
|
+
netLiquidity0: c.netLiquidity0,
|
|
507
|
+
removedLiquidity0: c.removedLiquidity0,
|
|
508
|
+
netLiquidity1: c.netLiquidity1,
|
|
509
|
+
removedLiquidity1: c.removedLiquidity1
|
|
510
|
+
}));
|
|
511
|
+
const peakMultiplier = (r) => Math.max(r.spreadMultiplier0, r.spreadMultiplier1);
|
|
512
|
+
const bestForSelling = [...rows].filter((r) => peakMultiplier(r) > 1).sort((a, b) => peakMultiplier(b) - peakMultiplier(a)).slice(0, 3).map((r) => ({
|
|
513
|
+
strike: r.strike,
|
|
514
|
+
peakMultiplier: peakMultiplier(r)
|
|
515
|
+
}));
|
|
516
|
+
const bestForBuying = [...rows].sort((a, b) => peakMultiplier(a) - peakMultiplier(b)).slice(0, 3).map((r) => ({
|
|
517
|
+
strike: r.strike,
|
|
518
|
+
peakMultiplier: peakMultiplier(r)
|
|
519
|
+
}));
|
|
520
|
+
return jsonResult({
|
|
521
|
+
pool: poolAddress,
|
|
522
|
+
currentTick: pool.currentTick,
|
|
523
|
+
tickSpacing: pool.tickSpacing,
|
|
524
|
+
timescale: width !== void 0 ? "custom" : resolvedTimescale,
|
|
525
|
+
width: chunkWidth,
|
|
526
|
+
vegoid: vg,
|
|
527
|
+
tickRange: {
|
|
528
|
+
lower,
|
|
529
|
+
upper
|
|
530
|
+
},
|
|
531
|
+
interpretation: "spreadMultiplier is the premium multiplier for the chunk. 1.0x = no liquidity removed (lowest IV) → cheapest to BUY. Higher = more liquidity utilized → richer premium → most profitable to SELL. token0/token1 suffixes are the two token types (put vs call side).",
|
|
532
|
+
recommendation: {
|
|
533
|
+
sellHere: bestForSelling.length ? bestForSelling : "No chunk currently carries a spread multiplier above 1.0x (no removed liquidity) — selling earns only the base premium everywhere in range.",
|
|
534
|
+
buyHere: bestForBuying
|
|
535
|
+
},
|
|
536
|
+
chunkCount: rows.length,
|
|
537
|
+
chunks: rows,
|
|
538
|
+
_meta
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
//#endregion
|
|
544
|
+
//#region src/tools/explainPosition.ts
|
|
545
|
+
/** A coarse, human-readable label for the shape of a decoded position. */
|
|
546
|
+
function classifyStrategy(tokenId, legCount) {
|
|
547
|
+
const parts = [`${legCount}-leg`];
|
|
548
|
+
if (isShortOnly(tokenId)) parts.push("short-only");
|
|
549
|
+
else if (hasLongLeg(tokenId)) parts.push("has-long-leg");
|
|
550
|
+
if (isSpread(tokenId)) parts.push("spread");
|
|
551
|
+
if (hasLoanOrCredit(tokenId)) parts.push("loan/credit");
|
|
552
|
+
return parts.join(" ");
|
|
553
|
+
}
|
|
554
|
+
function registerExplainPosition(server, _ctx) {
|
|
555
|
+
server.registerTool("explain_position", {
|
|
556
|
+
title: "Explain a Panoptic position",
|
|
557
|
+
description: "Decode a Panoptic TokenId into human-readable legs (asset, call/put, long/short, strike, width, ratio, risk partner) and a coarse strategy label. Pure decode — no RPC.",
|
|
558
|
+
inputSchema: { tokenId: z.string().describe("The position TokenId, as either a 0x-prefixed hex string (as seen on explorers / in calldata) or a decimal string. Pass it verbatim — do not convert.") }
|
|
559
|
+
}, async ({ tokenId }) => {
|
|
560
|
+
let id;
|
|
561
|
+
try {
|
|
562
|
+
id = BigInt(tokenId.trim());
|
|
563
|
+
} catch {
|
|
564
|
+
throw new Error(`Invalid tokenId (expected a 0x-hex or decimal string): ${tokenId}`);
|
|
565
|
+
}
|
|
566
|
+
if (id < 0n) throw new Error(`Invalid tokenId (must be non-negative): ${tokenId}`);
|
|
567
|
+
const decoded = decodeTokenId(id);
|
|
568
|
+
const strategy = classifyStrategy(id, decoded.legs.length);
|
|
569
|
+
return jsonResult({
|
|
570
|
+
tokenId: id,
|
|
571
|
+
tokenIdHex: toHex(id),
|
|
572
|
+
strategy,
|
|
573
|
+
decoded
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/tools/getPortfolio.ts
|
|
580
|
+
function registerGetPortfolio(server, ctx) {
|
|
581
|
+
server.registerTool("get_portfolio", {
|
|
582
|
+
title: "Get a Panoptic portfolio",
|
|
583
|
+
description: "An account's open positions on the configured pool — decoded legs, sizes, collateral balances, and net account greeks (delta/gamma/theta/vega). Discovers positions via an on-chain event scan (no subgraph).",
|
|
584
|
+
inputSchema: {
|
|
585
|
+
address: z.string().describe("The account address."),
|
|
586
|
+
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan. Defaults to the pool deployment block.")
|
|
587
|
+
}
|
|
588
|
+
}, async ({ address, fromBlock }) => {
|
|
589
|
+
const account = parseAccountAddress(address);
|
|
590
|
+
const { poolAddress, chainId, tokenIds, storage } = await syncAccountPositions(ctx, account, fromBlock ? BigInt(fromBlock) : void 0);
|
|
591
|
+
const [positions, collateral, greeks] = await Promise.all([
|
|
592
|
+
getPositions({
|
|
593
|
+
client: ctx.client,
|
|
594
|
+
poolAddress,
|
|
595
|
+
owner: account,
|
|
596
|
+
tokenIds
|
|
597
|
+
}),
|
|
598
|
+
getAccountCollateral({
|
|
599
|
+
client: ctx.client,
|
|
600
|
+
poolAddress,
|
|
601
|
+
account
|
|
602
|
+
}),
|
|
603
|
+
getAccountGreeks({
|
|
604
|
+
client: ctx.client,
|
|
605
|
+
chainId,
|
|
606
|
+
poolAddress,
|
|
607
|
+
account,
|
|
608
|
+
storage
|
|
609
|
+
})
|
|
610
|
+
]);
|
|
611
|
+
return jsonResult({
|
|
612
|
+
account,
|
|
613
|
+
pool: poolAddress,
|
|
614
|
+
openTokenIds: tokenIds,
|
|
615
|
+
positions,
|
|
616
|
+
collateral,
|
|
617
|
+
greeks
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region src/tools/hedgeParams.ts
|
|
624
|
+
function registerHedgeParams(server, ctx) {
|
|
625
|
+
server.registerTool("hedge_params", {
|
|
626
|
+
title: "Delta-hedge parameters",
|
|
627
|
+
description: "Compute the SDK's delta-hedge parameters to reach a target delta (default delta-neutral). Two modes: whole-portfolio (pass `address` — current delta is read via getAccountGreeks over positions + deposited collateral) or single-position (pass `tokenId` + `positionSize`). Returns the hedge leg config, hedge amount, hedge type (loan), and whether to swap at mint. Read-only; it does not build or send anything.",
|
|
628
|
+
inputSchema: {
|
|
629
|
+
address: z.string().optional().describe("Account address for whole-portfolio hedging. Provide this OR tokenId."),
|
|
630
|
+
tokenId: z.string().optional().describe("TokenId (0x-hex or decimal) for single-position hedging. Requires positionSize."),
|
|
631
|
+
positionSize: z.string().optional().describe("Position size (decimal string). Required with tokenId."),
|
|
632
|
+
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("Directional asset (0/1) for whole-portfolio mode. Defaults to inferring the non-stablecoin token."),
|
|
634
|
+
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
635
|
+
}
|
|
636
|
+
}, async ({ address, tokenId, positionSize, targetDelta, assetIndex, fromBlock }) => {
|
|
637
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
638
|
+
const chainId = BigInt(ctx.config.chainId);
|
|
639
|
+
let target;
|
|
640
|
+
try {
|
|
641
|
+
target = targetDelta !== void 0 ? BigInt(targetDelta) : 0n;
|
|
642
|
+
} catch {
|
|
643
|
+
throw new Error(`Invalid targetDelta (expected a decimal string): ${targetDelta}`);
|
|
644
|
+
}
|
|
645
|
+
if (tokenId !== void 0) {
|
|
646
|
+
if (positionSize === void 0) throw new Error("positionSize is required when hedging a single tokenId.");
|
|
647
|
+
let id;
|
|
648
|
+
let size;
|
|
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
|
+
}
|
|
659
|
+
const hedge$1 = await getDeltaHedgeParams({
|
|
660
|
+
client: ctx.client,
|
|
661
|
+
poolAddress,
|
|
662
|
+
chainId,
|
|
663
|
+
tokenId: id,
|
|
664
|
+
positionSize: size,
|
|
665
|
+
targetDelta: target
|
|
666
|
+
});
|
|
667
|
+
return jsonResult({
|
|
668
|
+
mode: "single",
|
|
669
|
+
tokenId: id,
|
|
670
|
+
positionSize: size,
|
|
671
|
+
targetDelta: target,
|
|
672
|
+
hedge: hedge$1
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (address === void 0) throw new Error("Provide either `address` (whole-portfolio) or `tokenId` + `positionSize` (single).");
|
|
676
|
+
if (!isAddress(address)) throw new Error(`Invalid address: ${address}`);
|
|
677
|
+
const account = getAddress(address);
|
|
678
|
+
const pool = await getPool({
|
|
679
|
+
client: ctx.client,
|
|
680
|
+
poolAddress,
|
|
681
|
+
chainId,
|
|
682
|
+
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
683
|
+
});
|
|
684
|
+
const { asset, inferred: assetInferred } = inferAssetIndex(pool, assetIndex);
|
|
685
|
+
let scanFromBlock;
|
|
686
|
+
try {
|
|
687
|
+
scanFromBlock = fromBlock ? BigInt(fromBlock) : void 0;
|
|
688
|
+
} catch {
|
|
689
|
+
throw new Error(`Invalid fromBlock (expected a decimal string): ${fromBlock}`);
|
|
690
|
+
}
|
|
691
|
+
const { tokenIds, storage } = await syncAccountPositions(ctx, account, scanFromBlock);
|
|
692
|
+
const greeks = await getAccountGreeks({
|
|
693
|
+
client: ctx.client,
|
|
694
|
+
chainId,
|
|
695
|
+
poolAddress,
|
|
696
|
+
account,
|
|
697
|
+
storage,
|
|
698
|
+
includeCollateral: true,
|
|
699
|
+
assetIndex: asset
|
|
700
|
+
});
|
|
701
|
+
const hedge = await getDeltaHedgeParams({
|
|
702
|
+
client: ctx.client,
|
|
703
|
+
poolAddress,
|
|
704
|
+
chainId,
|
|
705
|
+
currentDelta: greeks.totalDelta,
|
|
706
|
+
asset,
|
|
707
|
+
targetDelta: target
|
|
708
|
+
});
|
|
709
|
+
return jsonResult({
|
|
710
|
+
mode: "portfolio",
|
|
711
|
+
account,
|
|
712
|
+
pool: poolAddress,
|
|
713
|
+
assetIndex: asset,
|
|
714
|
+
assetSymbol: asset === 0n ? pool.collateralTracker0.symbol : pool.collateralTracker1.symbol,
|
|
715
|
+
assetInferred,
|
|
716
|
+
openTokenIds: tokenIds,
|
|
717
|
+
currentDelta: greeks.totalDelta,
|
|
718
|
+
targetDelta: target,
|
|
719
|
+
hedge
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/tools/identifyAddress.ts
|
|
726
|
+
function registerIdentifyAddress(server, ctx) {
|
|
727
|
+
server.registerTool("identify_address", {
|
|
728
|
+
title: "Identify a Panoptic address",
|
|
729
|
+
description: "Map an address to its role in a Panoptic deployment (pool, collateral tracker, factory, query, SFPM, risk engine, vault, manager, …) using the deployment registry. Returns \"unknown\" if unmatched.",
|
|
730
|
+
inputSchema: {
|
|
731
|
+
address: z.string().describe("The address to identify."),
|
|
732
|
+
chainId: z.string().optional().describe("Chain id (decimal string) to look up against. Defaults to the configured chain.")
|
|
733
|
+
}
|
|
734
|
+
}, async ({ address, chainId }) => {
|
|
735
|
+
const needle = parseAccountAddress(address);
|
|
736
|
+
const target = resolveChainId(chainId, ctx.config.chainId);
|
|
737
|
+
const dep = getChainDeployment(target);
|
|
738
|
+
if (dep === void 0) throw new Error(`Unsupported chain id ${target}`);
|
|
739
|
+
const candidates = {
|
|
740
|
+
panopticPool: dep.panoptic.pool.panopticPool,
|
|
741
|
+
collateralTracker0: dep.panoptic.pool.collateralTracker0,
|
|
742
|
+
collateralTracker1: dep.panoptic.pool.collateralTracker1,
|
|
743
|
+
...dep.panoptic.v2,
|
|
744
|
+
uniswapV3Factory: dep.infrastructure.uniswapV3Factory,
|
|
745
|
+
poolManager: dep.infrastructure.poolManager,
|
|
746
|
+
stateView: dep.infrastructure.stateView
|
|
747
|
+
};
|
|
748
|
+
const roles = Object.entries(candidates).filter(([, addr]) => isAddress(addr) && getAddress(addr) === needle).map(([role]) => role);
|
|
749
|
+
return jsonResult({
|
|
750
|
+
address: needle,
|
|
751
|
+
chainId: target,
|
|
752
|
+
roles: roles.length > 0 ? roles : ["unknown"]
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
//#endregion
|
|
758
|
+
//#region src/tools/liquidityMap.ts
|
|
759
|
+
const DEFAULT_N_TICKS = 50n;
|
|
760
|
+
function registerLiquidityMap(server, ctx) {
|
|
761
|
+
server.registerTool("liquidity_map", {
|
|
762
|
+
title: "Map pool liquidity",
|
|
763
|
+
description: "Tick liquidity distribution around a range of the configured pool. Defaults to a symmetric band centered on the current tick; override with startTick / nTicks.",
|
|
764
|
+
inputSchema: {
|
|
765
|
+
startTick: z.string().optional().describe("Starting tick (decimal string). Defaults to a band below the current tick."),
|
|
766
|
+
nTicks: z.string().optional().describe(`Number of ticks to query (decimal string). Defaults to ${DEFAULT_N_TICKS}.`)
|
|
767
|
+
}
|
|
768
|
+
}, async ({ startTick, nTicks }) => {
|
|
769
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
770
|
+
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
771
|
+
const chainId = BigInt(ctx.config.chainId);
|
|
772
|
+
const count = nTicks !== void 0 && nTicks.trim() !== "" ? BigInt(nTicks) : DEFAULT_N_TICKS;
|
|
773
|
+
const pool = await getPool({
|
|
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;
|
|
780
|
+
const liquidities = await getPoolLiquidities({
|
|
781
|
+
client: ctx.client,
|
|
782
|
+
poolAddress,
|
|
783
|
+
queryAddress,
|
|
784
|
+
startTick: start,
|
|
785
|
+
nTicks: count
|
|
786
|
+
});
|
|
787
|
+
return jsonResult({
|
|
788
|
+
pool: poolAddress,
|
|
789
|
+
currentTick: pool.currentTick,
|
|
790
|
+
tickSpacing: pool.tickSpacing,
|
|
791
|
+
startTick: start,
|
|
792
|
+
nTicks: count,
|
|
793
|
+
liquidities
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/tools/listPools.ts
|
|
800
|
+
function registerListPools(server, ctx) {
|
|
801
|
+
server.registerTool("list_pools", {
|
|
802
|
+
title: "List Panoptic pools",
|
|
803
|
+
description: "The known Panoptic pool deployment for a chain (addresses from the deployment registry), hydrated with live on-chain state — tokens, current tick, collateral trackers, health — for the chain this server is configured against.",
|
|
804
|
+
inputSchema: { chainId: z.string().optional().describe("Chain id (decimal string) to look up. Defaults to the configured chain.") }
|
|
805
|
+
}, async ({ chainId }) => {
|
|
806
|
+
const target = resolveChainId(chainId, ctx.config.chainId);
|
|
807
|
+
const dep = getChainDeployment(target);
|
|
808
|
+
if (dep === void 0) throw new Error(`Unsupported chain id ${target}`);
|
|
809
|
+
const registry = {
|
|
810
|
+
chainId: target,
|
|
811
|
+
panopticPool: dep.panoptic.pool.panopticPool,
|
|
812
|
+
collateralTracker0: dep.panoptic.pool.collateralTracker0,
|
|
813
|
+
collateralTracker1: dep.panoptic.pool.collateralTracker1,
|
|
814
|
+
panopticQuery: dep.panoptic.v2.panopticQuery
|
|
815
|
+
};
|
|
816
|
+
if (target !== ctx.config.chainId) return jsonResult({
|
|
817
|
+
...registry,
|
|
818
|
+
note: `Live state unavailable: this server is configured for chain ${ctx.config.chainId}. Restart with --chain-id ${target} (and a matching RPC) to hydrate live state.`
|
|
819
|
+
});
|
|
820
|
+
const pool = await getPool({
|
|
821
|
+
client: ctx.client,
|
|
822
|
+
poolAddress: dep.panoptic.pool.panopticPool,
|
|
823
|
+
chainId: BigInt(target),
|
|
824
|
+
stateViewAddress: dep.infrastructure.stateView
|
|
825
|
+
});
|
|
826
|
+
return jsonResult({
|
|
827
|
+
...registry,
|
|
828
|
+
pool
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/tools/positionScenario.ts
|
|
835
|
+
const DEFAULT_SPAN = 2500n;
|
|
836
|
+
const DEFAULT_POINTS = 25;
|
|
837
|
+
function clampTick(tick) {
|
|
838
|
+
if (tick < MIN_TICK) return MIN_TICK;
|
|
839
|
+
if (tick > MAX_TICK) return MAX_TICK;
|
|
840
|
+
return tick;
|
|
841
|
+
}
|
|
842
|
+
/** Human token1-per-token0 price at a tick (display float; uses token decimals). */
|
|
843
|
+
function priceAtTick(tick, decimals0, decimals1) {
|
|
844
|
+
return Math.pow(1.0001, Number(tick)) * 10 ** (Number(decimals0) - Number(decimals1));
|
|
845
|
+
}
|
|
846
|
+
function registerPositionScenario(server, ctx) {
|
|
847
|
+
server.registerTool("position_scenario", {
|
|
848
|
+
title: "Position payoff / NLV curve",
|
|
849
|
+
description: "An account's net liquidation value (NLV) across a band of hypothetical prices — the payoff curve that answers \"what is my P&L if price moves to X?\". Evaluates the on-chain NLV at each tick in a band (default ±2500 ticks ≈ ±28% around the current tick). NLV is returned in both token0 and token1 smallest units per point.",
|
|
850
|
+
inputSchema: {
|
|
851
|
+
address: z.string().describe("The account address."),
|
|
852
|
+
startTick: z.string().optional().describe("Band start tick (decimal string). Defaults to currentTick − 2500."),
|
|
853
|
+
endTick: z.string().optional().describe("Band end tick (decimal string). Defaults to currentTick + 2500."),
|
|
854
|
+
points: z.number().int().min(2).max(100).optional().describe(`Number of evenly spaced sample ticks. Defaults to ${DEFAULT_POINTS}.`),
|
|
855
|
+
includePendingPremium: z.boolean().optional().describe("Defaults to true."),
|
|
856
|
+
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
857
|
+
}
|
|
858
|
+
}, async ({ address, startTick, endTick, points, includePendingPremium, fromBlock }) => {
|
|
859
|
+
if (!isAddress(address)) throw new Error(`Invalid address: ${address}`);
|
|
860
|
+
const account = getAddress(address);
|
|
861
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
862
|
+
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
863
|
+
const pool = await getPool({
|
|
864
|
+
client: ctx.client,
|
|
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);
|
|
870
|
+
if (tokenIds.length === 0) return jsonResult({
|
|
871
|
+
account,
|
|
872
|
+
pool: poolAddress,
|
|
873
|
+
currentTick: pool.currentTick,
|
|
874
|
+
curve: [],
|
|
875
|
+
note: "Account has no open positions on this pool; nothing to evaluate."
|
|
876
|
+
});
|
|
877
|
+
const n = points ?? DEFAULT_POINTS;
|
|
878
|
+
const start = clampTick(startTick !== void 0 ? BigInt(startTick) : pool.currentTick - DEFAULT_SPAN);
|
|
879
|
+
const end = clampTick(endTick !== void 0 ? BigInt(endTick) : pool.currentTick + DEFAULT_SPAN);
|
|
880
|
+
const span = Number(end - start);
|
|
881
|
+
const atTicks = [];
|
|
882
|
+
for (let i = 0; i < n; i++) atTicks.push(clampTick(start + BigInt(Math.round(i * span / (n - 1)))));
|
|
883
|
+
const nlv = await getNetLiquidationValues({
|
|
884
|
+
client: ctx.client,
|
|
885
|
+
poolAddress,
|
|
886
|
+
account,
|
|
887
|
+
tokenIds,
|
|
888
|
+
atTicks,
|
|
889
|
+
includePendingPremium: includePendingPremium ?? true,
|
|
890
|
+
queryAddress
|
|
891
|
+
});
|
|
892
|
+
const dec0 = pool.collateralTracker0.decimals;
|
|
893
|
+
const dec1 = pool.collateralTracker1.decimals;
|
|
894
|
+
const curve = atTicks.map((tick, i) => ({
|
|
895
|
+
tick,
|
|
896
|
+
priceToken1PerToken0: priceAtTick(tick, dec0, dec1),
|
|
897
|
+
value0: nlv.values0[i],
|
|
898
|
+
value1: nlv.values1[i]
|
|
899
|
+
}));
|
|
900
|
+
return jsonResult({
|
|
901
|
+
account,
|
|
902
|
+
pool: poolAddress,
|
|
903
|
+
currentTick: pool.currentTick,
|
|
904
|
+
token0: pool.collateralTracker0.symbol,
|
|
905
|
+
token1: pool.collateralTracker1.symbol,
|
|
906
|
+
includePendingPremium: includePendingPremium ?? true,
|
|
907
|
+
note: "value0/value1 are net liquidation value at each tick, in token0/token1 smallest units. priceToken1PerToken0 is a display price derived from the tick.",
|
|
908
|
+
curve,
|
|
909
|
+
_meta: nlv._meta
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
//#endregion
|
|
915
|
+
//#region src/tools/previewPosition.ts
|
|
916
|
+
function registerPreviewPosition(server, ctx) {
|
|
917
|
+
server.registerTool("preview_position", {
|
|
918
|
+
title: "Preview opening a position",
|
|
919
|
+
description: "Pre-trade quote for opening a position: read-only dry-run (eth_call, no broadcast) of a mint against the account's current state. Returns collateral required, post-mint collateral balances and requirements, and whether the account stays solvent. Takes a TokenId as input — construct it elsewhere; this tool is tokenId-in.",
|
|
920
|
+
inputSchema: {
|
|
921
|
+
address: z.string().describe("The account address."),
|
|
922
|
+
tokenId: z.string().describe("TokenId of the position to open, as a 0x-hex or decimal string."),
|
|
923
|
+
positionSize: z.string().describe("Position size to open, as a decimal string."),
|
|
924
|
+
tickLimitLow: z.string().optional().describe("Lower tick limit (decimal string). Defaults to MIN_TICK (no bound)."),
|
|
925
|
+
tickLimitHigh: z.string().optional().describe("Upper tick limit (decimal string). Defaults to MAX_TICK (no bound)."),
|
|
926
|
+
swapAtMint: z.boolean().optional().describe("Swap tokens at mint. Defaults to false."),
|
|
927
|
+
usePremiaAsCollateral: z.boolean().optional().describe("Defaults to false."),
|
|
928
|
+
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
929
|
+
}
|
|
930
|
+
}, async ({ address, tokenId, positionSize, tickLimitLow, tickLimitHigh, swapAtMint, usePremiaAsCollateral, fromBlock }) => {
|
|
931
|
+
if (!isAddress(address)) throw new Error(`Invalid address: ${address}`);
|
|
932
|
+
const account = getAddress(address);
|
|
933
|
+
let id;
|
|
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
|
+
}
|
|
945
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
946
|
+
const queryAddress = ctx.deployment.panoptic.v2.panopticQuery;
|
|
947
|
+
const { tokenIds } = await syncAccountPositions(ctx, account, fromBlock ? BigInt(fromBlock) : void 0);
|
|
948
|
+
const preview = await getOpenPositionPreview({
|
|
949
|
+
client: ctx.client,
|
|
950
|
+
poolAddress,
|
|
951
|
+
account,
|
|
952
|
+
existingPositionIds: tokenIds,
|
|
953
|
+
tokenId: id,
|
|
954
|
+
positionSize: size,
|
|
955
|
+
queryAddress,
|
|
956
|
+
tickLimitLow: tickLimitLow !== void 0 ? BigInt(tickLimitLow) : MIN_TICK,
|
|
957
|
+
tickLimitHigh: tickLimitHigh !== void 0 ? BigInt(tickLimitHigh) : MAX_TICK,
|
|
958
|
+
swapAtMint: swapAtMint ?? false,
|
|
959
|
+
usePremiaAsCollateral: usePremiaAsCollateral ?? false
|
|
960
|
+
});
|
|
961
|
+
const revert = preview.simulation.success === false && preview.simulation.error ? {
|
|
962
|
+
name: preview.simulation.error.name,
|
|
963
|
+
message: preview.simulation.error.message
|
|
964
|
+
} : null;
|
|
965
|
+
return jsonResult({
|
|
966
|
+
account,
|
|
967
|
+
pool: poolAddress,
|
|
968
|
+
tokenId: id,
|
|
969
|
+
positionSize: size,
|
|
970
|
+
isSolvent: preview.isSolvent,
|
|
971
|
+
amount0Required: preview.amount0Required,
|
|
972
|
+
amount1Required: preview.amount1Required,
|
|
973
|
+
postCollateral0: preview.postCollateral0,
|
|
974
|
+
postCollateral1: preview.postCollateral1,
|
|
975
|
+
postMintCollateralReqToken0: preview.postMintCollateralReqToken0,
|
|
976
|
+
postMintCollateralReqToken1: preview.postMintCollateralReqToken1,
|
|
977
|
+
currentBuyingPower: preview.currentBuyingPower,
|
|
978
|
+
revert
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
//#endregion
|
|
984
|
+
//#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
|
+
function registerSuggestDeltaHedge(server, ctx) {
|
|
991
|
+
server.registerTool("suggest_delta_hedge", {
|
|
992
|
+
title: "Suggest a delta-neutral hedge",
|
|
993
|
+
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
|
+
inputSchema: {
|
|
995
|
+
address: z.string().describe("The account address."),
|
|
996
|
+
assetIndex: z.union([z.literal(0), z.literal(1)]).optional().describe("Which token is the directional asset (0 or 1). Defaults to inferring the non-stablecoin token from pool symbols."),
|
|
997
|
+
fromBlock: z.string().optional().describe("Optional starting block (decimal string) to bound the position event scan.")
|
|
998
|
+
}
|
|
999
|
+
}, async ({ address, assetIndex, fromBlock }) => {
|
|
1000
|
+
if (!isAddress(address)) throw new Error(`Invalid address: ${address}`);
|
|
1001
|
+
const account = getAddress(address);
|
|
1002
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
1003
|
+
const chainId = BigInt(ctx.config.chainId);
|
|
1004
|
+
const pool = await getPool({
|
|
1005
|
+
client: ctx.client,
|
|
1006
|
+
poolAddress,
|
|
1007
|
+
chainId,
|
|
1008
|
+
stateViewAddress: ctx.deployment.infrastructure.stateView
|
|
1009
|
+
});
|
|
1010
|
+
const { asset, inferred: assetInferred } = inferAssetIndex(pool, assetIndex);
|
|
1011
|
+
const assetSymbol = asset === 0n ? pool.collateralTracker0.symbol : pool.collateralTracker1.symbol;
|
|
1012
|
+
const { tokenIds, storage } = await syncAccountPositions(ctx, account, fromBlock ? BigInt(fromBlock) : void 0);
|
|
1013
|
+
const greeks = await getAccountGreeks({
|
|
1014
|
+
client: ctx.client,
|
|
1015
|
+
chainId,
|
|
1016
|
+
poolAddress,
|
|
1017
|
+
account,
|
|
1018
|
+
storage,
|
|
1019
|
+
includeCollateral: true,
|
|
1020
|
+
assetIndex: asset
|
|
1021
|
+
});
|
|
1022
|
+
const netDelta = greeks.totalDelta;
|
|
1023
|
+
const facts = {
|
|
1024
|
+
account,
|
|
1025
|
+
pool: poolAddress,
|
|
1026
|
+
assetIndex: asset,
|
|
1027
|
+
assetSymbol,
|
|
1028
|
+
assetInferred,
|
|
1029
|
+
openTokenIds: tokenIds,
|
|
1030
|
+
positionsDelta: greeks.positionsDelta,
|
|
1031
|
+
collateralDelta: greeks.collateralDelta,
|
|
1032
|
+
netDelta,
|
|
1033
|
+
netDeltaNote: `Net delta is expressed in ${assetSymbol} smallest units (positive = net long the asset).`
|
|
1034
|
+
};
|
|
1035
|
+
if (netDelta === 0n) return jsonResult({
|
|
1036
|
+
...facts,
|
|
1037
|
+
hedge: null,
|
|
1038
|
+
recommendation: "Account is already delta-neutral on this pool; no hedge needed."
|
|
1039
|
+
});
|
|
1040
|
+
const isLoan = netDelta > 0n;
|
|
1041
|
+
const size = netDelta > 0n ? netDelta : -netDelta;
|
|
1042
|
+
const strike = alignStrike(pool.currentTick, pool.tickSpacing);
|
|
1043
|
+
const builder = createTokenIdBuilder(pool.poolId);
|
|
1044
|
+
const legConfig = {
|
|
1045
|
+
asset,
|
|
1046
|
+
tokenType: asset,
|
|
1047
|
+
strike
|
|
1048
|
+
};
|
|
1049
|
+
const hedgeTokenId = (isLoan ? builder.addLoan(legConfig) : builder.addCredit(legConfig)).build();
|
|
1050
|
+
const hedgeLeg = decodeTokenId(hedgeTokenId).legs[0];
|
|
1051
|
+
const hedgeLegDelta = getLegDelta(hedgeLeg, pool.currentTick, size, pool.tickSpacing, void 0, false, asset);
|
|
1052
|
+
const residualDelta = netDelta + hedgeLegDelta;
|
|
1053
|
+
return jsonResult({
|
|
1054
|
+
...facts,
|
|
1055
|
+
hedge: {
|
|
1056
|
+
action: isLoan ? "open_loan" : "open_credit",
|
|
1057
|
+
rationale: isLoan ? `Account is net long ${size} ${assetSymbol}; a width=0 loan borrowing ${assetSymbol} adds short delta to offset it.` : `Account is net short ${size} ${assetSymbol}; a width=0 credit lending ${assetSymbol} adds long delta to offset it.`,
|
|
1058
|
+
tokenId: hedgeTokenId,
|
|
1059
|
+
tokenIdHex: toHex(hedgeTokenId),
|
|
1060
|
+
size,
|
|
1061
|
+
strike,
|
|
1062
|
+
asset,
|
|
1063
|
+
tokenType: asset,
|
|
1064
|
+
leg: hedgeLeg,
|
|
1065
|
+
expectedHedgeDelta: hedgeLegDelta,
|
|
1066
|
+
residualDelta
|
|
1067
|
+
},
|
|
1068
|
+
recommendation: `${isLoan ? "Open a loan (borrow" : "Open a credit (lend"} ${size} ${assetSymbol}) at strike tick ${strike} to bring net delta from ${netDelta} to ~${residualDelta}. Hand tokenId ${hedgeTokenId} + size to build_position to get an unsigned transaction envelope.`
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
//#endregion
|
|
1074
|
+
//#region src/tools/tradeHistory.ts
|
|
1075
|
+
function registerTradeHistory(server, ctx) {
|
|
1076
|
+
server.registerTool("trade_history", {
|
|
1077
|
+
title: "Account trade history",
|
|
1078
|
+
description: "An account's Panoptic trade history on the configured pool: mints and burns reconstructed from on-chain events, in chronological order (block, then log index), with sizes, ticks, and timestamps. This is event-level history only — it does NOT compute realized P&L or cost basis. Defaults to scanning from the pool deployment block.",
|
|
1079
|
+
inputSchema: {
|
|
1080
|
+
address: z.string().describe("The account address."),
|
|
1081
|
+
fromBlock: z.string().optional().describe("Start block (decimal string). Defaults to the pool deployment block."),
|
|
1082
|
+
toBlock: z.string().optional().describe("End block (decimal string). Defaults to latest.")
|
|
1083
|
+
}
|
|
1084
|
+
}, async ({ address, fromBlock, toBlock }) => {
|
|
1085
|
+
const account = parseAccountAddress(address);
|
|
1086
|
+
const poolAddress = ctx.deployment.panoptic.pool.panopticPool;
|
|
1087
|
+
const hasFrom = fromBlock !== void 0 && fromBlock.trim() !== "";
|
|
1088
|
+
const hasTo = toBlock !== void 0 && toBlock.trim() !== "";
|
|
1089
|
+
const start = hasFrom ? BigInt(fromBlock) : await getPoolDeploymentBlock(ctx.client, poolAddress) ?? 0n;
|
|
1090
|
+
const history = await getAccountHistory({
|
|
1091
|
+
client: ctx.client,
|
|
1092
|
+
poolAddress,
|
|
1093
|
+
account,
|
|
1094
|
+
fromBlock: start,
|
|
1095
|
+
toBlock: hasTo ? BigInt(toBlock) : void 0
|
|
1096
|
+
});
|
|
1097
|
+
return jsonResult({
|
|
1098
|
+
account,
|
|
1099
|
+
pool: poolAddress,
|
|
1100
|
+
fromBlock: start,
|
|
1101
|
+
tradeCount: history.trades.length,
|
|
1102
|
+
trades: history.trades,
|
|
1103
|
+
_meta: history._meta
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
//#endregion
|
|
1109
|
+
//#region src/server.ts
|
|
1110
|
+
const SERVER_NAME = "panoptic-mcp";
|
|
1111
|
+
const require = createRequire(import.meta.url);
|
|
1112
|
+
const SERVER_VERSION = require("../package.json").version;
|
|
1113
|
+
/**
|
|
1114
|
+
* Build the Panoptic MCP server and register the v0 read-only tool catalog.
|
|
1115
|
+
* The transport (stdio locally; HTTP for a future hosted server) is wired by
|
|
1116
|
+
* the caller, so this factory is reusable across surfaces.
|
|
1117
|
+
*/
|
|
1118
|
+
function createServer(ctx) {
|
|
1119
|
+
const server = new McpServer({
|
|
1120
|
+
name: SERVER_NAME,
|
|
1121
|
+
version: SERVER_VERSION
|
|
1122
|
+
});
|
|
1123
|
+
registerListPools(server, ctx);
|
|
1124
|
+
registerExplainPosition(server, ctx);
|
|
1125
|
+
registerIdentifyAddress(server, ctx);
|
|
1126
|
+
registerGetPortfolio(server, ctx);
|
|
1127
|
+
registerAccountHealth(server, ctx);
|
|
1128
|
+
registerLiquidityMap(server, ctx);
|
|
1129
|
+
registerChunkSpread(server, ctx);
|
|
1130
|
+
registerSuggestDeltaHedge(server, ctx);
|
|
1131
|
+
registerAdvisePosition(server, ctx);
|
|
1132
|
+
registerPreviewPosition(server, ctx);
|
|
1133
|
+
registerPositionScenario(server, ctx);
|
|
1134
|
+
registerHedgeParams(server, ctx);
|
|
1135
|
+
registerTradeHistory(server, ctx);
|
|
1136
|
+
return server;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
//#endregion
|
|
1140
|
+
export { KNOWN_CHAIN_IDS, SERVER_NAME, SERVER_VERSION, buildEnvelope, createContext, createServer, jsonResult, resolveConfig, toJsonSafe };
|