@aibtc/mcp-server 1.65.1 → 1.66.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/dist/config/legion.d.ts +58 -0
- package/dist/config/legion.js +100 -0
- package/dist/services/legion.service.d.ts +188 -0
- package/dist/services/legion.service.js +410 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/legion.tools.d.ts +16 -0
- package/dist/tools/legion.tools.js +1373 -0
- package/package.json +2 -1
- package/skill/SKILL.md +1 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIBTC News Legion (news-gov-v5) chain reads.
|
|
3
|
+
*
|
|
4
|
+
* Everything here reads the deployed contracts rather than trusting constants:
|
|
5
|
+
* governance parameters, the timing mode (which clock the windows count on),
|
|
6
|
+
* and the sBTC token the treasury actually holds all come off chain and are
|
|
7
|
+
* cached per process. A doc that drifts from the contract is a doc; a read that
|
|
8
|
+
* drifts from the contract is a bug, so there is nothing to drift.
|
|
9
|
+
*/
|
|
10
|
+
import { ClarityType, cvToJSON, deserializeCV, getAddressFromPrivateKey, principalCV, uintCV, } from "@stacks/transactions";
|
|
11
|
+
import { LEGION_GOV_CONTRACT, LEGION_NETWORK, LEGION_TREASURY_CONTRACT, ORDINALS_CONTENT_BASE, ORDINALS_INSCRIPTION_BASE, } from "../config/legion.js";
|
|
12
|
+
import { getHiroApi } from "./hiro-api.js";
|
|
13
|
+
import { getAccount } from "./x402.service.js";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Clarity value → plain JS
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Deep-unwrap a ClarityValue into plain JS. `cvToValue` only unwraps the top
|
|
19
|
+
* level — nested tuples and optionals come back as `{type, value}` envelopes —
|
|
20
|
+
* so the legion's tuple-heavy views need this instead.
|
|
21
|
+
*
|
|
22
|
+
* uints/ints become strings to stay exact; callers narrow with `num()`.
|
|
23
|
+
*/
|
|
24
|
+
export function simplifyCV(cv) {
|
|
25
|
+
switch (cv.type) {
|
|
26
|
+
case ClarityType.BoolTrue:
|
|
27
|
+
return true;
|
|
28
|
+
case ClarityType.BoolFalse:
|
|
29
|
+
return false;
|
|
30
|
+
case ClarityType.Int:
|
|
31
|
+
case ClarityType.UInt:
|
|
32
|
+
return cv.value.toString();
|
|
33
|
+
case ClarityType.Buffer:
|
|
34
|
+
return `0x${cv.value}`;
|
|
35
|
+
case ClarityType.StringASCII:
|
|
36
|
+
case ClarityType.StringUTF8:
|
|
37
|
+
case ClarityType.PrincipalStandard:
|
|
38
|
+
case ClarityType.PrincipalContract:
|
|
39
|
+
return cv.value;
|
|
40
|
+
case ClarityType.OptionalNone:
|
|
41
|
+
return null;
|
|
42
|
+
case ClarityType.OptionalSome:
|
|
43
|
+
case ClarityType.ResponseOk:
|
|
44
|
+
case ClarityType.ResponseErr:
|
|
45
|
+
return simplifyCV(cv.value);
|
|
46
|
+
case ClarityType.List:
|
|
47
|
+
return cv.value.map(simplifyCV);
|
|
48
|
+
case ClarityType.Tuple: {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const [key, value] of Object.entries(cv.value)) {
|
|
51
|
+
out[key] = simplifyCV(value);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
default:
|
|
56
|
+
return cvToJSON(cv).value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** A uint field from a simplified tuple, as a JS number. */
|
|
60
|
+
export function num(value) {
|
|
61
|
+
return Number(value);
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Contract reads
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Call a read-only function on a legion contract and return it deep-unwrapped.
|
|
68
|
+
* Always reads the legion's own chain, never the globally configured one.
|
|
69
|
+
*/
|
|
70
|
+
export async function readLegionContract(contractId, functionName, functionArgs = []) {
|
|
71
|
+
const hiro = getHiroApi(LEGION_NETWORK);
|
|
72
|
+
const sender = contractId.split(".")[0];
|
|
73
|
+
const result = await hiro.callReadOnlyFunction(contractId, functionName, functionArgs, sender);
|
|
74
|
+
if (!result.okay || !result.result) {
|
|
75
|
+
throw new Error(`${contractId}::${functionName} failed: ${result.cause ?? "no result"}`);
|
|
76
|
+
}
|
|
77
|
+
const hex = result.result.startsWith("0x")
|
|
78
|
+
? result.result.slice(2)
|
|
79
|
+
: result.result;
|
|
80
|
+
return simplifyCV(deserializeCV(Buffer.from(hex, "hex")));
|
|
81
|
+
}
|
|
82
|
+
export function readGov(functionName, functionArgs = []) {
|
|
83
|
+
return readLegionContract(LEGION_GOV_CONTRACT, functionName, functionArgs);
|
|
84
|
+
}
|
|
85
|
+
export function readTreasury(functionName, functionArgs = []) {
|
|
86
|
+
return readLegionContract(LEGION_TREASURY_CONTRACT, functionName, functionArgs);
|
|
87
|
+
}
|
|
88
|
+
let paramsCache = null;
|
|
89
|
+
let timingModeCache = null;
|
|
90
|
+
let tokenCache = null;
|
|
91
|
+
/** Every governance parameter, straight from `get-params`. */
|
|
92
|
+
export async function getParams() {
|
|
93
|
+
if (paramsCache)
|
|
94
|
+
return paramsCache;
|
|
95
|
+
const raw = (await readGov("get-params"));
|
|
96
|
+
paramsCache = {
|
|
97
|
+
votingQuorum: num(raw.votingQuorum),
|
|
98
|
+
votingThreshold: num(raw.votingThreshold),
|
|
99
|
+
vetoQuorum: num(raw.vetoQuorum),
|
|
100
|
+
minParticipants: num(raw.minParticipants),
|
|
101
|
+
minWeight: num(raw.minWeight),
|
|
102
|
+
minContribution: num(raw.minContribution),
|
|
103
|
+
drawBps: num(raw.drawBps),
|
|
104
|
+
votingDelay: num(raw.votingDelay),
|
|
105
|
+
voteWindow: num(raw.voteWindow),
|
|
106
|
+
vetoWindow: num(raw.vetoWindow),
|
|
107
|
+
concludeWindow: num(raw.concludeWindow),
|
|
108
|
+
proposeInterval: num(raw.proposeInterval),
|
|
109
|
+
};
|
|
110
|
+
return paramsCache;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Which clock the windows count on. `"TEST-STACKS-BLOCKS"` counts Stacks
|
|
114
|
+
* blocks; `"PROD-BURN"` counts Bitcoin burn blocks. Never assume — a window
|
|
115
|
+
* measured against the wrong tip is off by a factor of ten.
|
|
116
|
+
*/
|
|
117
|
+
export async function getTimingMode() {
|
|
118
|
+
if (timingModeCache)
|
|
119
|
+
return timingModeCache;
|
|
120
|
+
timingModeCache = (await readGov("get-timing-mode"));
|
|
121
|
+
return timingModeCache;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The sBTC token the treasury actually moves, plus its fungible-token asset
|
|
125
|
+
* name — both needed to write an exact post-condition. Read from the treasury
|
|
126
|
+
* and its contract interface so a post-condition can never name the wrong asset.
|
|
127
|
+
*/
|
|
128
|
+
export async function getLegionToken() {
|
|
129
|
+
if (tokenCache)
|
|
130
|
+
return tokenCache;
|
|
131
|
+
const contract = (await readTreasury("get-token"));
|
|
132
|
+
const iface = await getHiroApi(LEGION_NETWORK).getContractInterface(contract);
|
|
133
|
+
const assetName = iface.fungible_tokens?.[0]?.name;
|
|
134
|
+
if (!assetName) {
|
|
135
|
+
throw new Error(`${contract} declares no fungible token — cannot build a post-condition for it.`);
|
|
136
|
+
}
|
|
137
|
+
tokenCache = { contract, assetName };
|
|
138
|
+
return tokenCache;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The height the contract's windows are measured against, on the clock
|
|
142
|
+
* `get-timing-mode` names.
|
|
143
|
+
*/
|
|
144
|
+
export async function getClockHeight() {
|
|
145
|
+
const [timingMode, info] = await Promise.all([
|
|
146
|
+
getTimingMode(),
|
|
147
|
+
getHiroApi(LEGION_NETWORK).getCoreApiInfo(),
|
|
148
|
+
]);
|
|
149
|
+
const clock = timingMode === "PROD-BURN" ? "burn" : "stacks";
|
|
150
|
+
return {
|
|
151
|
+
height: clock === "burn" ? info.burn_block_height : info.stacks_tip_height,
|
|
152
|
+
clock,
|
|
153
|
+
timingMode,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// The signing account, pinned to the legion's chain
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* The unlocked wallet, re-derived for the legion's network.
|
|
161
|
+
*
|
|
162
|
+
* The same private key yields a different address per chain, so a mainnet
|
|
163
|
+
* session still signs legion calls from its testnet address against the testnet
|
|
164
|
+
* node. This is the guard that makes a mainnet-configured server safe here.
|
|
165
|
+
*/
|
|
166
|
+
export async function getLegionAccount() {
|
|
167
|
+
const base = await getAccount();
|
|
168
|
+
if (base.network === LEGION_NETWORK)
|
|
169
|
+
return base;
|
|
170
|
+
return {
|
|
171
|
+
...base,
|
|
172
|
+
address: getAddressFromPrivateKey(base.privateKey, LEGION_NETWORK),
|
|
173
|
+
network: LEGION_NETWORK,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export async function getStory(proposalId) {
|
|
177
|
+
const raw = (await readGov("get-story", [uintCV(proposalId)]));
|
|
178
|
+
if (!raw)
|
|
179
|
+
return null;
|
|
180
|
+
return {
|
|
181
|
+
proposer: String(raw.proposer),
|
|
182
|
+
bond: num(raw.bond),
|
|
183
|
+
draw: num(raw.draw),
|
|
184
|
+
createdAt: num(raw.createdAt),
|
|
185
|
+
voteEnd: num(raw.voteEnd),
|
|
186
|
+
eligibleSnapshot: num(raw.eligibleSnapshot),
|
|
187
|
+
yesWeight: num(raw.yesWeight),
|
|
188
|
+
noWeight: num(raw.noWeight),
|
|
189
|
+
vetoWeight: num(raw.vetoWeight),
|
|
190
|
+
voterCount: num(raw.voterCount),
|
|
191
|
+
status: num(raw.status),
|
|
192
|
+
reason: String(raw.reason ?? ""),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export async function getStoryMeta(proposalId) {
|
|
196
|
+
const raw = (await readGov("get-story-meta", [uintCV(proposalId)]));
|
|
197
|
+
if (!raw)
|
|
198
|
+
return null;
|
|
199
|
+
return {
|
|
200
|
+
title: String(raw.title ?? ""),
|
|
201
|
+
description: String(raw.description ?? ""),
|
|
202
|
+
link: String(raw.link ?? ""),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* `"none" | "pending" | "voting" | "veto" | "concludable" | "expired" |
|
|
207
|
+
* "passed" | "failed"` — the contract's own view of where a piece stands.
|
|
208
|
+
*/
|
|
209
|
+
export async function getPhase(proposalId) {
|
|
210
|
+
return (await readGov("get-phase", [uintCV(proposalId)]));
|
|
211
|
+
}
|
|
212
|
+
export async function getLastProposalId() {
|
|
213
|
+
return num(await readGov("get-last-proposal-id"));
|
|
214
|
+
}
|
|
215
|
+
export async function getVoteRecord(proposalId, voter) {
|
|
216
|
+
const raw = (await readGov("get-vote-record", [
|
|
217
|
+
uintCV(proposalId),
|
|
218
|
+
principalCV(voter),
|
|
219
|
+
]));
|
|
220
|
+
if (!raw)
|
|
221
|
+
return null;
|
|
222
|
+
return { support: Boolean(raw.support), weight: num(raw.weight) };
|
|
223
|
+
}
|
|
224
|
+
export async function getVetoRecord(proposalId, voter) {
|
|
225
|
+
const raw = (await readGov("get-veto-record", [
|
|
226
|
+
uintCV(proposalId),
|
|
227
|
+
principalCV(voter),
|
|
228
|
+
]));
|
|
229
|
+
if (!raw)
|
|
230
|
+
return null;
|
|
231
|
+
return { weight: num(raw.weight) };
|
|
232
|
+
}
|
|
233
|
+
/** Every propose precondition folded into one read, with the individual gates. */
|
|
234
|
+
export async function getProposeStatus(who) {
|
|
235
|
+
const raw = (await readGov("propose-status", [principalCV(who)]));
|
|
236
|
+
return {
|
|
237
|
+
canPropose: Boolean(raw.canPropose),
|
|
238
|
+
eligible: Boolean(raw.eligible),
|
|
239
|
+
slotOpen: Boolean(raw.slotOpen),
|
|
240
|
+
noLiveProposal: Boolean(raw.noLiveProposal),
|
|
241
|
+
poolOk: Boolean(raw.poolOk),
|
|
242
|
+
nextProposeHeight: num(raw.nextProposeHeight),
|
|
243
|
+
lockOnPropose: num(raw.lockOnPropose),
|
|
244
|
+
draw: num(raw.draw),
|
|
245
|
+
freeWeight: num(raw.freeWeight),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** Why a propose would revert right now, in the caller's words. */
|
|
249
|
+
export function proposeBlockers(status, minWeight) {
|
|
250
|
+
const blockers = [];
|
|
251
|
+
if (!status.eligible) {
|
|
252
|
+
blockers.push(`your weight is below minWeight (${minWeight}) — call legion_contribute first`);
|
|
253
|
+
}
|
|
254
|
+
if (!status.noLiveProposal) {
|
|
255
|
+
blockers.push("you already have a live proposal — one per principal until it concludes or lapses");
|
|
256
|
+
}
|
|
257
|
+
if (!status.slotOpen) {
|
|
258
|
+
blockers.push(`the contract-wide propose interval has not elapsed — next open at height ${status.nextProposeHeight}`);
|
|
259
|
+
}
|
|
260
|
+
if (!status.poolOk) {
|
|
261
|
+
blockers.push("the pool is empty or the draw would round to zero sats");
|
|
262
|
+
}
|
|
263
|
+
return blockers;
|
|
264
|
+
}
|
|
265
|
+
/** When each window opens and closes, on the clock the contract counts. */
|
|
266
|
+
export function buildTimeline(story, params, height, clock) {
|
|
267
|
+
const votingOpensAt = story.createdAt + params.votingDelay;
|
|
268
|
+
const vetoEnd = story.voteEnd + params.vetoWindow;
|
|
269
|
+
const concludeDeadline = vetoEnd + params.concludeWindow;
|
|
270
|
+
const nextBoundary = [votingOpensAt, story.voteEnd, vetoEnd, concludeDeadline].find((boundary) => boundary > height);
|
|
271
|
+
return {
|
|
272
|
+
createdAt: story.createdAt,
|
|
273
|
+
votingOpensAt,
|
|
274
|
+
voteEnd: story.voteEnd,
|
|
275
|
+
vetoEnd,
|
|
276
|
+
concludeDeadline,
|
|
277
|
+
currentHeight: height,
|
|
278
|
+
clock,
|
|
279
|
+
blocksUntilNextTransition: nextBoundary === undefined ? null : nextBoundary - height,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* What `conclude` would decide if it ran at this height, following the
|
|
284
|
+
* contract's decision order exactly (integer division included, since Clarity
|
|
285
|
+
* floors every percentage).
|
|
286
|
+
*
|
|
287
|
+
* A story that is already terminal reports what the contract RECORDED, not a
|
|
288
|
+
* forecast. Forecasting a settled piece would be worse than useless: a passed
|
|
289
|
+
* proposal is necessarily past its conclude window, so the forecast branch
|
|
290
|
+
* would call every settled piece "expired".
|
|
291
|
+
*/
|
|
292
|
+
export function predictOutcome(story, params, poolBalance, height) {
|
|
293
|
+
const cast = story.yesWeight + story.noWeight;
|
|
294
|
+
const eligible = story.eligibleSnapshot;
|
|
295
|
+
const quorumPct = eligible > 0 ? Math.floor((cast * 100) / eligible) : 0;
|
|
296
|
+
const yesPct = cast > 0 ? Math.floor((story.yesWeight * 100) / cast) : 0;
|
|
297
|
+
const vetoPct = eligible > 0 ? Math.floor((story.vetoWeight * 100) / eligible) : 0;
|
|
298
|
+
const participantsMet = story.voterCount >= params.minParticipants;
|
|
299
|
+
const quorumMet = eligible > 0 && participantsMet && quorumPct >= params.votingQuorum;
|
|
300
|
+
const thresholdMet = cast > 0 && yesPct >= params.votingThreshold;
|
|
301
|
+
const vetoed = eligible > 0 && vetoPct >= params.vetoQuorum;
|
|
302
|
+
const base = {
|
|
303
|
+
cast,
|
|
304
|
+
quorumPct,
|
|
305
|
+
yesPct,
|
|
306
|
+
vetoPct,
|
|
307
|
+
quorumMet,
|
|
308
|
+
thresholdMet,
|
|
309
|
+
vetoed,
|
|
310
|
+
participantsMet,
|
|
311
|
+
};
|
|
312
|
+
// Already terminal: report the record.
|
|
313
|
+
if (story.status !== 0) {
|
|
314
|
+
const settledOutcome = story.status === 1 ? "passed" : story.status === 3 ? "expired" : "failed";
|
|
315
|
+
return {
|
|
316
|
+
...base,
|
|
317
|
+
settled: true,
|
|
318
|
+
outcome: settledOutcome,
|
|
319
|
+
reason: story.reason || settledOutcome,
|
|
320
|
+
payout: story.status === 1 ? story.draw : 0,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const concludeDeadline = story.voteEnd + params.vetoWindow + params.concludeWindow;
|
|
324
|
+
if (height >= concludeDeadline) {
|
|
325
|
+
return {
|
|
326
|
+
...base,
|
|
327
|
+
settled: false,
|
|
328
|
+
outcome: "expired",
|
|
329
|
+
reason: "not-concluded — the conclude window closed with nobody calling it",
|
|
330
|
+
payout: 0,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (vetoed) {
|
|
334
|
+
return { ...base, settled: false, outcome: "failed", reason: "vetoed", payout: 0 };
|
|
335
|
+
}
|
|
336
|
+
if (!quorumMet) {
|
|
337
|
+
return { ...base, settled: false, outcome: "failed", reason: "no-quorum", payout: 0 };
|
|
338
|
+
}
|
|
339
|
+
if (!thresholdMet) {
|
|
340
|
+
return { ...base, settled: false, outcome: "failed", reason: "voted-down", payout: 0 };
|
|
341
|
+
}
|
|
342
|
+
if (story.draw > poolBalance) {
|
|
343
|
+
return { ...base, settled: false, outcome: "failed", reason: "pool-short", payout: 0 };
|
|
344
|
+
}
|
|
345
|
+
return { ...base, settled: false, outcome: "passed", reason: "paid", payout: story.draw };
|
|
346
|
+
}
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Links and validation
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
/**
|
|
351
|
+
* Accept either a bare inscription id (`<txid>i0`) or a full URL, and return
|
|
352
|
+
* the link the contract should store.
|
|
353
|
+
*
|
|
354
|
+
* A bare id becomes the VIEWER url, not the content url: that is what every
|
|
355
|
+
* proposal already on chain uses, and it is what a voter wants from a click.
|
|
356
|
+
* The reader at legions.aibtc.news accepts either form and both bare ids.
|
|
357
|
+
*/
|
|
358
|
+
export function normalizeOrdinalsLink(input) {
|
|
359
|
+
const trimmed = input.trim();
|
|
360
|
+
if (/^[0-9a-f]{64}i\d+$/i.test(trimmed)) {
|
|
361
|
+
return `${ORDINALS_INSCRIPTION_BASE}${trimmed.toLowerCase()}`;
|
|
362
|
+
}
|
|
363
|
+
if (/^https?:\/\//i.test(trimmed))
|
|
364
|
+
return trimmed;
|
|
365
|
+
throw new Error(`"${input}" is neither an inscription id (<64-hex-txid>i0) nor a URL. ` +
|
|
366
|
+
`Pass the inscription id from legion_inscribe_reveal, or a full https:// link.`);
|
|
367
|
+
}
|
|
368
|
+
/** The inscription id inside an ordinals link, when there is one. */
|
|
369
|
+
export function inscriptionIdFromLink(link) {
|
|
370
|
+
const match = link.match(/([0-9a-f]{64}i\d+)/i);
|
|
371
|
+
return match ? match[1].toLowerCase() : null;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The raw-bytes url for a stored link, so a voter can read the piece rather
|
|
375
|
+
* than just look at where it lives. Null when the link names no inscription.
|
|
376
|
+
*/
|
|
377
|
+
export function contentUrlFromLink(link) {
|
|
378
|
+
const id = inscriptionIdFromLink(link);
|
|
379
|
+
return id ? `${ORDINALS_CONTENT_BASE}${id}` : null;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Clarity `string-ascii` holds one byte per character, so a smart quote or an
|
|
383
|
+
* em dash pasted into a headline is a revert, not a truncation. Catch it here
|
|
384
|
+
* with a message that names the character.
|
|
385
|
+
*/
|
|
386
|
+
export function assertAscii(value, field, maxLength) {
|
|
387
|
+
const offender = [...value].find((char) => char.charCodeAt(0) > 127);
|
|
388
|
+
if (offender) {
|
|
389
|
+
throw new Error(`${field} must be ASCII — "${offender}" (U+${offender
|
|
390
|
+
.charCodeAt(0)
|
|
391
|
+
.toString(16)
|
|
392
|
+
.toUpperCase()
|
|
393
|
+
.padStart(4, "0")}) is not. Clarity string-ascii cannot hold it.`);
|
|
394
|
+
}
|
|
395
|
+
if (value.length > maxLength) {
|
|
396
|
+
throw new Error(`${field} is ${value.length} characters; the contract accepts at most ${maxLength}.`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** `(err uXXX)` from a failed read or a broadcast rejection, as a sentence. */
|
|
400
|
+
export function explainError(error, errors) {
|
|
401
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
402
|
+
const match = text.match(/\bu(\d{3})\b/);
|
|
403
|
+
if (match) {
|
|
404
|
+
const explanation = errors[Number(match[1])];
|
|
405
|
+
if (explanation)
|
|
406
|
+
return `${text} — ${explanation}`;
|
|
407
|
+
}
|
|
408
|
+
return text;
|
|
409
|
+
}
|
|
410
|
+
//# sourceMappingURL=legion.service.js.map
|
package/dist/tools/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import { registerJingswapTools } from "./jingswap.tools.js";
|
|
|
36
36
|
import { registerSigningTools } from "./signing.tools.js";
|
|
37
37
|
import { registerInferenceMarketplaceTools } from "./inference-marketplace.tools.js";
|
|
38
38
|
import { registerNewsTools } from "./news.tools.js";
|
|
39
|
+
import { registerLegionTools } from "./legion.tools.js";
|
|
39
40
|
import { registerIdentityTools } from "./identity.tools.js";
|
|
40
41
|
import { registerCredentialsTools } from "./credentials.tools.js";
|
|
41
42
|
import { registerSouldinalsTools } from "./souldinals.tools.js";
|
|
@@ -167,6 +168,8 @@ export function registerAllTools(server) {
|
|
|
167
168
|
registerInferenceMarketplaceTools(server);
|
|
168
169
|
// AIBTC News (signal feed, leaderboard, file signals)
|
|
169
170
|
registerNewsTools(server);
|
|
171
|
+
// AIBTC News Legion (news-gov-v5 governance — inscribe, propose, vote, veto, conclude)
|
|
172
|
+
registerLegionTools(server);
|
|
170
173
|
// Identity (ERC-8004 on-chain agent identity management)
|
|
171
174
|
registerIdentityTools(server);
|
|
172
175
|
// Credentials (encrypted credential store — list, get, set, delete, unlock)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIBTC News Legion tools (news-gov-v5).
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle: inscribe → reveal → propose → vote → veto → conclude, plus
|
|
5
|
+
* contribute (buys weight) and sponsor (does not).
|
|
6
|
+
*
|
|
7
|
+
* These tools pin their own network: `getLegionAccount()` re-derives the
|
|
8
|
+
* unlocked wallet for the legion's chain, so a mainnet-configured server signs
|
|
9
|
+
* against testnet. Inscription is the exception — real BTC on whatever chain
|
|
10
|
+
* NETWORK names.
|
|
11
|
+
*
|
|
12
|
+
* Fund-moving calls sign in DENY mode with an exact post-condition.
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
|
+
export declare function registerLegionTools(server: McpServer): void;
|
|
16
|
+
//# sourceMappingURL=legion.tools.d.ts.map
|