@gblin-protocol/mcp-server 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/AGENTS.md +49 -20
  2. package/README.md +111 -11
  3. package/dist/abi.d.ts +27 -0
  4. package/dist/abi.d.ts.map +1 -1
  5. package/dist/abi.js +1 -0
  6. package/dist/abi.js.map +1 -1
  7. package/dist/actions.d.ts +306 -0
  8. package/dist/actions.d.ts.map +1 -0
  9. package/dist/actions.js +749 -0
  10. package/dist/actions.js.map +1 -0
  11. package/dist/auction.d.ts.map +1 -1
  12. package/dist/auction.js +5 -4
  13. package/dist/auction.js.map +1 -1
  14. package/dist/config.d.ts +20 -3
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +23 -6
  17. package/dist/config.js.map +1 -1
  18. package/dist/helpers.d.ts +6 -0
  19. package/dist/helpers.d.ts.map +1 -1
  20. package/dist/helpers.js +37 -13
  21. package/dist/helpers.js.map +1 -1
  22. package/dist/index.d.ts +3 -4
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +38 -19
  25. package/dist/index.js.map +1 -1
  26. package/dist/output-schemas.d.ts +18 -0
  27. package/dist/output-schemas.d.ts.map +1 -0
  28. package/dist/output-schemas.js +243 -0
  29. package/dist/output-schemas.js.map +1 -0
  30. package/dist/payments.d.ts +170 -0
  31. package/dist/payments.d.ts.map +1 -0
  32. package/dist/payments.js +524 -0
  33. package/dist/payments.js.map +1 -0
  34. package/dist/paywall.d.ts +10 -1
  35. package/dist/paywall.d.ts.map +1 -1
  36. package/dist/paywall.js +1 -1
  37. package/dist/paywall.js.map +1 -1
  38. package/dist/prompts.d.ts +33 -0
  39. package/dist/prompts.d.ts.map +1 -0
  40. package/dist/prompts.js +139 -0
  41. package/dist/prompts.js.map +1 -0
  42. package/dist/receipts.d.ts +2 -1
  43. package/dist/receipts.d.ts.map +1 -1
  44. package/dist/receipts.js +16 -12
  45. package/dist/receipts.js.map +1 -1
  46. package/dist/resources.d.ts +17 -0
  47. package/dist/resources.d.ts.map +1 -0
  48. package/dist/resources.js +146 -0
  49. package/dist/resources.js.map +1 -0
  50. package/dist/shared.d.ts +35 -0
  51. package/dist/shared.d.ts.map +1 -0
  52. package/dist/shared.js +68 -0
  53. package/dist/shared.js.map +1 -0
  54. package/dist/tools.d.ts +106 -19
  55. package/dist/tools.d.ts.map +1 -1
  56. package/dist/tools.js +124 -117
  57. package/dist/tools.js.map +1 -1
  58. package/dist/version.d.ts +2 -0
  59. package/dist/version.d.ts.map +1 -0
  60. package/dist/version.js +3 -0
  61. package/dist/version.js.map +1 -0
  62. package/llms.txt +24 -2
  63. package/package.json +8 -3
@@ -0,0 +1,749 @@
1
+ /**
2
+ * GBLIN MCP — prepare, simulate, follow.
3
+ *
4
+ * Four tools that close the loop around a transaction, in the order an agent uses them:
5
+ *
6
+ * prepare_action → the unsigned steps for any operation on the vault
7
+ * preview_steps → the same steps simulated in sequence against the latest block
8
+ * get_transaction_status → what a sent transaction did
9
+ * get_nav_history → NAV per share over time, beside ETH and BTC
10
+ *
11
+ * Nothing here signs or sends. Simulation uses eth_simulateV1; history uses historical eth_call,
12
+ * which the default public RPC refuses, so both fall back to public endpoints that serve them.
13
+ */
14
+ import { createPublicClient, decodeErrorResult, encodeFunctionData, fallback, formatUnits, getAddress, http, isAddress, isHex, parseAbi, parseUnits, toHex, } from "viem";
15
+ import { base } from "viem/chains";
16
+ import { z } from "zod";
17
+ import { CHAINLINK_AGGREGATOR_ABI, ERC20_ABI, GBLIN_ABI, LENS_ABI, ZAP_ABI } from "./abi.js";
18
+ import { getAuctionState } from "./auction.js";
19
+ import { client } from "./client.js";
20
+ import { GBLIN_LENS, GBLIN_VAULT, GBLIN_ZAP, WETH } from "./config.js";
21
+ import { applySlippageBuffer, checkCooldown, getDynamicSlippage } from "./helpers.js";
22
+ import { ZAP_GAS_LIMIT, appendBuilderCode, toolError, toolResult, venueDataPerRow } from "./shared.js";
23
+ // ─── Shared ─────────────────────────────────────────────────────────────────
24
+ /** The first block at which the Lens of the vault in service exists: NAV history starts here. */
25
+ const LENS_DEPLOY_BLOCK = 51563262n;
26
+ /** Base produces a block every two seconds. */
27
+ const BLOCKS_PER_HOUR = 1800n;
28
+ /** Public endpoints that serve historical state and eth_simulateV1 (checked on Base mainnet). */
29
+ // Checked on Base mainnet for historical eth_call. Several, because a hosted server reaches some and not
30
+ // others (mainnet.base.org refuses or rate-limits datacenter ranges, Cloudflare among them), and every failed
31
+ // attempt costs a request, so each endpoint is tried once.
32
+ const HISTORY_RPCS = [
33
+ "https://gateway.tenderly.co/public/base",
34
+ "https://base-mainnet.public.blastapi.io",
35
+ "https://base.drpc.org",
36
+ "https://base-public.nodies.app",
37
+ "https://mainnet.base.org",
38
+ ];
39
+ const historyClient = createPublicClient({
40
+ chain: base,
41
+ transport: fallback([
42
+ ...(process.env.GBLIN_ARCHIVE_RPC_URL ? [http(process.env.GBLIN_ARCHIVE_RPC_URL, { timeout: 15_000 })] : []),
43
+ ...HISTORY_RPCS.map((url) => http(url, { timeout: 15_000, retryCount: 0 })),
44
+ ], { rank: false }),
45
+ });
46
+ /** Custom errors of the vault, the Zap and the adapter, from the verified sources. */
47
+ const PROTOCOL_ERRORS = parseAbi([
48
+ "error AssetAlreadyExists()",
49
+ "error AuthorizationInvalid()",
50
+ "error BadRoute()",
51
+ "error CooldownActive()",
52
+ "error DepositTooSmall()",
53
+ "error EthRefused()",
54
+ "error FillOpen()",
55
+ "error InsufficientGas()",
56
+ "error InvalidAddress()",
57
+ "error InvalidAmount()",
58
+ "error InvalidIndex()",
59
+ "error InvalidParameters()",
60
+ "error InvalidRouter()",
61
+ "error LegNotDelivered(address token)",
62
+ "error ListingDelayActive()",
63
+ "error NoAssetProposed()",
64
+ "error NoAuction()",
65
+ "error NoPool()",
66
+ "error NothingDelivered()",
67
+ "error NothingToClaim()",
68
+ "error ParamOutOfBounds()",
69
+ "error PriceOffTwap(int24 spot, int24 twapTick)",
70
+ "error PriceUnavailable()",
71
+ "error ScaleMismatch()",
72
+ "error SequencerDown()",
73
+ "error SlippageExceeded()",
74
+ "error SwapActive()",
75
+ "error TokenNotConformant()",
76
+ "error Unauthorized()",
77
+ "error WeightOutOfBounds()",
78
+ "error ZeroOutput()",
79
+ ]);
80
+ /** A short explanation for the errors an agent actually meets. */
81
+ const ERROR_HINTS = {
82
+ CooldownActive: "The redemption cooldown after your own mint has not elapsed; analyze_treasury_health reports the seconds left.",
83
+ PriceUnavailable: "An oracle price is older than the vault accepts, or a basket token did not answer; wait and retry.",
84
+ SlippageExceeded: "The output would be below the minimum you set; re-quote and retry.",
85
+ InsufficientGas: "The gas limit is too low for the vault's capped transfers; use the gas the step carries.",
86
+ NoAuction: "No rebalancing auction is open.",
87
+ SequencerDown: "The Base sequencer uptime feed reports an outage or a recent restart.",
88
+ SwapActive: "An auction fill is in progress in this block; retry in the next block.",
89
+ DepositTooSmall: "The amount is below the vault's minimum deposit.",
90
+ ZeroOutput: "The operation would produce nothing.",
91
+ PriceOffTwap: "The pool price is too far from its time-weighted average: the adapter refuses to trade; retry later.",
92
+ };
93
+ const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
94
+ /** eth_simulateV1 reports native ETH movements as Transfer logs from this pseudo-address. */
95
+ const ETH_PSEUDO = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
96
+ const AddressSchema = z
97
+ .string()
98
+ .refine((v) => isAddress(v), { message: "Invalid EVM address" })
99
+ .transform((v) => getAddress(v));
100
+ const AmountSchema = z.string().regex(/^\d+(\.\d+)?$/, "Amount must be a positive decimal string");
101
+ function decodeRevert(data) {
102
+ if (typeof data !== "string" || !isHex(data) || data.length < 10)
103
+ return null;
104
+ try {
105
+ const decoded = decodeErrorResult({ abi: PROTOCOL_ERRORS, data: data });
106
+ const name = decoded.errorName;
107
+ const args = decoded.args?.length ? `(${decoded.args.map(String).join(", ")})` : "";
108
+ return { error: `${name}${args}`, ...(ERROR_HINTS[name] ? { hint: ERROR_HINTS[name] } : {}) };
109
+ }
110
+ catch {
111
+ return { error: `unrecognized revert data ${data.slice(0, 10)}` };
112
+ }
113
+ }
114
+ const tokenMeta = new Map();
115
+ async function describeToken(address) {
116
+ const key = address.toLowerCase();
117
+ if (key === ETH_PSEUDO)
118
+ return { symbol: "ETH", decimals: 18 };
119
+ if (key === GBLIN_VAULT.toLowerCase())
120
+ return { symbol: "GBLIN", decimals: 18 };
121
+ const cached = tokenMeta.get(key);
122
+ if (cached)
123
+ return cached;
124
+ const [symbol, decimals] = await Promise.all([
125
+ client.readContract({ address: key, abi: ERC20_ABI, functionName: "symbol" }).catch(() => "?"),
126
+ client.readContract({ address: key, abi: ERC20_ABI, functionName: "decimals" }).catch(() => 18),
127
+ ]);
128
+ const meta = { symbol: String(symbol), decimals: Number(decimals) };
129
+ tokenMeta.set(key, meta);
130
+ return meta;
131
+ }
132
+ /** Net token movements for `account` in a list of logs, ETH included when the node reports it. */
133
+ async function netTransfers(logs, account) {
134
+ const me = account.toLowerCase();
135
+ const deltas = new Map();
136
+ for (const log of logs) {
137
+ if (log.topics?.[0]?.toLowerCase() !== TRANSFER_TOPIC || log.topics.length < 3)
138
+ continue;
139
+ const from = `0x${(log.topics[1] ?? "").slice(26)}`.toLowerCase();
140
+ const to = `0x${(log.topics[2] ?? "").slice(26)}`.toLowerCase();
141
+ if (from !== me && to !== me)
142
+ continue;
143
+ const amount = BigInt(log.data === "0x" ? 0 : log.data);
144
+ const token = log.address.toLowerCase();
145
+ const signed = (to === me ? amount : 0n) - (from === me ? amount : 0n);
146
+ deltas.set(token, (deltas.get(token) ?? 0n) + signed);
147
+ }
148
+ const out = [];
149
+ for (const [token, delta] of deltas) {
150
+ if (delta === 0n)
151
+ continue;
152
+ const meta = await describeToken(token);
153
+ out.push({
154
+ token: token === ETH_PSEUDO ? "ETH" : getAddress(token),
155
+ symbol: meta.symbol,
156
+ delta: formatUnits(delta, meta.decimals),
157
+ });
158
+ }
159
+ return out;
160
+ }
161
+ // ─── prepare_action ─────────────────────────────────────────────────────────
162
+ const ACTIONS = [
163
+ "mint_with_eth",
164
+ "mint_with_weth",
165
+ "mint_with_usdc",
166
+ "redeem_in_kind",
167
+ "exit_to_eth",
168
+ "exit_to_usdc",
169
+ "bid",
170
+ ];
171
+ const PrepareSchema = z.object({
172
+ action: z.enum(ACTIONS),
173
+ wallet_address: AddressSchema,
174
+ amount: AmountSchema.optional(),
175
+ row: z.number().int().nonnegative().optional(),
176
+ });
177
+ export const PREPARE_ACTION_DEFINITION = {
178
+ name: "prepare_action",
179
+ description: "Build the unsigned transactions for any operation on the GBLIN vault, in the order to send them. Actions: mint_with_eth (amount in ETH), mint_with_weth (amount in WETH), mint_with_usdc (amount in USDC, through the Zap), redeem_in_kind (amount in shares; pro rata basket tokens, no fee, no price feed), exit_to_eth (amount in shares, through the Zap, all or nothing), exit_to_usdc (amount = the USDC you need), bid (trade with the rebalancing auction; row optional, the largest gap by default). Every output bound is non-zero and every step through the vault or the Zap carries its gas limit. Nothing is signed or sent: simulate the steps with preview_steps, then send them from the wallet.",
180
+ inputSchema: {
181
+ type: "object",
182
+ properties: {
183
+ action: { type: "string", enum: [...ACTIONS], description: "The operation to prepare." },
184
+ wallet_address: { type: "string", pattern: "^0x[a-fA-F0-9]{40}$", description: "The wallet that will sign and receive." },
185
+ amount: {
186
+ type: "string",
187
+ pattern: "^\\d+(\\.\\d+)?$",
188
+ description: "Decimal amount. Unit depends on the action (see the description). Not used by bid.",
189
+ },
190
+ row: { type: "integer", minimum: 0, description: "bid only: the basket row to bid on. Default: the row with the largest gap." },
191
+ },
192
+ required: ["action", "wallet_address"],
193
+ additionalProperties: false,
194
+ },
195
+ };
196
+ function withAction(action, inner) {
197
+ const res = inner;
198
+ if (res.isError || !res.structuredContent)
199
+ return inner;
200
+ return toolResult({
201
+ action,
202
+ ...res.structuredContent,
203
+ next: "Simulate the steps with preview_steps, then send them in order from the wallet.",
204
+ });
205
+ }
206
+ export async function handlePrepareAction(args) {
207
+ const parsed = PrepareSchema.safeParse(args);
208
+ if (!parsed.success)
209
+ return toolError(`Invalid arguments: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
210
+ const { action, wallet_address: wallet, amount, row } = parsed.data;
211
+ if (action !== "bid" && !amount)
212
+ return toolError(`amount is required for ${action}.`);
213
+ try {
214
+ // Loaded at call time: tools.ts registers this module, so a static import would be circular.
215
+ if (action === "mint_with_usdc") {
216
+ const { handleInvest } = await import("./tools.js");
217
+ return withAction(action, await handleInvest({ usdc_amount: amount, wallet_address: wallet }));
218
+ }
219
+ if (action === "exit_to_usdc") {
220
+ const { handleJitSwap } = await import("./tools.js");
221
+ return withAction(action, await handleJitSwap({ usdc_needed: amount, wallet_address: wallet }));
222
+ }
223
+ const slippage = await getDynamicSlippage();
224
+ const warnings = [];
225
+ let steps = [];
226
+ let expected = {};
227
+ if (action === "mint_with_eth" || action === "mint_with_weth") {
228
+ const value = parseUnits(amount, 18);
229
+ if (value === 0n)
230
+ return toolError("amount must be greater than zero.");
231
+ const [out] = await client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "quoteBuy", args: [GBLIN_VAULT, value] });
232
+ const minOut = applySlippageBuffer(out, slippage.bps);
233
+ if (minOut === 0n)
234
+ return toolError("The quote is zero: the amount is too small or the NAV cannot be priced now.");
235
+ if (action === "mint_with_eth") {
236
+ steps = [
237
+ {
238
+ step: 1,
239
+ description: "Mint GBLIN with ETH at NAV",
240
+ target: GBLIN_VAULT,
241
+ calldata: appendBuilderCode(encodeFunctionData({ abi: GBLIN_ABI, functionName: "buyGBLIN", args: [minOut] })),
242
+ value: value.toString(),
243
+ gas: ZAP_GAS_LIMIT.toString(),
244
+ },
245
+ ];
246
+ }
247
+ else {
248
+ steps = [
249
+ {
250
+ step: 1,
251
+ description: "Approve WETH to the vault",
252
+ target: WETH,
253
+ calldata: appendBuilderCode(encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [GBLIN_VAULT, value] })),
254
+ value: "0",
255
+ },
256
+ {
257
+ step: 2,
258
+ description: "Mint GBLIN with WETH at NAV",
259
+ target: GBLIN_VAULT,
260
+ calldata: appendBuilderCode(encodeFunctionData({ abi: GBLIN_ABI, functionName: "buyGBLINWithWeth", args: [value, minOut, wallet] })),
261
+ value: "0",
262
+ gas: ZAP_GAS_LIMIT.toString(),
263
+ },
264
+ ];
265
+ }
266
+ expected = { shares_out: formatUnits(out, 18), min_shares_out: formatUnits(minOut, 18), slippage_buffer_pct: slippage.pct };
267
+ }
268
+ if (action === "redeem_in_kind" || action === "exit_to_eth") {
269
+ const shares = parseUnits(amount, 18);
270
+ if (shares === 0n)
271
+ return toolError("amount must be greater than zero.");
272
+ const [balance, cooldown] = await Promise.all([
273
+ client.readContract({ address: GBLIN_VAULT, abi: GBLIN_ABI, functionName: "balanceOf", args: [wallet] }),
274
+ checkCooldown(wallet),
275
+ ]);
276
+ if (balance < shares)
277
+ warnings.push(`The wallet holds ${formatUnits(balance, 18)} shares, less than ${amount}.`);
278
+ if (cooldown.active)
279
+ warnings.push(`The redemption cooldown after your own mint is active for ${cooldown.secondsRemaining} more seconds.`);
280
+ if (action === "redeem_in_kind") {
281
+ const [supply, rowCount] = await Promise.all([
282
+ client.readContract({ address: GBLIN_VAULT, abi: GBLIN_ABI, functionName: "totalSupply" }),
283
+ client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "basketLength", args: [GBLIN_VAULT] }),
284
+ ]);
285
+ const legs = [];
286
+ for (let i = 0n; i < rowCount; i++) {
287
+ const r = await client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "asset", args: [GBLIN_VAULT, i] });
288
+ if (r[7])
289
+ continue;
290
+ const held = await client.readContract({ address: r[0], abi: ERC20_ABI, functionName: "balanceOf", args: [GBLIN_VAULT] });
291
+ const meta = await describeToken(r[0]);
292
+ const isWeth = r[0].toLowerCase() === WETH.toLowerCase();
293
+ legs.push({
294
+ token: isWeth ? "ETH" : r[0],
295
+ symbol: isWeth ? "ETH" : meta.symbol,
296
+ approx_amount: formatUnits(supply > 0n ? (held * shares) / supply : 0n, meta.decimals),
297
+ });
298
+ }
299
+ steps = [
300
+ {
301
+ step: 1,
302
+ description: "Redeem in kind: receive your pro rata share of every basket token",
303
+ target: GBLIN_VAULT,
304
+ calldata: appendBuilderCode(encodeFunctionData({ abi: GBLIN_ABI, functionName: "sellGBLIN", args: [shares] })),
305
+ value: "0",
306
+ gas: ZAP_GAS_LIMIT.toString(),
307
+ },
308
+ ];
309
+ expected = {
310
+ legs,
311
+ note: "Pro rata of the vault's balances; the WETH leg is paid in ETH. No fee and no price feed are involved. A leg a token refuses to deliver becomes a credit claimable with claimPending(token).",
312
+ };
313
+ }
314
+ else {
315
+ const ethOut = await client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "quoteSell", args: [GBLIN_VAULT, shares] });
316
+ const minEthOut = applySlippageBuffer(ethOut, slippage.bps);
317
+ const venue = await venueDataPerRow();
318
+ steps = [
319
+ {
320
+ step: 1,
321
+ description: "Approve the shares to the GBLIN Zap",
322
+ target: GBLIN_VAULT,
323
+ calldata: appendBuilderCode(encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [GBLIN_ZAP, shares] })),
324
+ value: "0",
325
+ },
326
+ {
327
+ step: 2,
328
+ description: "Redeem in kind and sell every leg for ETH through the Zap (all or nothing)",
329
+ target: GBLIN_ZAP,
330
+ calldata: appendBuilderCode(encodeFunctionData({ abi: ZAP_ABI, functionName: "sellGBLINForEth", args: [shares, minEthOut, venue, wallet] })),
331
+ value: "0",
332
+ gas: ZAP_GAS_LIMIT.toString(),
333
+ },
334
+ ];
335
+ expected = { eth_out: formatUnits(ethOut, 18), min_eth_out: formatUnits(minEthOut, 18), slippage_buffer_pct: slippage.pct };
336
+ }
337
+ }
338
+ if (action === "bid") {
339
+ const state = await getAuctionState();
340
+ const target = row === undefined ? state.best : state.rows.find((r) => r.index === row) ?? null;
341
+ if (!state.auctionOpen)
342
+ return toolError("No rebalancing auction is open.", "get_auction_state reports when one opens.");
343
+ if (!target || !target.calldata) {
344
+ return toolError(row === undefined ? "No row can be bid on right now." : `Row ${row} cannot be bid on right now.`, "get_auction_state lists every row with its side and gap.");
345
+ }
346
+ steps = [
347
+ {
348
+ step: 1,
349
+ description: `Approve ${target.inputSymbol} to the vault`,
350
+ target: target.calldata.approve.target,
351
+ calldata: target.calldata.approve.data,
352
+ value: "0",
353
+ },
354
+ {
355
+ step: 2,
356
+ description: `Bid on row ${target.index} (${target.symbol}): hand over ${target.inputAmount} ${target.inputSymbol}`,
357
+ target: target.calldata.bid.target,
358
+ calldata: target.calldata.bid.data,
359
+ value: "0",
360
+ gas: ZAP_GAS_LIMIT.toString(),
361
+ },
362
+ ];
363
+ expected = {
364
+ row: target.index,
365
+ vault_buys_asset: target.vaultBuysAsset,
366
+ gap_eth: target.gapEth,
367
+ input: `${target.inputAmount} ${target.inputSymbol}`,
368
+ premium_bps: state.premiumBps,
369
+ note: "You receive the other side at the oracle price adjusted by the premium; the input is reduced to what closes the gap.",
370
+ };
371
+ }
372
+ return toolResult({
373
+ action,
374
+ wallet,
375
+ steps,
376
+ expected,
377
+ warnings,
378
+ next: "Simulate the steps with preview_steps, then send them in order from the wallet, each with the gas it carries.",
379
+ });
380
+ }
381
+ catch (err) {
382
+ return toolError(err.message, "Check RPC connectivity and oracle freshness.");
383
+ }
384
+ }
385
+ // ─── preview_steps ──────────────────────────────────────────────────────────
386
+ const StepInputSchema = z.object({
387
+ target: AddressSchema,
388
+ calldata: z.string().regex(/^0x([0-9a-fA-F]{2})*$/, "calldata must be 0x-prefixed hex"),
389
+ value: z.string().regex(/^\d+$/).optional(),
390
+ gas: z.string().regex(/^\d+$/).optional(),
391
+ description: z.string().optional(),
392
+ step: z.number().optional(),
393
+ });
394
+ const PreviewSchema = z.object({
395
+ from: AddressSchema,
396
+ steps: z.array(StepInputSchema).min(1).max(8),
397
+ });
398
+ export const PREVIEW_STEPS_DEFINITION = {
399
+ name: "preview_steps",
400
+ description: "Simulate a list of transactions in sequence against the latest Base block, as if the wallet sent them one after the other, before anything is signed. Each step sees the state the previous ones left (an approval before a mint works). Returns, per step, whether it would succeed, the gas it uses, whether the gas limit it carries is enough, and the decoded revert reason with a hint when it fails; and the net token and ETH movements for the wallet. Pass the steps exactly as prepare_action, invest_usdc_to_gblin or swap_gblin_to_usdc_jit return them. State can change before your transactions land: a successful preview is evidence, not a guarantee.",
401
+ inputSchema: {
402
+ type: "object",
403
+ properties: {
404
+ from: { type: "string", pattern: "^0x[a-fA-F0-9]{40}$", description: "The wallet that will send the steps." },
405
+ steps: {
406
+ type: "array",
407
+ minItems: 1,
408
+ maxItems: 8,
409
+ description: "The steps, in order: target, calldata, value in wei (optional), gas (optional).",
410
+ items: {
411
+ type: "object",
412
+ properties: {
413
+ target: { type: "string", pattern: "^0x[a-fA-F0-9]{40}$" },
414
+ calldata: { type: "string", pattern: "^0x([0-9a-fA-F]{2})*$" },
415
+ value: { type: "string", pattern: "^\\d+$" },
416
+ gas: { type: "string", pattern: "^\\d+$" },
417
+ },
418
+ required: ["target", "calldata"],
419
+ additionalProperties: true,
420
+ },
421
+ },
422
+ },
423
+ required: ["from", "steps"],
424
+ additionalProperties: false,
425
+ },
426
+ };
427
+ async function simulate(from, steps) {
428
+ const params = [
429
+ {
430
+ blockStateCalls: [
431
+ {
432
+ calls: steps.map((s) => ({
433
+ from,
434
+ to: s.target,
435
+ data: s.calldata,
436
+ ...(s.value && s.value !== "0" ? { value: toHex(BigInt(s.value)) } : {}),
437
+ ...(s.gas ? { gas: toHex(BigInt(s.gas)) } : {}),
438
+ })),
439
+ },
440
+ ],
441
+ traceTransfers: true,
442
+ validation: false,
443
+ },
444
+ "latest",
445
+ ];
446
+ let lastError;
447
+ for (const c of [client, historyClient]) {
448
+ try {
449
+ const res = (await c.request({ method: "eth_simulateV1", params }));
450
+ if (res?.[0]?.calls)
451
+ return res[0];
452
+ }
453
+ catch (err) {
454
+ lastError = err;
455
+ }
456
+ }
457
+ throw new Error(`eth_simulateV1 is not available on the configured RPC or the public fallbacks: ${lastError?.message ?? "no result"}`);
458
+ }
459
+ /**
460
+ * The smallest gas limit with which step `i` still succeeds, found by bisection on simulations.
461
+ * For calls into the vault or the Zap the gas a step USES is not the limit it NEEDS: the vault checks
462
+ * that enough gas is left for its capped transfers before making them, so the required limit sits
463
+ * well above the gas consumed. Only a search against the real contract finds it.
464
+ */
465
+ async function minimalGasLimit(from, steps, i, used) {
466
+ const upTo = steps.slice(0, i + 1).map(({ gas: _gas, ...rest }, k) => (k === i ? rest : { ...rest }));
467
+ const passes = async (limit) => {
468
+ const trial = upTo.map((st, k) => (k === i ? { ...st, gas: limit.toString() } : st));
469
+ const r = await simulate(from, trial).catch(() => null);
470
+ return r?.calls[i]?.status === "0x1";
471
+ };
472
+ let lo = used;
473
+ let hi = used * 3n > 3000000n ? used * 3n : 3000000n;
474
+ if (!(await passes(hi)))
475
+ return null;
476
+ while (hi - lo > 2000n) {
477
+ const mid = (lo + hi) / 2n;
478
+ if (await passes(mid))
479
+ hi = mid;
480
+ else
481
+ lo = mid;
482
+ }
483
+ return hi;
484
+ }
485
+ const RESERVE_TARGETS = new Set([GBLIN_VAULT.toLowerCase(), GBLIN_ZAP.toLowerCase()]);
486
+ export async function handlePreviewSteps(args) {
487
+ const parsed = PreviewSchema.safeParse(args);
488
+ if (!parsed.success)
489
+ return toolError(`Invalid arguments: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
490
+ const { from, steps } = parsed.data;
491
+ try {
492
+ const sim = await simulate(from, steps);
493
+ // When a step fails under the limit it carries, simulate again without limits: if it then succeeds,
494
+ // the limit was the cause, and the unbounded run measures what the step really needs.
495
+ const failedUnderLimit = sim.calls.some((c, i) => c.status !== "0x1" && steps[i]?.gas);
496
+ const unbounded = failedUnderLimit
497
+ ? await simulate(from, steps.map(({ gas: _gas, ...rest }) => rest)).catch(() => null)
498
+ : null;
499
+ const allLogs = [];
500
+ let wouldSucceed = true;
501
+ const results = [];
502
+ for (let i = 0; i < sim.calls.length; i++) {
503
+ const call = sim.calls[i];
504
+ const s = steps[i];
505
+ const ok = call.status === "0x1";
506
+ const gasUsed = BigInt(call.gasUsed ?? "0x0");
507
+ const free = unbounded?.calls[i];
508
+ const outOfGas = !ok && Boolean(s.gas) && free?.status === "0x1";
509
+ const measured = outOfGas && free ? free : call;
510
+ const peak = measured.maxUsedGas ? BigInt(measured.maxUsedGas) : BigInt(measured.gasUsed ?? "0x0");
511
+ // Steps into the vault or the Zap: the minimal passing limit found by bisection, plus a tenth.
512
+ // Other steps: a fifth above the peak gas the call touched.
513
+ let recommended = (peak * 12n) / 10n;
514
+ if (RESERVE_TARGETS.has(s.target.toLowerCase()) && (ok || outOfGas)) {
515
+ const earlierOk = sim.calls.slice(0, i).every((c) => c.status === "0x1");
516
+ const minimal = earlierOk ? await minimalGasLimit(from, steps, i, peak) : null;
517
+ if (minimal)
518
+ recommended = (minimal * 11n) / 10n;
519
+ }
520
+ const given = s.gas ? BigInt(s.gas) : null;
521
+ if (!ok)
522
+ wouldSucceed = false;
523
+ if (ok)
524
+ allLogs.push(...(call.logs ?? []));
525
+ const revert = ok
526
+ ? null
527
+ : outOfGas
528
+ ? { error: "Out of gas at the limit this step carries: it succeeds with a higher limit.", hint: ERROR_HINTS.InsufficientGas }
529
+ : decodeRevert(call.error?.data ?? call.returnData);
530
+ results.push({
531
+ step: s.step ?? i + 1,
532
+ target: s.target,
533
+ ...(s.description ? { description: s.description } : {}),
534
+ success: ok,
535
+ gas_used: Number(gasUsed),
536
+ gas_limit_given: given === null ? null : Number(given),
537
+ // true when the step passes with its limit; false only when the limit is what makes it fail;
538
+ // null when it fails for another reason, since the simulation cannot tell whether the limit would do.
539
+ gas_limit_enough: given === null ? null : ok ? true : outOfGas ? false : null,
540
+ recommended_gas_limit: Number(recommended),
541
+ ...(given === null && RESERVE_TARGETS.has(s.target.toLowerCase())
542
+ ? { gas_warning: "No gas limit given: send this step with recommended_gas_limit. A wallet's automatic estimate can fall under what the vault's capped transfers need." }
543
+ : {}),
544
+ ...(ok
545
+ ? {}
546
+ : {
547
+ error: revert?.error ?? call.error?.message ?? "reverted",
548
+ ...(revert?.hint ? { hint: revert.hint } : {}),
549
+ }),
550
+ });
551
+ }
552
+ // Steps after a failure cannot be trusted: the wallet would stop at the first revert.
553
+ const firstFailure = results.findIndex((r) => !r.success);
554
+ return toolResult({
555
+ would_succeed: wouldSucceed,
556
+ from,
557
+ block: Number(BigInt(sim.number)),
558
+ steps: results,
559
+ first_failing_step: firstFailure === -1 ? null : results[firstFailure].step,
560
+ balance_changes: wouldSucceed ? await netTransfers(allLogs, from) : [],
561
+ note: "Simulated in sequence against the latest block, without signature or nonce checks. State can change before your transactions land: a successful preview is evidence, not a guarantee.",
562
+ });
563
+ }
564
+ catch (err) {
565
+ return toolError(err.message);
566
+ }
567
+ }
568
+ // ─── get_transaction_status ─────────────────────────────────────────────────
569
+ const TxStatusSchema = z.object({ hash: z.string().regex(/^0x[0-9a-fA-F]{64}$/, "hash must be a 32-byte 0x hex string") });
570
+ export const TX_STATUS_DEFINITION = {
571
+ name: "get_transaction_status",
572
+ description: "Report what a sent Base transaction did: pending, succeeded, reverted or not found; its block and confirmations; the fee paid; the net token movements for the sender (GBLIN shares minted or redeemed, USDC, basket tokens); and, when it reverted, the decoded reason. Use it after sending the steps from prepare_action to confirm each one before the next.",
573
+ inputSchema: {
574
+ type: "object",
575
+ properties: { hash: { type: "string", pattern: "^0x[0-9a-fA-F]{64}$", description: "The transaction hash." } },
576
+ required: ["hash"],
577
+ additionalProperties: false,
578
+ },
579
+ };
580
+ export async function handleTransactionStatus(args) {
581
+ const parsed = TxStatusSchema.safeParse(args);
582
+ if (!parsed.success)
583
+ return toolError(`Invalid arguments: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
584
+ const hash = parsed.data.hash;
585
+ try {
586
+ const receipt = await client.getTransactionReceipt({ hash }).catch(() => historyClient.getTransactionReceipt({ hash }).catch(() => null));
587
+ if (!receipt) {
588
+ const tx = await client.getTransaction({ hash }).catch(() => null);
589
+ return toolResult({
590
+ hash,
591
+ status: tx ? "pending" : "not_found",
592
+ note: tx ? "Known to the node but not yet in a block." : "No node consulted knows this hash. It may not have been broadcast.",
593
+ });
594
+ }
595
+ const [latest, tx] = await Promise.all([client.getBlockNumber(), client.getTransaction({ hash }).catch(() => null)]);
596
+ const succeeded = receipt.status === "success";
597
+ let revert = null;
598
+ if (!succeeded && tx) {
599
+ try {
600
+ await historyClient.call({ account: tx.from, to: tx.to ?? undefined, data: tx.input, value: tx.value, blockNumber: receipt.blockNumber - 1n });
601
+ revert = { error: "The call succeeds when replayed on the parent block: it most likely ran out of gas.", hint: ERROR_HINTS.InsufficientGas };
602
+ }
603
+ catch (err) {
604
+ const data = err.cause?.data ?? err.data;
605
+ revert = decodeRevert(data) ?? { error: err.message.split("\n")[0] ?? "reverted" };
606
+ }
607
+ }
608
+ const l1Fee = receipt.l1Fee ?? 0n;
609
+ const feeWei = receipt.gasUsed * receipt.effectiveGasPrice + (l1Fee ?? 0n);
610
+ return toolResult({
611
+ hash,
612
+ status: succeeded ? "success" : "reverted",
613
+ block: Number(receipt.blockNumber),
614
+ confirmations: Number(latest - receipt.blockNumber + 1n),
615
+ from: receipt.from,
616
+ to: receipt.to,
617
+ gas_used: Number(receipt.gasUsed),
618
+ gas_limit: tx ? Number(tx.gas) : null,
619
+ fee_eth: formatUnits(feeWei, 18),
620
+ balance_changes: succeeded
621
+ ? await netTransfers(receipt.logs.map((l) => ({ address: l.address, topics: l.topics, data: l.data })), receipt.from)
622
+ : [],
623
+ ...(revert ? { revert } : {}),
624
+ explorer: `https://basescan.org/tx/${hash}`,
625
+ });
626
+ }
627
+ catch (err) {
628
+ return toolError(err.message);
629
+ }
630
+ }
631
+ // ─── get_nav_history ────────────────────────────────────────────────────────
632
+ const HistorySchema = z.object({
633
+ interval: z.enum(["hour", "day"]).default("day"),
634
+ points: z.number().int().min(2).max(90).default(30),
635
+ });
636
+ export const NAV_HISTORY_DEFINITION = {
637
+ name: "get_nav_history",
638
+ description: "NAV per GBLIN share over time, read from the chain at past blocks, beside the ETH/USD and BTC/USD oracle prices the vault itself uses, with the change of each over the window and the NAV's largest drawdown. History starts at the deployment of the vault in service. Use it to judge how the index behaved against holding ETH or BTC over the same span.",
639
+ inputSchema: {
640
+ type: "object",
641
+ properties: {
642
+ interval: { type: "string", enum: ["hour", "day"], description: "Spacing of the points. Default day." },
643
+ points: { type: "integer", minimum: 2, maximum: 90, description: "How many points, newest last. Default 30." },
644
+ },
645
+ additionalProperties: false,
646
+ },
647
+ };
648
+ const pct = (a, b) => (a > 0 ? Number((((b - a) / a) * 100).toFixed(2)) : null);
649
+ export async function handleNavHistory(args) {
650
+ const parsed = HistorySchema.safeParse(args ?? {});
651
+ if (!parsed.success)
652
+ return toolError(`Invalid arguments: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
653
+ const { interval, points } = parsed.data;
654
+ try {
655
+ const step = interval === "hour" ? BLOCKS_PER_HOUR : BLOCKS_PER_HOUR * 24n;
656
+ const latest = await historyClient.getBlockNumber();
657
+ // The feeds are the vault's own: the WETH oracle, and the oracle of the non-stable, non-WETH row.
658
+ const [wethOracle, rowCount] = await Promise.all([
659
+ client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "wethOracle", args: [GBLIN_VAULT] }),
660
+ client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "basketLength", args: [GBLIN_VAULT] }),
661
+ ]);
662
+ let btcOracle = null;
663
+ for (let i = 0n; i < rowCount && !btcOracle; i++) {
664
+ const r = await client.readContract({ address: GBLIN_LENS, abi: LENS_ABI, functionName: "asset", args: [GBLIN_VAULT, i] });
665
+ if (!r[2] && r[0].toLowerCase() !== WETH.toLowerCase() && !r[7])
666
+ btcOracle = r[1];
667
+ }
668
+ const blocks = [];
669
+ for (let k = 0n; k < BigInt(points); k++) {
670
+ const b = latest - k * step;
671
+ if (b < LENS_DEPLOY_BLOCK)
672
+ break;
673
+ blocks.unshift(b);
674
+ }
675
+ const truncated = blocks.length < points;
676
+ // One multicall per point (NAV and both feeds read at the same block), and times derived from the
677
+ // block number: Base produces exactly one block every two seconds. That keeps a request inside the
678
+ // subrequest budget of a hosted Worker as well as fast on a local server.
679
+ const latestBlock = await historyClient.getBlock({ blockNumber: latest });
680
+ const series = [];
681
+ for (const blockNumber of blocks) {
682
+ const contracts = [
683
+ { address: GBLIN_LENS, abi: LENS_ABI, functionName: "quoteSell", args: [GBLIN_VAULT, 10n ** 18n] },
684
+ { address: wethOracle, abi: CHAINLINK_AGGREGATOR_ABI, functionName: "latestRoundData" },
685
+ ...(btcOracle ? [{ address: btcOracle, abi: CHAINLINK_AGGREGATOR_ABI, functionName: "latestRoundData" }] : []),
686
+ ];
687
+ const out = await historyClient.multicall({ contracts: contracts, blockNumber, allowFailure: false });
688
+ const navEthWei = out[0];
689
+ const eth = out[1];
690
+ const btc = (btcOracle ? out[2] : null);
691
+ const timestamp = Number(latestBlock.timestamp) - Number(latest - blockNumber) * 2;
692
+ const ethUsd = Number(eth[1]) / 1e8;
693
+ const navEth = Number(formatUnits(navEthWei, 18));
694
+ series.push({
695
+ time: new Date(timestamp * 1000).toISOString(),
696
+ block: Number(blockNumber),
697
+ nav_eth: Number(navEth.toFixed(8)),
698
+ nav_usd: Number((navEth * ethUsd).toFixed(4)),
699
+ eth_usd: Number(ethUsd.toFixed(2)),
700
+ btc_usd: btc ? Number((Number(btc[1]) / 1e8).toFixed(2)) : null,
701
+ });
702
+ }
703
+ const first = series[0];
704
+ const last = series[series.length - 1];
705
+ let peak = 0;
706
+ let maxDrawdown = 0;
707
+ for (const p of series) {
708
+ peak = Math.max(peak, p.nav_usd);
709
+ if (peak > 0)
710
+ maxDrawdown = Math.max(maxDrawdown, (peak - p.nav_usd) / peak);
711
+ }
712
+ return toolResult({
713
+ interval,
714
+ points: series.length,
715
+ series,
716
+ summary: first && last
717
+ ? {
718
+ from: first.time,
719
+ to: last.time,
720
+ nav_usd_change_pct: pct(first.nav_usd, last.nav_usd),
721
+ eth_usd_change_pct: pct(first.eth_usd, last.eth_usd),
722
+ btc_usd_change_pct: first.btc_usd && last.btc_usd ? pct(first.btc_usd, last.btc_usd) : null,
723
+ nav_max_drawdown_pct: Number((maxDrawdown * 100).toFixed(2)),
724
+ }
725
+ : null,
726
+ ...(truncated ? { note: "The window reaches back before the vault in service existed: history starts at its deployment." } : {}),
727
+ method: "NAV per share = the Lens quote for redeeming one share, in ETH, times the vault's ETH/USD oracle, read at each block.",
728
+ });
729
+ }
730
+ catch (err) {
731
+ return toolError(err.message, "Historical reads need an archive-capable RPC. Set GBLIN_ARCHIVE_RPC_URL, or retry: the public fallbacks rate-limit.");
732
+ }
733
+ }
734
+ export const ACTION_TOOL_HANDLERS = {
735
+ prepare_action: handlePrepareAction,
736
+ preview_steps: handlePreviewSteps,
737
+ get_transaction_status: handleTransactionStatus,
738
+ get_nav_history: handleNavHistory,
739
+ };
740
+ export const ACTION_TOOL_DEFINITIONS = [
741
+ { def: PREPARE_ACTION_DEFINITION, title: "Prepare an operation on the vault" },
742
+ { def: PREVIEW_STEPS_DEFINITION, title: "Simulate steps before signing" },
743
+ { def: TX_STATUS_DEFINITION, title: "Read a transaction's outcome" },
744
+ { def: NAV_HISTORY_DEFINITION, title: "Read the NAV history" },
745
+ ].map(({ def, title }) => ({
746
+ ...def,
747
+ annotations: { title, readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
748
+ }));
749
+ //# sourceMappingURL=actions.js.map