@odatano/nightgate-mcp 0.17.1 → 0.18.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.
- package/CHANGELOG.md +70 -0
- package/README.md +17 -8
- package/dist/builder.js +7 -19
- package/dist/builder.js.map +1 -1
- package/dist/client.d.ts +5 -1
- package/dist/client.js +12 -3
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +12 -8
- package/dist/config.js +30 -3
- package/dist/config.js.map +1 -1
- package/dist/factory.js +5 -13
- package/dist/factory.js.map +1 -1
- package/dist/jobs.d.ts +15 -0
- package/dist/jobs.js +116 -0
- package/dist/jobs.js.map +1 -0
- package/dist/seed.d.ts +11 -0
- package/dist/seed.js +75 -0
- package/dist/seed.js.map +1 -0
- package/dist/server.js +1 -1
- package/dist/swap.d.ts +2 -0
- package/dist/swap.js +6 -3
- package/dist/swap.js.map +1 -1
- package/dist/tools.d.ts +4 -7
- package/dist/tools.js +318 -796
- package/dist/tools.js.map +1 -1
- package/dist/tx-module.js +2 -6
- package/dist/tx-module.js.map +1 -1
- package/docs/configuration.md +11 -4
- package/docs/tools.md +102 -37
- package/package.json +3 -7
package/dist/tools.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { int64, raw, NightgateApiError } from './client.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { isGatewayKey, vaultOf } from './config.js';
|
|
5
|
+
import { BUILDABLE_CALLS, BUILDABLE_ARTIFACTS, DEFAULT_ARTIFACT, buildSponsorable, attesterIdentity } from './builder.js';
|
|
6
|
+
import { buildOffer, halfJobIds, postedOfferIds, readOffer, revertHalf, sameOffer, settleBoardRows, settleHalfJob, swapWalletInfo, takenHalves, takeOffer, trackHalfJob, trackPostedHalf, } from './swap.js';
|
|
6
7
|
import { buildMint } from './factory.js';
|
|
7
8
|
import { resolveTxSource } from './built.js';
|
|
9
|
+
import { awaitJob, forgetStart, jobSession, startOnce } from './jobs.js';
|
|
10
|
+
import { seedCreated, seedNotice } from './seed.js';
|
|
8
11
|
const HEX64 = /^[0-9a-fA-F]{64}$/;
|
|
9
12
|
const hex64 = (what) => z.string().regex(HEX64, `${what} must be 64 hex characters`);
|
|
10
13
|
/** Scaled non-negative integer, as string (preferred, exact) or JS number; a number stays below 2^53 so String() renders it exactly. */
|
|
@@ -12,19 +15,6 @@ const scaledInt = (what) => z.union([
|
|
|
12
15
|
z.string().regex(/^\d+$/, `${what} must be a non-negative integer (decimal string)`),
|
|
13
16
|
z.number().int().nonnegative().safe(),
|
|
14
17
|
]);
|
|
15
|
-
const merkleSiblings = z.array(hex64('sibling')).length(4)
|
|
16
|
-
.describe('Depth-4 Merkle inclusion path: exactly 4 sibling digests (64 hex each)');
|
|
17
|
-
const merkleDirs = z.array(z.boolean()).length(4)
|
|
18
|
-
.describe('Exactly 4 booleans; true means the current node is the LEFT child');
|
|
19
|
-
const setSiblings = z.array(hex64('setSibling')).length(6)
|
|
20
|
-
.describe('Depth-6 membership-set path: exactly 6 sibling digests (64 hex each)');
|
|
21
|
-
const setDirs = z.array(z.boolean()).length(6)
|
|
22
|
-
.describe('Exactly 6 booleans; true means the current node is the LEFT child');
|
|
23
|
-
const POLL_HINT = 'Async: returns { jobId, status } immediately; call get_job_status, which waits on the server until the job settles.';
|
|
24
|
-
/** How long one get_job_status call lets the server wait for the job to settle, in ms. */
|
|
25
|
-
const JOB_WAIT_MS = 20_000;
|
|
26
|
-
// Off for this process once a server below NIGHTGATE 0.37 refuses getJobStatus' waitMs.
|
|
27
|
-
let jobWait = true;
|
|
28
18
|
/** One side of a swap: a raw token type and an amount in atoms. */
|
|
29
19
|
const swapLeg = (what) => z.object({
|
|
30
20
|
tokenType: hex64(`${what}.tokenType`).describe('Raw token type (64 hex), what derive_token_type returns'),
|
|
@@ -47,136 +37,74 @@ const OFFER_STATUS = z.enum(['open', 'filled', 'retired', 'expired', 'all']);
|
|
|
47
37
|
const BOARD_ROW_HINT = 'Each row: offerId, the offer half (feed it to read_swap_offer or take_swap_offer), gives/wants ' +
|
|
48
38
|
'type and amount, tags, expiresAt, postedAt, status, filledTxHash, closedAt, changedAt. Never the poster or the ' +
|
|
49
39
|
"half's nullifiers.";
|
|
50
|
-
/**
|
|
51
|
-
* Per-slot salt of a proof field. Content-tree leaves are SALTED, so every
|
|
52
|
-
* single-field proof needs the slot's salt from prepare_document_proof
|
|
53
|
-
* (field `salt`; batch claims carry it as `salt` too). Witness material.
|
|
54
|
-
*/
|
|
55
|
-
const fieldSalt = hex64('fieldSalt')
|
|
56
|
-
.describe('Per-slot salt of this field, from prepare_document_proof (fields[].salt). Witness material, never publish');
|
|
57
|
-
/** The shared 16-slot schema descriptor list from prepare_document_proof. */
|
|
58
|
-
/**
|
|
59
|
-
* Content-tree slot widths NIGHTGATE ships: 16 (`attestation-vault`) and 32
|
|
60
|
-
* (`attestation-vault-32`). A document uses exactly one of them, picked by
|
|
61
|
-
* the `compiledArtifactRef` it is anchored under. These bounds only keep
|
|
62
|
-
* obvious nonsense out; the SERVER knows the registered width of the actual
|
|
63
|
-
* artifact and rejects a mismatch with a message naming it.
|
|
64
|
-
*/
|
|
65
|
-
const SLOT_WIDTHS = [16, 32];
|
|
66
40
|
const MAX_SLOT_WIDTH = 32;
|
|
41
|
+
const RETRY_HINT = 'If the call was cut off or came back pending, run it again with the same arguments: ' +
|
|
42
|
+
'it picks up the job it started and submits nothing new.';
|
|
43
|
+
const WAIT_HINT = 'The tool waits until the job settled (about 1 minute) and returns { status, txHash, transactionHash, result }. ' +
|
|
44
|
+
'failed: `next` says what to do. ' + RETRY_HINT;
|
|
67
45
|
const MAX_MASK = 0xffffffff;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* (non-padding) slot of the schema proves nothing at all. Checking it
|
|
73
|
-
* against the SCHEMA rather than against a fixed all-ones constant is what
|
|
74
|
-
* makes it width-independent, and it also catches the case a constant never
|
|
75
|
-
* could, a mask that frees every real slot of a SHORT schema.
|
|
76
|
-
*/
|
|
77
|
-
function maskFreesEveryRealSlot(allowedMask, schema) {
|
|
78
|
-
return schema.every((s, i) => s.kind === 2 || (allowedMask & (1 << i)) !== 0);
|
|
46
|
+
/** Names one request, so a repeat of it is recognised; witness and bytes enter only as a hash. */
|
|
47
|
+
function callKey(...parts) {
|
|
48
|
+
return createHash('sha256').update(JSON.stringify(parts, (_k, v) => (v && typeof v === 'object' && !Array.isArray(v)
|
|
49
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b))) : v))).digest('hex');
|
|
79
50
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const membershipClaim = z.object({
|
|
124
|
-
predicate: z.literal('setMembership'),
|
|
125
|
-
fieldKey: hex64('fieldKey'),
|
|
126
|
-
value: z.string().min(1).optional().describe('Raw member string (witness only)'),
|
|
127
|
-
valueDigest: hex64('valueDigest').optional().describe('blake2b-256 of the exact member string (witness only)'),
|
|
128
|
-
allowedValues: z.array(z.string().min(1)).min(1).max(64).optional()
|
|
129
|
-
.describe('The public allow-list; the server builds the canonical set root + path'),
|
|
130
|
-
setRoot: hex64('setRoot').optional().describe('Precomputed canonical set root (64 hex)'),
|
|
131
|
-
setSiblings: setSiblings.optional(),
|
|
132
|
-
setDirs: setDirs.optional(),
|
|
133
|
-
salt: fieldSalt,
|
|
134
|
-
siblings: merkleSiblings,
|
|
135
|
-
dirs: merkleDirs,
|
|
136
|
-
}).superRefine((c, ctx) => {
|
|
137
|
-
if (!!c.value === !!c.valueDigest) {
|
|
138
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'pass exactly one of value / valueDigest' });
|
|
139
|
-
}
|
|
140
|
-
const hasList = c.allowedValues !== undefined;
|
|
141
|
-
const hasPath = !!(c.setRoot || c.setSiblings || c.setDirs);
|
|
142
|
-
if (hasList && hasPath) {
|
|
143
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'pass either allowedValues or setRoot + setSiblings + setDirs, not both' });
|
|
144
|
-
}
|
|
145
|
-
if (!hasList && !(c.setRoot && c.setSiblings && c.setDirs)) {
|
|
146
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'allowedValues or setRoot + setSiblings + setDirs is required' });
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
/** Cross-root claim shapes: they relate the batch payload hash (document A) to a second document. */
|
|
150
|
-
const documentIntegrityClaim = z.object({
|
|
151
|
-
predicate: z.literal('documentIntegrity'),
|
|
152
|
-
payloadHashB: hex64('payloadHashB').describe('The second document (A is the batch payloadHash)'),
|
|
153
|
-
attesterIdB: hex64('attesterIdB').optional().describe('Document B\'s attester; default the batch attester'),
|
|
154
|
-
allowedMask: z.number().int().min(0).max(MAX_MASK - 1)
|
|
155
|
-
.describe('Packed slot mask, one bit per slot of the vault width (16 bits by default, 32 on attestation-vault-32), bit i = slot i MAY differ. Must leave at least one real schema slot constrained, otherwise the claim is vacuous and rejected'),
|
|
156
|
-
schema: schemaSlots,
|
|
157
|
-
openingA: documentOpening,
|
|
158
|
-
openingB: documentOpening,
|
|
159
|
-
}).superRefine((c, ctx) => {
|
|
160
|
-
if (maskFreesEveryRealSlot(c.allowedMask, c.schema)) {
|
|
161
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: VACUOUS_MASK_MESSAGE });
|
|
51
|
+
/** The free verify call that shows a vault call's effect on-chain; none when the call names its record by key only. */
|
|
52
|
+
function checkFor(input, ownAttesterId) {
|
|
53
|
+
const p = input.params;
|
|
54
|
+
const hex = (k) => (typeof p[k] === 'string' ? String(p[k]).toLowerCase() : undefined);
|
|
55
|
+
const base = { contractAddress: input.contractAddress, compiledArtifactRef: input.compiledArtifactRef ?? DEFAULT_ARTIFACT };
|
|
56
|
+
const record = (payloadHash, attesterId) => payloadHash ? { ...base, attesterId: attesterId ?? ownAttesterId, payloadHash } : undefined;
|
|
57
|
+
const claim = (args) => (args ? { tool: 'verify_predicate', args } : undefined);
|
|
58
|
+
switch (input.call) {
|
|
59
|
+
case 'attest':
|
|
60
|
+
case 'anchorContentRoot':
|
|
61
|
+
case 'grantDisclosure':
|
|
62
|
+
case 'revokeDisclosure':
|
|
63
|
+
case 'bindPassport': {
|
|
64
|
+
const r = record(hex('payloadHash'));
|
|
65
|
+
if (!r)
|
|
66
|
+
return undefined;
|
|
67
|
+
const root = input.call === 'anchorContentRoot' ? { contentRoot: hex('contentRoot'), schemaId: hex('schemaId') } : {};
|
|
68
|
+
return { tool: 'verify_attestation', args: { ...r, ...root } };
|
|
69
|
+
}
|
|
70
|
+
case 'proveFieldPredicate': {
|
|
71
|
+
const r = record(hex('payloadHash'), hex('attesterId'));
|
|
72
|
+
return claim(r && { ...r, predicate: Number(p.op) === 1 ? 'greaterOrEqual' : 'lessOrEqual', fieldKey: hex('fieldKey'), threshold: String(p.threshold) });
|
|
73
|
+
}
|
|
74
|
+
case 'proveFieldEquality': {
|
|
75
|
+
const r = record(hex('payloadHash'), hex('attesterId'));
|
|
76
|
+
return claim(r && { ...r, predicate: 'bytesEquality', fieldKey: hex('fieldKey'), expectedDigest: hex('expectedDigest') });
|
|
77
|
+
}
|
|
78
|
+
case 'proveFieldMembership': {
|
|
79
|
+
const r = record(hex('payloadHash'), hex('attesterId'));
|
|
80
|
+
return claim(r && { ...r, predicate: 'setMembership', fieldKey: hex('fieldKey'), setRoot: hex('setRoot') });
|
|
81
|
+
}
|
|
82
|
+
case 'proveFieldsUnchangedExcept':
|
|
83
|
+
case 'proveFieldsDiffer': {
|
|
84
|
+
const r = record(hex('payloadHashA'), hex('attesterIdA'));
|
|
85
|
+
if (!r || !hex('payloadHashB'))
|
|
86
|
+
return undefined;
|
|
87
|
+
const extraArgs = input.call === 'proveFieldsUnchangedExcept'
|
|
88
|
+
? { predicate: 'documentIntegrity', allowedMask: Number(p.allowedMask) }
|
|
89
|
+
: { predicate: 'documentDiff', k: Number(p.k) };
|
|
90
|
+
return claim({ ...r, payloadHashB: hex('payloadHashB'), attesterIdB: hex('attesterIdB'), ...extraArgs });
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
return undefined;
|
|
162
94
|
}
|
|
163
|
-
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
attesterIdB: hex64('attesterIdB').optional().describe('Document B\'s attester; default the batch attester'),
|
|
168
|
-
k: z.number().int().min(1).max(MAX_SLOT_WIDTH).describe('Minimum number of differing slots to prove, up to the vault width'),
|
|
169
|
-
schema: schemaSlots,
|
|
170
|
-
openingA: documentOpening,
|
|
171
|
-
openingB: documentOpening,
|
|
172
|
-
});
|
|
173
|
-
/**
|
|
174
|
-
* Phase A tool set: crawler-free verification, job polling, and the
|
|
175
|
-
* curated write path (anchoring, field predicates, disclosure).
|
|
176
|
-
* Wallet lifecycle actions are deliberately never exposed over MCP.
|
|
177
|
-
*/
|
|
95
|
+
}
|
|
96
|
+
const CONTRACT = z.string().regex(HEX64, 'contractAddress must be 64 hex characters').optional()
|
|
97
|
+
.describe('AttestationVault address (64 hex); default the shared sandbox vault of the network (preprod, preview)');
|
|
98
|
+
/** Every NIGHTGATE tool. Writes are built here with the caller seed and paid by a sponsor; no tool needs a server wallet. */
|
|
178
99
|
export function registerTools(server, client, config) {
|
|
179
100
|
const run = wrapHandler(client);
|
|
101
|
+
const vault = (contractAddress, compiledArtifactRef) => {
|
|
102
|
+
if (config)
|
|
103
|
+
return vaultOf(config, contractAddress, compiledArtifactRef);
|
|
104
|
+
if (!contractAddress)
|
|
105
|
+
throw new Error('contractAddress is required');
|
|
106
|
+
return contractAddress.toLowerCase();
|
|
107
|
+
};
|
|
180
108
|
server.registerTool('verify_attestation', {
|
|
181
109
|
description: 'Verify against LIVE Midnight contract state that an attester\'s record of a payload ' +
|
|
182
110
|
'hash stands in an AttestationVault (crawler-free, no txHash needed). A record is ONE ' +
|
|
@@ -184,9 +112,12 @@ export function registerTools(server, client, config) {
|
|
|
184
112
|
'name it by attesterId + payloadHash, or by a bound documentId, which resolves to exactly ' +
|
|
185
113
|
'one record and reveals its attester. Optionally also checks that the anchored content ' +
|
|
186
114
|
'root / schema id match. Returns verified:false (not an error) when absent; the result ' +
|
|
187
|
-
'carries attesterId, payloadHash, recordKey and documentId.'
|
|
115
|
+
'carries attesterId, payloadHash, recordKey and documentId. bindingRegistered is only ' +
|
|
116
|
+
'about documentId lookups: true when the vault registrar assigned that id to this ' +
|
|
117
|
+
'attester. A record anchored without a document id reports false, and that is fine. ' +
|
|
118
|
+
'Free: it reads the public verify service.',
|
|
188
119
|
inputSchema: {
|
|
189
|
-
contractAddress:
|
|
120
|
+
contractAddress: CONTRACT,
|
|
190
121
|
attesterId: hex64('attesterId').optional()
|
|
191
122
|
.describe('The attester whose record to check (64 hex; get_attester_identity for this server\'s own). Required unless documentId is given'),
|
|
192
123
|
payloadHash: hex64('payloadHash').optional()
|
|
@@ -204,8 +135,8 @@ export function registerTools(server, client, config) {
|
|
|
204
135
|
if (!args.documentId && !(args.attesterId && args.payloadHash)) {
|
|
205
136
|
throw new Error('name the record: attesterId + payloadHash, or documentId');
|
|
206
137
|
}
|
|
207
|
-
return client.
|
|
208
|
-
contractAddress: args.contractAddress,
|
|
138
|
+
return client.callVerifyFunction('verifyAttestationState', {
|
|
139
|
+
contractAddress: vault(args.contractAddress, args.compiledArtifactRef),
|
|
209
140
|
attesterId: args.attesterId,
|
|
210
141
|
payloadHash: args.payloadHash,
|
|
211
142
|
documentId: args.documentId,
|
|
@@ -222,9 +153,10 @@ export function registerTools(server, client, config) {
|
|
|
222
153
|
'mismatch yields verified:false) and optionally fieldKey for field-bound proofs. ' +
|
|
223
154
|
'bytesEquality needs fieldKey + expectedDigest; setMembership needs fieldKey + setRoot ' +
|
|
224
155
|
'(recompute it from the published list via prepare_membership_set). Every claim is bound ' +
|
|
225
|
-
'to ONE attester\'s record of the document, so attesterId is part of the coordinates.'
|
|
156
|
+
'to ONE attester\'s record of the document, so attesterId is part of the coordinates. ' +
|
|
157
|
+
'Free: it reads the public verify service.',
|
|
226
158
|
inputSchema: {
|
|
227
|
-
contractAddress:
|
|
159
|
+
contractAddress: CONTRACT,
|
|
228
160
|
attesterId: hex64('attesterId').describe('The attester whose record of payloadHash carries the claim (64 hex)'),
|
|
229
161
|
payloadHash: hex64('payloadHash').describe('The attestation payload hash (64 hex)'),
|
|
230
162
|
predicate: z.enum(['lessOrEqual', 'greaterOrEqual', 'bytesEquality', 'setMembership',
|
|
@@ -265,8 +197,8 @@ export function registerTools(server, client, config) {
|
|
|
265
197
|
if (args.predicate === 'documentDiff' && (!args.payloadHashB || args.k === undefined)) {
|
|
266
198
|
throw new Error("predicate 'documentDiff' requires payloadHashB and k");
|
|
267
199
|
}
|
|
268
|
-
return client.
|
|
269
|
-
contractAddress: args.contractAddress,
|
|
200
|
+
return client.callVerifyFunction('verifyPredicateState', {
|
|
201
|
+
contractAddress: vault(args.contractAddress, args.compiledArtifactRef),
|
|
270
202
|
attesterId: args.attesterId,
|
|
271
203
|
payloadHash: args.payloadHash,
|
|
272
204
|
attesterIdB: args.attesterIdB,
|
|
@@ -282,37 +214,11 @@ export function registerTools(server, client, config) {
|
|
|
282
214
|
network: args.network,
|
|
283
215
|
});
|
|
284
216
|
}));
|
|
285
|
-
server.registerTool('verify_predicate_attestation', {
|
|
286
|
-
description: 'Verify a server-issued predicate attestation by its NIGHTGATE row id (UUID). ' +
|
|
287
|
-
'Confirms the proving transaction succeeded, with a crawler-free live-state fallback. ' +
|
|
288
|
-
'Use verify_predicate instead when you only have on-chain coordinates.',
|
|
289
|
-
inputSchema: {
|
|
290
|
-
predicateAttestationId: z.string().uuid().describe('PredicateAttestations row id'),
|
|
291
|
-
},
|
|
292
|
-
}, run(async (args) => client.callFunction('verifyPredicateAttestation', {
|
|
293
|
-
predicateAttestationId: args.predicateAttestationId,
|
|
294
|
-
})));
|
|
295
|
-
server.registerTool('verify_document', {
|
|
296
|
-
description: 'Verify an anchored document by its NIGHTGATE document id: compares the provided sha256 ' +
|
|
297
|
-
'against the anchored original and confirms the anchoring transaction, with a crawler-free ' +
|
|
298
|
-
'live-state fallback when contractAddress is supplied.',
|
|
299
|
-
inputSchema: {
|
|
300
|
-
documentId: z.string().uuid().describe('Documents row id'),
|
|
301
|
-
providedSha256: hex64('providedSha256').describe('sha256 of the document to check (64 hex)'),
|
|
302
|
-
contractAddress: z.string().optional().describe('Optional vault address, enables the crawler-free fallback'),
|
|
303
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
304
|
-
},
|
|
305
|
-
}, run(async (args) => client.callFunction('verifyDocument', {
|
|
306
|
-
documentId: args.documentId,
|
|
307
|
-
providedSha256: args.providedSha256,
|
|
308
|
-
contractAddress: args.contractAddress,
|
|
309
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
310
|
-
})));
|
|
311
217
|
server.registerTool('prepare_document_proof', {
|
|
312
218
|
description: 'Turn a structured document into everything the proof tools need: canonical JSON and its ' +
|
|
313
219
|
'payloadHash (what anchor_document anchors), a Merkle contentRoot over an ORDERED list of ' +
|
|
314
|
-
'up to 16 proof fields, and per-field inclusion paths ready for
|
|
315
|
-
'
|
|
220
|
+
'up to 16 proof fields, and per-field inclusion paths ready for the proveField* calls of ' +
|
|
221
|
+
'submit_vault_call. kind "uint" (default) scales a numeric ' +
|
|
316
222
|
'value; kind "bytes" (NIGHTGATE >= 0.16.0) enters a STRING field as the digest of the ' +
|
|
317
223
|
'exact string, feeding the equality/membership proofs. Keep the field order stable across ' +
|
|
318
224
|
'anchor and proof: it is part of the tree identity. Compute-only and synchronous, nothing ' +
|
|
@@ -345,117 +251,6 @@ export function registerTools(server, client, config) {
|
|
|
345
251
|
saltSeed: args.saltSeed,
|
|
346
252
|
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
347
253
|
})));
|
|
348
|
-
server.registerTool('attest_agent_output', {
|
|
349
|
-
description: 'Anchor agent-output provenance on the Midnight chain: "agent X produced output O from ' +
|
|
350
|
-
'input I at time T". Builds the canonical v1 envelope server-side, hashes it and anchors ' +
|
|
351
|
-
'it; the response returns the envelopeJson any third party can re-hash and check via ' +
|
|
352
|
-
'verify_attestation, without trusting this server. ' + POLL_HINT,
|
|
353
|
-
inputSchema: {
|
|
354
|
-
agentId: z.string().min(1).max(200).describe('Agent identity, ideally a registered grantee id'),
|
|
355
|
-
inputHash: hex64('inputHash').describe('Commitment to the agent input (64 hex)'),
|
|
356
|
-
outputHash: hex64('outputHash').describe('Commitment to the produced output (64 hex)'),
|
|
357
|
-
sessionId: z.string().uuid().describe('Wallet session id that signs and submits'),
|
|
358
|
-
contractAddress: z.string().min(1).describe('AttestationVault deployment'),
|
|
359
|
-
modelId: z.string().max(200).optional().describe('Optional model identifier'),
|
|
360
|
-
policyHash: hex64('policyHash').optional().describe('Optional commitment to the governing policy (64 hex)'),
|
|
361
|
-
producedAt: z.string().datetime().optional().describe('Optional ISO-8601 production time, defaults to now'),
|
|
362
|
-
storageRef: z.string().optional().describe('Where output/envelope live, defaults to agent-output://<agentId>'),
|
|
363
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
364
|
-
idempotencyKey: z.string().optional().describe('Dedupes retries'),
|
|
365
|
-
sponsorSessionId: z.string().uuid().optional().describe('Optional second session that pays the dust fee'),
|
|
366
|
-
},
|
|
367
|
-
}, run(async (args) => client.callAction('attestAgentOutput', {
|
|
368
|
-
agentId: args.agentId,
|
|
369
|
-
inputHash: args.inputHash,
|
|
370
|
-
outputHash: args.outputHash,
|
|
371
|
-
modelId: args.modelId,
|
|
372
|
-
policyHash: args.policyHash,
|
|
373
|
-
producedAt: args.producedAt,
|
|
374
|
-
storageRef: args.storageRef,
|
|
375
|
-
sessionId: args.sessionId,
|
|
376
|
-
contractAddress: args.contractAddress,
|
|
377
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
378
|
-
idempotencyKey: args.idempotencyKey,
|
|
379
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
380
|
-
})));
|
|
381
|
-
server.registerTool('anchor_document', {
|
|
382
|
-
description: 'Anchor a document content hash on the Midnight chain via the AttestationVault attest ' +
|
|
383
|
-
'circuit (one transaction). Commits only the sha256 + public metadata; you are responsible ' +
|
|
384
|
-
'for storing the actual bytes at storageRef. The on-chain record is keyed by the session\'s ' +
|
|
385
|
-
'attester id AND the hash, so nobody can pre-empt or take over it; the same hash anchored ' +
|
|
386
|
-
'by another identity is a separate record. ' + POLL_HINT +
|
|
387
|
-
' Also returns documentId for verify_document and attesterId, which verifiers need next to the hash.',
|
|
388
|
-
inputSchema: {
|
|
389
|
-
sha256: hex64('sha256').describe('sha256 of the document content (64 hex), becomes the on-chain payload hash'),
|
|
390
|
-
storageRef: z.string().min(1).describe('Where the bytes live, e.g. file://, s3://, ipfs://'),
|
|
391
|
-
sessionId: z.string().uuid().describe('Wallet session id that signs and submits'),
|
|
392
|
-
contractAddress: z.string().min(1).describe('AttestationVault deployment to anchor into'),
|
|
393
|
-
contentType: z.string().optional().describe('MIME type, informational'),
|
|
394
|
-
size: z.number().int().nonnegative().optional().describe('Content size in bytes, informational'),
|
|
395
|
-
metadata: z.record(z.unknown()).optional().describe('Public metadata object; its hash is anchored alongside'),
|
|
396
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
397
|
-
idempotencyKey: z.string().optional().describe('Dedupes retries of the same anchor request'),
|
|
398
|
-
sponsorSessionId: z.string().uuid().optional().describe('Optional second session that pays the dust fee'),
|
|
399
|
-
},
|
|
400
|
-
}, run(async (args) => client.callAction('anchorDocument', {
|
|
401
|
-
sha256: args.sha256,
|
|
402
|
-
contentType: args.contentType,
|
|
403
|
-
size: args.size,
|
|
404
|
-
storageRef: args.storageRef,
|
|
405
|
-
metadata: args.metadata === undefined ? undefined : JSON.stringify(args.metadata),
|
|
406
|
-
sessionId: args.sessionId,
|
|
407
|
-
contractAddress: args.contractAddress,
|
|
408
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
409
|
-
idempotencyKey: args.idempotencyKey,
|
|
410
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
411
|
-
})));
|
|
412
|
-
server.registerTool('prove_field_predicate', {
|
|
413
|
-
description: 'Issue a zero-knowledge field-bound predicate proof: prove that a hidden field of an ' +
|
|
414
|
-
'anchored document satisfies "value <= threshold" or "value >= threshold" WITHOUT revealing ' +
|
|
415
|
-
'the value. Needs the depth-4 Merkle inclusion path of the field in the anchored content ' +
|
|
416
|
-
'root. value/threshold are scaled integers (decimal strings); the value is a witness and ' +
|
|
417
|
-
'never persisted. If contentRoot is supplied it is anchored first. A false predicate fails ' +
|
|
418
|
-
'at local proving time, nothing is submitted. ' + POLL_HINT,
|
|
419
|
-
inputSchema: {
|
|
420
|
-
payloadHash: hex64('payloadHash').describe('Attestation payload hash (64 hex)'),
|
|
421
|
-
attesterId: hex64('attesterId').optional()
|
|
422
|
-
.describe('The attester whose record of payloadHash the claim is bound to (64 hex); default the session\'s own. A contentRoot can only be anchored under the session\'s own record'),
|
|
423
|
-
fieldKey: hex64('fieldKey').describe('Canonical field id (64 hex, public)'),
|
|
424
|
-
value: z.string().regex(/^\d+$/, 'value must be a non-negative integer (decimal string)')
|
|
425
|
-
.describe('Scaled integer field value (witness only, never persisted)'),
|
|
426
|
-
fieldSalt,
|
|
427
|
-
siblings: merkleSiblings,
|
|
428
|
-
dirs: merkleDirs,
|
|
429
|
-
predicate: z.enum(['lessOrEqual', 'greaterOrEqual']).describe('Predicate operator'),
|
|
430
|
-
threshold: scaledInt('threshold').describe('Scaled integer threshold (same scaling as value)'),
|
|
431
|
-
sessionId: z.string().uuid().describe('Wallet session id that signs and submits'),
|
|
432
|
-
contractAddress: z.string().min(1).describe('AttestationVault deployment'),
|
|
433
|
-
contentRoot: hex64('contentRoot').optional().describe('Optional Merkle root (64 hex) to anchor first'),
|
|
434
|
-
schemaId: hex64('schemaId').optional().describe('Schema id of the field list, required whenever contentRoot is supplied'),
|
|
435
|
-
unit: z.string().optional().describe('Informational unit, e.g. kWh'),
|
|
436
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
437
|
-
idempotencyKey: z.string().optional().describe('Dedupes retries'),
|
|
438
|
-
sponsorSessionId: z.string().uuid().optional().describe('Optional second session that pays the dust fee'),
|
|
439
|
-
},
|
|
440
|
-
}, run(async (args) => client.callAction('issueFieldPredicateAttestation', {
|
|
441
|
-
payloadHash: args.payloadHash,
|
|
442
|
-
attesterId: args.attesterId,
|
|
443
|
-
fieldKey: args.fieldKey,
|
|
444
|
-
value: args.value,
|
|
445
|
-
fieldSalt: args.fieldSalt,
|
|
446
|
-
contentRoot: args.contentRoot,
|
|
447
|
-
schemaId: args.schemaId,
|
|
448
|
-
siblings: args.siblings,
|
|
449
|
-
dirs: args.dirs,
|
|
450
|
-
predicate: args.predicate,
|
|
451
|
-
threshold: String(args.threshold),
|
|
452
|
-
unit: args.unit,
|
|
453
|
-
sessionId: args.sessionId,
|
|
454
|
-
contractAddress: args.contractAddress,
|
|
455
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
456
|
-
idempotencyKey: args.idempotencyKey,
|
|
457
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
458
|
-
})));
|
|
459
254
|
server.registerTool('prepare_membership_set', {
|
|
460
255
|
description: 'Build the canonical depth-6 membership-set tree over a public allow-list (NIGHTGATE ' +
|
|
461
256
|
'>= 0.16.0). Deterministic rule (digest each exact string, dedupe, sort ascending, pad by ' +
|
|
@@ -483,311 +278,111 @@ export function registerTools(server, client, config) {
|
|
|
483
278
|
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
484
279
|
});
|
|
485
280
|
}));
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}, run(async (args) => {
|
|
514
|
-
if (!!args.expectedValue === !!args.expectedDigest) {
|
|
515
|
-
throw new Error('pass exactly one of expectedValue / expectedDigest');
|
|
516
|
-
}
|
|
517
|
-
return client.callAction('issueFieldEqualityAttestation', {
|
|
518
|
-
payloadHash: args.payloadHash,
|
|
519
|
-
attesterId: args.attesterId,
|
|
520
|
-
fieldKey: args.fieldKey,
|
|
521
|
-
expectedValue: args.expectedValue,
|
|
522
|
-
expectedDigest: args.expectedDigest,
|
|
523
|
-
fieldSalt: args.fieldSalt,
|
|
524
|
-
contentRoot: args.contentRoot,
|
|
525
|
-
schemaId: args.schemaId,
|
|
526
|
-
siblings: args.siblings,
|
|
527
|
-
dirs: args.dirs,
|
|
528
|
-
sessionId: args.sessionId,
|
|
529
|
-
contractAddress: args.contractAddress,
|
|
530
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
531
|
-
idempotencyKey: args.idempotencyKey,
|
|
532
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
533
|
-
});
|
|
534
|
-
}));
|
|
535
|
-
server.registerTool('prove_field_membership', {
|
|
536
|
-
description: 'Issue a ZK set-membership proof (NIGHTGATE >= 0.16.0): the anchored document field\'s ' +
|
|
537
|
-
'HIDDEN value is one of a public allow-list, without revealing which one. Supply the ' +
|
|
538
|
-
'allow-list directly (allowedValues: the server builds the canonical set root + path and ' +
|
|
539
|
-
'rejects a non-member with 400 BEFORE any proving) or a precomputed setRoot + setSiblings ' +
|
|
540
|
-
'+ setDirs from prepare_membership_set. The field must be a kind:"bytes" leaf from ' +
|
|
541
|
-
'prepare_document_proof; value/valueDigest stay witness material, never persisted. If ' +
|
|
542
|
-
'contentRoot is supplied it is anchored first. ' + POLL_HINT,
|
|
543
|
-
inputSchema: {
|
|
544
|
-
payloadHash: hex64('payloadHash').describe('Attestation payload hash (64 hex)'),
|
|
545
|
-
attesterId: hex64('attesterId').optional()
|
|
546
|
-
.describe('The attester whose record of payloadHash the claim is bound to (64 hex); default the session\'s own. A contentRoot can only be anchored under the session\'s own record'),
|
|
547
|
-
fieldKey: hex64('fieldKey').describe('Canonical field id (64 hex, public)'),
|
|
548
|
-
value: z.string().min(1).optional()
|
|
549
|
-
.describe('Raw member string (witness only; pass this OR valueDigest)'),
|
|
550
|
-
valueDigest: hex64('valueDigest').optional()
|
|
551
|
-
.describe('blake2b-256 of the exact member string (witness only)'),
|
|
552
|
-
allowedValues: z.array(z.string().min(1)).min(1).max(64).optional()
|
|
553
|
-
.describe('The public allow-list; pass this OR setRoot + setSiblings + setDirs'),
|
|
554
|
-
setRoot: hex64('setRoot').optional().describe('Precomputed canonical set root (64 hex)'),
|
|
555
|
-
setSiblings: setSiblings.optional(),
|
|
556
|
-
setDirs: setDirs.optional(),
|
|
557
|
-
fieldSalt,
|
|
558
|
-
siblings: merkleSiblings,
|
|
559
|
-
dirs: merkleDirs,
|
|
560
|
-
sessionId: z.string().uuid().describe('Wallet session id that signs and submits'),
|
|
561
|
-
contractAddress: z.string().min(1).describe('AttestationVault deployment'),
|
|
562
|
-
contentRoot: hex64('contentRoot').optional().describe('Optional Merkle root (64 hex) to anchor first'),
|
|
563
|
-
schemaId: hex64('schemaId').optional().describe('Schema id of the field list, required whenever contentRoot is supplied'),
|
|
564
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
565
|
-
idempotencyKey: z.string().optional().describe('Dedupes retries'),
|
|
566
|
-
sponsorSessionId: z.string().uuid().optional().describe('Optional second session that pays the dust fee'),
|
|
567
|
-
},
|
|
568
|
-
}, run(async (args) => {
|
|
569
|
-
if (!!args.value === !!args.valueDigest) {
|
|
570
|
-
throw new Error('pass exactly one of value / valueDigest');
|
|
571
|
-
}
|
|
572
|
-
const hasList = args.allowedValues !== undefined;
|
|
573
|
-
const hasPath = !!(args.setRoot || args.setSiblings || args.setDirs);
|
|
574
|
-
if (hasList && hasPath)
|
|
575
|
-
throw new Error('pass either allowedValues or setRoot + setSiblings + setDirs, not both');
|
|
576
|
-
if (!hasList && !(args.setRoot && args.setSiblings && args.setDirs)) {
|
|
577
|
-
throw new Error('allowedValues or setRoot + setSiblings + setDirs is required');
|
|
578
|
-
}
|
|
579
|
-
return client.callAction('issueFieldMembershipAttestation', {
|
|
580
|
-
payloadHash: args.payloadHash,
|
|
581
|
-
attesterId: args.attesterId,
|
|
582
|
-
fieldKey: args.fieldKey,
|
|
583
|
-
value: args.value,
|
|
584
|
-
valueDigest: args.valueDigest,
|
|
585
|
-
allowedValues: args.allowedValues,
|
|
586
|
-
setRoot: args.setRoot,
|
|
587
|
-
setSiblings: args.setSiblings,
|
|
588
|
-
setDirs: args.setDirs,
|
|
589
|
-
fieldSalt: args.fieldSalt,
|
|
590
|
-
contentRoot: args.contentRoot,
|
|
591
|
-
schemaId: args.schemaId,
|
|
592
|
-
siblings: args.siblings,
|
|
593
|
-
dirs: args.dirs,
|
|
594
|
-
sessionId: args.sessionId,
|
|
595
|
-
contractAddress: args.contractAddress,
|
|
596
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
597
|
-
idempotencyKey: args.idempotencyKey,
|
|
598
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
281
|
+
/** Wait for a swap job; a landed swap frees its half here. */
|
|
282
|
+
const awaitSwap = async (started, offerId, extra) => {
|
|
283
|
+
const check = offerId ? { tool: 'get_swap_offer', args: { offerId } } : { tool: 'get_swap_wallet', args: {} };
|
|
284
|
+
const job = await awaitJob(client, config, started, extra, check);
|
|
285
|
+
if (typeof started.jobId === 'string')
|
|
286
|
+
settleHalfJob(started.jobId, job.status);
|
|
287
|
+
return job;
|
|
288
|
+
};
|
|
289
|
+
/** Hand bytes to a sponsor once and wait; the same bytes again pick up that job. */
|
|
290
|
+
const sponsorOnce = async (action, body, extra, check) => {
|
|
291
|
+
const key = callKey(action, body);
|
|
292
|
+
const started = await startOnce(key, async () => await client.callAction(action, body));
|
|
293
|
+
const job = await awaitJob(client, config, started, extra, check);
|
|
294
|
+
if (job.status === 'failed')
|
|
295
|
+
forgetStart(key);
|
|
296
|
+
return job;
|
|
297
|
+
};
|
|
298
|
+
/** Build one vault call here, hand it to the sponsor and wait until it settled. */
|
|
299
|
+
const submitVaultCall = async (input, idempotencyKey, extra) => {
|
|
300
|
+
if (!config)
|
|
301
|
+
throw new Error('local building is not configured for this server instance');
|
|
302
|
+
const key = callKey('vault', input.contractAddress, input.compiledArtifactRef ?? DEFAULT_ARTIFACT, input.call, input.params, input.merkleProof, input.docPair);
|
|
303
|
+
const started = await startOnce(key, async () => {
|
|
304
|
+
const built = await buildSponsorable(config, { ...input, bind: false });
|
|
305
|
+
const { b64 } = await resolveTxSource({ txId: built.txId }, 'unboundTxB64');
|
|
306
|
+
const job = await client.callAction('sponsorUnboundTransaction', { unboundTxB64: b64, idempotencyKey });
|
|
307
|
+
return { ...job, attesterId: built.attesterId, contractAddress: input.contractAddress, buildMs: built.buildMs };
|
|
599
308
|
});
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
'
|
|
608
|
-
'
|
|
609
|
-
'
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
attesterId
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
compiledArtifactRef: z.
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
},
|
|
625
|
-
}, run(async (args) => {
|
|
626
|
-
if (args.contentRoot
|
|
627
|
-
throw new Error('
|
|
309
|
+
const job = await awaitJob(client, config, started, extra, checkFor(input, String(started.attesterId)));
|
|
310
|
+
if (job.status === 'failed')
|
|
311
|
+
forgetStart(key);
|
|
312
|
+
return job;
|
|
313
|
+
};
|
|
314
|
+
server.registerTool('anchor_document', {
|
|
315
|
+
description: 'Anchor a document hash on Midnight in one call: builds the attest transaction here with ' +
|
|
316
|
+
'the caller seed, a sponsor pays the fee, and the tool waits until it landed (about 1 to 2 ' +
|
|
317
|
+
'minutes; the first call also downloads the prover keys). The record belongs to this ' +
|
|
318
|
+
'seed (attesterId). Only the hash and the hash of the metadata go on-chain; keep the ' +
|
|
319
|
+
'document yourself. With contentRoot and schemaId from prepare_document_proof it also ' +
|
|
320
|
+
'anchors the content root, which the field claims of submit_vault_call need. Returns ' +
|
|
321
|
+
'{ status, attesterId, payloadHash, contractAddress, metadataJson, txHash, transactionHash, ' +
|
|
322
|
+
'check }; check is the verify_attestation call that shows the record on-chain. ' + RETRY_HINT,
|
|
323
|
+
inputSchema: {
|
|
324
|
+
sha256: hex64('sha256').describe('sha256 of the document (64 hex); it becomes the payloadHash'),
|
|
325
|
+
metadata: z.record(z.unknown()).optional()
|
|
326
|
+
.describe('Public metadata; its sha256 is anchored with the hash. Default none'),
|
|
327
|
+
contentRoot: hex64('contentRoot').optional().describe('Content root from prepare_document_proof (64 hex); needs schemaId'),
|
|
328
|
+
schemaId: hex64('schemaId').optional().describe('Schema id from prepare_document_proof (64 hex); needs contentRoot'),
|
|
329
|
+
contractAddress: CONTRACT,
|
|
330
|
+
compiledArtifactRef: z.enum(BUILDABLE_ARTIFACTS).optional()
|
|
331
|
+
.describe("Vault lineage, default 'attestation-vault-32' (the sandbox vaults); 'attestation-vault' for a 16-slot vault"),
|
|
332
|
+
idempotencyKey: z.string().optional().describe('Dedupes retries of the same anchor'),
|
|
333
|
+
},
|
|
334
|
+
}, run(async (args, extra) => {
|
|
335
|
+
if (!!args.contentRoot !== !!args.schemaId)
|
|
336
|
+
throw new Error('pass contentRoot and schemaId together');
|
|
337
|
+
const contractAddress = vault(args.contractAddress, args.compiledArtifactRef);
|
|
338
|
+
const payloadHash = args.sha256.toLowerCase();
|
|
339
|
+
const metadataJson = args.metadata === undefined ? '' : JSON.stringify(args.metadata);
|
|
340
|
+
const metadataHash = createHash('sha256').update(metadataJson, 'utf8').digest('hex');
|
|
341
|
+
const ref = args.compiledArtifactRef ?? DEFAULT_ARTIFACT;
|
|
342
|
+
const anchor = await submitVaultCall({ contractAddress, call: 'attest', params: { payloadHash, metadataHash }, compiledArtifactRef: ref }, args.idempotencyKey, extra);
|
|
343
|
+
if (!args.contentRoot || !args.schemaId)
|
|
344
|
+
return { ...anchor, payloadHash, metadataJson };
|
|
345
|
+
if (anchor.status !== 'succeeded') {
|
|
346
|
+
const rootNext = anchor.status === 'pending'
|
|
347
|
+
? 'Run anchor_document again with the same arguments: it waits for this anchor and then anchors the content root.'
|
|
348
|
+
: 'The content root needs the anchor first.';
|
|
349
|
+
return { ...anchor, payloadHash, metadataJson, contentRoot: { status: 'not anchored', next: rootNext } };
|
|
628
350
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
attesterId: args.attesterId,
|
|
632
|
-
contentRoot: args.contentRoot,
|
|
633
|
-
schemaId: args.schemaId,
|
|
634
|
-
claims: args.claims.map((c) => 'threshold' in c ? { ...c, threshold: String(c.threshold) } : c),
|
|
635
|
-
sessionId: args.sessionId,
|
|
636
|
-
contractAddress: args.contractAddress,
|
|
637
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
638
|
-
idempotencyKey: args.idempotencyKey,
|
|
639
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
640
|
-
});
|
|
351
|
+
const root = await submitVaultCall({ contractAddress, call: 'anchorContentRoot', params: { payloadHash, contentRoot: args.contentRoot, schemaId: args.schemaId }, compiledArtifactRef: ref }, args.idempotencyKey && `${args.idempotencyKey}:root`, extra);
|
|
352
|
+
return { ...root, payloadHash, metadataJson, anchorTxHash: anchor.txHash };
|
|
641
353
|
}));
|
|
642
|
-
server.registerTool('
|
|
643
|
-
description: '
|
|
644
|
-
'
|
|
645
|
-
'
|
|
646
|
-
'
|
|
647
|
-
'
|
|
648
|
-
'
|
|
649
|
-
'
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
.describe('
|
|
657
|
-
|
|
658
|
-
.describe('
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
contentRootA: hex64('contentRootA').optional().describe('Optional: anchor A\'s root in the same flow'),
|
|
665
|
-
contentRootB: hex64('contentRootB').optional().describe('Optional: anchor B\'s root in the same flow'),
|
|
666
|
-
schemaId: hex64('schemaId').optional().describe('Shared schema id, required whenever a contentRoot is supplied'),
|
|
667
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
668
|
-
idempotencyKey: z.string().optional().describe('Dedupes retries'),
|
|
669
|
-
sponsorSessionId: z.string().uuid().optional().describe('Optional second session that pays the dust fee'),
|
|
670
|
-
},
|
|
671
|
-
}, run(async (args) => {
|
|
672
|
-
if (maskFreesEveryRealSlot(args.allowedMask, args.schema))
|
|
673
|
-
throw new Error(VACUOUS_MASK_MESSAGE);
|
|
674
|
-
return client.callAction('issueDocumentIntegrityAttestation', {
|
|
675
|
-
payloadHashA: args.payloadHashA,
|
|
676
|
-
payloadHashB: args.payloadHashB,
|
|
677
|
-
attesterIdA: args.attesterIdA,
|
|
678
|
-
attesterIdB: args.attesterIdB,
|
|
679
|
-
allowedMask: args.allowedMask,
|
|
680
|
-
schema: args.schema,
|
|
681
|
-
openingA: args.openingA,
|
|
682
|
-
openingB: args.openingB,
|
|
683
|
-
contentRootA: args.contentRootA,
|
|
684
|
-
contentRootB: args.contentRootB,
|
|
685
|
-
schemaId: args.schemaId,
|
|
686
|
-
sessionId: args.sessionId,
|
|
687
|
-
contractAddress: args.contractAddress,
|
|
688
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
689
|
-
idempotencyKey: args.idempotencyKey,
|
|
690
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
691
|
-
});
|
|
692
|
-
}));
|
|
693
|
-
server.registerTool('prove_document_diff', {
|
|
694
|
-
description: 'Prove that at least k of the aligned slots differ between two anchored documents ' +
|
|
695
|
-
'(16 slots on the default vault, 32 on attestation-vault-32), ' +
|
|
696
|
-
'without revealing which slots or what values: the distinctness claim (k=1 is "provably ' +
|
|
697
|
-
'not the same document"). Same requirements as prove_document_integrity: identical field ' +
|
|
698
|
-
'list, both openings in hand. A difference is a value change or a field that appeared or ' +
|
|
699
|
-
'disappeared; both-empty slots compare equal and padding never counts. k above the real ' +
|
|
700
|
-
'count fails at local proving time with zero on-chain effect. ' + POLL_HINT,
|
|
701
|
-
inputSchema: {
|
|
702
|
-
payloadHashA: hex64('payloadHashA').describe('Anchored document A (64 hex)'),
|
|
703
|
-
payloadHashB: hex64('payloadHashB').describe('Anchored document B; must differ from A'),
|
|
704
|
-
attesterIdA: hex64('attesterIdA').optional()
|
|
705
|
-
.describe('Document A\'s attester (64 hex); default the session\'s own'),
|
|
706
|
-
attesterIdB: hex64('attesterIdB').optional()
|
|
707
|
-
.describe('Document B\'s attester (64 hex); default attesterIdA'),
|
|
708
|
-
k: z.number().int().min(1).max(MAX_SLOT_WIDTH).describe('Minimum number of differing slots to prove, up to the vault width'),
|
|
709
|
-
schema: schemaSlots,
|
|
710
|
-
openingA: documentOpening,
|
|
711
|
-
openingB: documentOpening,
|
|
712
|
-
sessionId: z.string().uuid().describe('Wallet session id that signs and submits'),
|
|
713
|
-
contractAddress: z.string().min(1).describe('AttestationVault deployment'),
|
|
714
|
-
contentRootA: hex64('contentRootA').optional().describe('Optional: anchor A\'s root in the same flow'),
|
|
715
|
-
contentRootB: hex64('contentRootB').optional().describe('Optional: anchor B\'s root in the same flow'),
|
|
716
|
-
schemaId: hex64('schemaId').optional().describe('Shared schema id, required whenever a contentRoot is supplied'),
|
|
717
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
718
|
-
idempotencyKey: z.string().optional().describe('Dedupes retries'),
|
|
719
|
-
sponsorSessionId: z.string().uuid().optional().describe('Optional second session that pays the dust fee'),
|
|
720
|
-
},
|
|
721
|
-
}, run(async (args) => client.callAction('issueDocumentDiffAttestation', {
|
|
722
|
-
payloadHashA: args.payloadHashA,
|
|
723
|
-
payloadHashB: args.payloadHashB,
|
|
724
|
-
attesterIdA: args.attesterIdA,
|
|
725
|
-
attesterIdB: args.attesterIdB,
|
|
726
|
-
k: args.k,
|
|
727
|
-
schema: args.schema,
|
|
728
|
-
openingA: args.openingA,
|
|
729
|
-
openingB: args.openingB,
|
|
730
|
-
contentRootA: args.contentRootA,
|
|
731
|
-
contentRootB: args.contentRootB,
|
|
732
|
-
schemaId: args.schemaId,
|
|
733
|
-
sessionId: args.sessionId,
|
|
734
|
-
contractAddress: args.contractAddress,
|
|
735
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
736
|
-
idempotencyKey: args.idempotencyKey,
|
|
737
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
738
|
-
})));
|
|
739
|
-
server.registerTool('grant_disclosure', {
|
|
740
|
-
description: 'Grant a disclosure level for an attestation to a grantee identity, on-chain via the ' +
|
|
741
|
-
'AttestationVault. Attester-only: the transaction is rejected in-circuit unless the ' +
|
|
742
|
-
'session wallet is the original attester. level: 0=public, 1=legitimate-interest, ' +
|
|
743
|
-
'2=authority. ' + POLL_HINT,
|
|
744
|
-
inputSchema: {
|
|
745
|
-
payloadHash: hex64('payloadHash').describe('The attestation payload hash (64 hex)'),
|
|
746
|
-
grantee: hex64('grantee').describe('Grantee identity (64 hex Bytes<32>)'),
|
|
747
|
-
level: z.union([z.literal(0), z.literal(1), z.literal(2)])
|
|
748
|
-
.describe('0=public, 1=legitimate-interest, 2=authority'),
|
|
749
|
-
sessionId: z.string().uuid().describe('Wallet session id (must be the attester)'),
|
|
750
|
-
contractAddress: z.string().min(1).describe('AttestationVault deployment'),
|
|
751
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
752
|
-
idempotencyKey: z.string().optional().describe('Dedupes retries'),
|
|
753
|
-
sponsorSessionId: z.string().uuid().optional().describe('Optional second session that pays the dust fee'),
|
|
754
|
-
},
|
|
755
|
-
}, run(async (args) => client.callAction('grantDisclosure', {
|
|
756
|
-
payloadHash: args.payloadHash,
|
|
757
|
-
grantee: args.grantee,
|
|
758
|
-
level: args.level,
|
|
759
|
-
sessionId: args.sessionId,
|
|
760
|
-
contractAddress: args.contractAddress,
|
|
761
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
762
|
-
idempotencyKey: args.idempotencyKey,
|
|
763
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
764
|
-
})));
|
|
765
|
-
server.registerTool('revoke_disclosure', {
|
|
766
|
-
description: 'Revoke a previously granted disclosure on-chain (removes the grantee entry). ' +
|
|
767
|
-
'Attester-only, enforced in-circuit. ' + POLL_HINT,
|
|
768
|
-
inputSchema: {
|
|
769
|
-
payloadHash: hex64('payloadHash').describe('The attestation payload hash (64 hex)'),
|
|
770
|
-
grantee: hex64('grantee').describe('Grantee identity (64 hex Bytes<32>)'),
|
|
771
|
-
sessionId: z.string().uuid().describe('Wallet session id (must be the attester)'),
|
|
772
|
-
contractAddress: z.string().min(1).describe('AttestationVault deployment'),
|
|
773
|
-
compiledArtifactRef: z.string().optional().describe("Contract artifact ref, defaults to 'attestation-vault-32'; 'attestation-vault' for a 16-slot vault"),
|
|
774
|
-
idempotencyKey: z.string().optional().describe('Dedupes retries'),
|
|
775
|
-
sponsorSessionId: z.string().uuid().optional().describe('Optional second session that pays the dust fee'),
|
|
354
|
+
server.registerTool('submit_vault_call', {
|
|
355
|
+
description: 'Run any AttestationVault call in one go: build, prove and sign it here with the caller ' +
|
|
356
|
+
'seed, a sponsor pays, and the tool waits until it landed. Same calls and params as ' +
|
|
357
|
+
'build_sponsorable_transaction: the ZK claims over a hidden field (proveFieldPredicate, ' +
|
|
358
|
+
'proveFieldEquality, proveFieldMembership), the claims across two documents ' +
|
|
359
|
+
'(proveFieldsUnchangedExcept, proveFieldsDiffer), disclosure (grantDisclosure, ' +
|
|
360
|
+
'revokeDisclosure), passports and anchoring. The witness from prepare_document_proof is ' +
|
|
361
|
+
'used here and never sent. Returns { status, attesterId, contractAddress, txHash, ' +
|
|
362
|
+
'transactionHash, check }; check is the free verify call that shows the effect on-chain. ' +
|
|
363
|
+
'A failed job says in `next` what to do. ' + RETRY_HINT,
|
|
364
|
+
inputSchema: {
|
|
365
|
+
contractAddress: CONTRACT,
|
|
366
|
+
call: z.enum(BUILDABLE_CALLS).describe('Which circuit to call'),
|
|
367
|
+
params: z.record(z.union([z.string(), z.number()]))
|
|
368
|
+
.describe('The call parameters, listed per call in build_sponsorable_transaction'),
|
|
369
|
+
merkleProof: z.record(z.any()).optional()
|
|
370
|
+
.describe('WITNESS for the proveField* calls, as prepare_document_proof returns it for that field'),
|
|
371
|
+
docPair: z.record(z.any()).optional()
|
|
372
|
+
.describe('WITNESS { schema, openingA, openingB } for proveFieldsUnchangedExcept and proveFieldsDiffer'),
|
|
373
|
+
compiledArtifactRef: z.enum(BUILDABLE_ARTIFACTS).optional()
|
|
374
|
+
.describe("Vault lineage, default 'attestation-vault-32' (the sandbox vaults); 'attestation-vault' for a 16-slot vault"),
|
|
375
|
+
idempotencyKey: z.string().optional().describe('Dedupes retries of the same call'),
|
|
776
376
|
},
|
|
777
|
-
}, run(async (args) =>
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
contractAddress: args.contractAddress,
|
|
782
|
-
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
783
|
-
idempotencyKey: args.idempotencyKey,
|
|
784
|
-
sponsorSessionId: args.sponsorSessionId,
|
|
785
|
-
})));
|
|
377
|
+
}, run(async (args, extra) => submitVaultCall({
|
|
378
|
+
contractAddress: vault(args.contractAddress, args.compiledArtifactRef), call: args.call, params: args.params,
|
|
379
|
+
merkleProof: args.merkleProof, docPair: args.docPair, compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
380
|
+
}, args.idempotencyKey, extra)));
|
|
786
381
|
server.registerTool('build_sponsorable_transaction', {
|
|
787
|
-
description: 'Build, prove and sign an AttestationVault call LOCALLY
|
|
788
|
-
'
|
|
789
|
-
'
|
|
790
|
-
'secret leaves this machine. The fee-unpaid transaction stays in this process under the ' +
|
|
382
|
+
description: 'Build, prove and sign an AttestationVault call LOCALLY and hand back the bytes, for a ' +
|
|
383
|
+
'sponsor on another server or a later hand-over; submit_vault_call does build and submit ' +
|
|
384
|
+
'in one go. The caller seed (NIGHTGATE_SEED_HEX, else the seed file) stays here, the ' +
|
|
385
|
+
'attestation secret is derived from it, nothing secret leaves this machine. The fee-unpaid transaction stays in this process under the ' +
|
|
791
386
|
'returned txId (its sha256; characters and bytes alongside), or is written to outFile: ' +
|
|
792
387
|
'UNBOUND by default (hand the txId to sponsor_unbound_transaction, parallel channel) or ' +
|
|
793
388
|
'bound with bind:true (sponsor_finalized_transaction). The on-chain effect carries the attester id ' +
|
|
@@ -814,7 +409,7 @@ export function registerTools(server, client, config) {
|
|
|
814
409
|
'the transaction TTL (~30 min) and against the contract state at build time: if the ' +
|
|
815
410
|
'sponsor job ends CHAIN_EXECUTION_FAILED, build again.',
|
|
816
411
|
inputSchema: {
|
|
817
|
-
contractAddress:
|
|
412
|
+
contractAddress: CONTRACT,
|
|
818
413
|
call: z.enum(BUILDABLE_CALLS).describe('Which circuit to call'),
|
|
819
414
|
params: z.record(z.union([z.string(), z.number()]))
|
|
820
415
|
.describe('The call parameters (see the per-call list in the description)'),
|
|
@@ -833,13 +428,13 @@ export function registerTools(server, client, config) {
|
|
|
833
428
|
if (!config)
|
|
834
429
|
throw new Error('local building is not configured for this server instance');
|
|
835
430
|
return buildSponsorable(config, {
|
|
836
|
-
contractAddress: args.contractAddress, call: args.call, params: args.params, bind: args.bind === true,
|
|
431
|
+
contractAddress: vault(args.contractAddress, args.compiledArtifactRef), call: args.call, params: args.params, bind: args.bind === true,
|
|
837
432
|
compiledArtifactRef: args.compiledArtifactRef ?? DEFAULT_ARTIFACT,
|
|
838
433
|
merkleProof: args.merkleProof, docPair: args.docPair, outFile: args.outFile,
|
|
839
434
|
});
|
|
840
435
|
}));
|
|
841
436
|
server.registerTool('get_attester_identity', {
|
|
842
|
-
description: 'The attester id this MCP server builds under (derived from
|
|
437
|
+
description: 'The attester id this MCP server builds under (derived from the caller seed via the ' +
|
|
843
438
|
'txbuilder), plus the network, the shielded address and the two public shielded keys ' +
|
|
844
439
|
'(what a sender needs to create a coin for this wallet). Use it to check ' +
|
|
845
440
|
'verify_attestation results against the identity that will appear on-chain; builds ' +
|
|
@@ -859,8 +454,7 @@ export function registerTools(server, client, config) {
|
|
|
859
454
|
'outside it. The transaction expires with its TTL (default 30 min), so hand it over ' +
|
|
860
455
|
'promptly. This is the SERIAL channel (one tx per sponsor wallet at a time); for ' +
|
|
861
456
|
'parallel submission build with bind:false and use sponsor_unbound_transaction. ' +
|
|
862
|
-
'The server picks the wallet that pays.
|
|
863
|
-
'RETURNED by this call. ' + POLL_HINT,
|
|
457
|
+
'The server picks the wallet that pays. ' + WAIT_HINT,
|
|
864
458
|
inputSchema: {
|
|
865
459
|
txId: TX_ID.describe('txId returned by build_sponsorable_transaction (bind:true) on this server; the bytes never pass through the conversation'),
|
|
866
460
|
file: TX_FILE,
|
|
@@ -869,13 +463,9 @@ export function registerTools(server, client, config) {
|
|
|
869
463
|
idempotencyKey: z.string().optional()
|
|
870
464
|
.describe('Dedupes retries; the request is also fingerprinted by the transaction bytes'),
|
|
871
465
|
},
|
|
872
|
-
}, run(async (args) => {
|
|
466
|
+
}, run(async (args, extra) => {
|
|
873
467
|
const { b64, sent } = await resolveTxSource({ txId: args.txId, file: args.file, b64: args.finalizedTxB64 }, 'finalizedTxB64');
|
|
874
|
-
|
|
875
|
-
finalizedTxB64: b64,
|
|
876
|
-
idempotencyKey: args.idempotencyKey,
|
|
877
|
-
});
|
|
878
|
-
return { ...job, sent };
|
|
468
|
+
return { ...await sponsorOnce('sponsorFinalizedTransaction', { finalizedTxB64: b64, idempotencyKey: args.idempotencyKey }, extra), sent };
|
|
879
469
|
}));
|
|
880
470
|
server.registerTool('sponsor_unbound_transaction', {
|
|
881
471
|
description: 'Cross-server fee sponsoring, PARALLEL channel (NIGHTGATE 0.18.0): submit an UNBOUND ' +
|
|
@@ -889,8 +479,7 @@ export function registerTools(server, client, config) {
|
|
|
889
479
|
'calls against the SAME contract in one block conflict, the loser lands on-chain but its ' +
|
|
890
480
|
'call does not apply and the job ends failed with errorCode CHAIN_EXECUTION_FAILED (the ' +
|
|
891
481
|
'transaction hash is in the error); then REBUILD the transaction against the current ' +
|
|
892
|
-
'contract state and sponsor again (never resubmit the same bytes).
|
|
893
|
-
'with the sessionId RETURNED by this call. ' + POLL_HINT,
|
|
482
|
+
'contract state and sponsor again (never resubmit the same bytes). ' + WAIT_HINT,
|
|
894
483
|
inputSchema: {
|
|
895
484
|
txId: TX_ID.describe('txId returned by build_sponsorable_transaction on this server; the bytes never pass through the conversation'),
|
|
896
485
|
file: TX_FILE,
|
|
@@ -899,16 +488,12 @@ export function registerTools(server, client, config) {
|
|
|
899
488
|
idempotencyKey: z.string().optional()
|
|
900
489
|
.describe('Dedupes retries of the SAME bytes; a rebuilt transaction is a new key'),
|
|
901
490
|
},
|
|
902
|
-
}, run(async (args) => {
|
|
491
|
+
}, run(async (args, extra) => {
|
|
903
492
|
const { b64, sent } = await resolveTxSource({ txId: args.txId, file: args.file, b64: args.unboundTxB64 }, 'unboundTxB64');
|
|
904
|
-
|
|
905
|
-
unboundTxB64: b64,
|
|
906
|
-
idempotencyKey: args.idempotencyKey,
|
|
907
|
-
});
|
|
908
|
-
return { ...job, sent };
|
|
493
|
+
return { ...await sponsorOnce('sponsorUnboundTransaction', { unboundTxB64: b64, idempotencyKey: args.idempotencyKey }, extra), sent };
|
|
909
494
|
}));
|
|
910
495
|
server.registerTool('get_swap_wallet', {
|
|
911
|
-
description: 'The shielded wallet this MCP server swaps from (
|
|
496
|
+
description: 'The shielded wallet this MCP server swaps from (the caller seed): address, ' +
|
|
912
497
|
'public keys, and per token type the balance, the number of free coins and `spendable`, ' +
|
|
913
498
|
'the most ONE swap half can give (what its largest coins hold, up to maxInputs coins). ' +
|
|
914
499
|
'Also lists the halves built here that are not handed over yet; one posted with ' +
|
|
@@ -922,6 +507,15 @@ export function registerTools(server, client, config) {
|
|
|
922
507
|
for (const offerId of postedOfferIds()) {
|
|
923
508
|
settleBoardRows(await client.callFunction('getSwapOffer', { offerId: raw(offerId) }).catch(() => undefined));
|
|
924
509
|
}
|
|
510
|
+
// a swap job that was still open when its tool returned may have landed since
|
|
511
|
+
for (const jobId of halfJobIds()) {
|
|
512
|
+
const sessionId = jobSession(jobId);
|
|
513
|
+
if (!sessionId)
|
|
514
|
+
continue;
|
|
515
|
+
const job = await client.callAction('getJobStatus', { jobId, sessionId }).catch(() => undefined);
|
|
516
|
+
if (job)
|
|
517
|
+
settleHalfJob(jobId, job.status);
|
|
518
|
+
}
|
|
925
519
|
return swapWalletInfo(config);
|
|
926
520
|
}));
|
|
927
521
|
server.registerTool('read_swap_offer', {
|
|
@@ -959,9 +553,9 @@ export function registerTools(server, client, config) {
|
|
|
959
553
|
description: SWAP_HINT + ' This takes an offer: it reads the offer\'s terms from the transaction, ' +
|
|
960
554
|
'refuses when they differ from `expect`, builds and proves the mirror half from this ' +
|
|
961
555
|
'server\'s wallet (it spends what the offer wants and creates what the offer gives). ' +
|
|
962
|
-
'With submit: true the two halves go to the sponsor at once and the
|
|
963
|
-
'
|
|
964
|
-
'
|
|
556
|
+
'With submit: true the two halves go to the sponsor at once and the tool waits until ' +
|
|
557
|
+
'the swap landed ({ status, txHash, transactionHash, result.swap, id, terms }). ' +
|
|
558
|
+
'Without it the answer carries both halves for sponsor_swap. ALWAYS pass ' +
|
|
965
559
|
'`expect` with the terms you agreed to: an offer is text from someone else. With offerId ' +
|
|
966
560
|
'the board entry is read first and the call refuses, before proving, when it is not open ' +
|
|
967
561
|
'or carries another offer than `offer`. A sponsor that refuses the swap leaves the proven ' +
|
|
@@ -978,45 +572,55 @@ export function registerTools(server, client, config) {
|
|
|
978
572
|
offerId: z.string().uuid().optional()
|
|
979
573
|
.describe('The board entry the offer came from (list_swap_offers), so the fill is recorded against it'),
|
|
980
574
|
},
|
|
981
|
-
}, run(async (args) => {
|
|
575
|
+
}, run(async (args, extra) => {
|
|
982
576
|
if (!config)
|
|
983
577
|
throw new Error('swaps are not configured for this server instance');
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
578
|
+
const take = async () => {
|
|
579
|
+
if (args.offerId) {
|
|
580
|
+
const row = await client.callFunction('getSwapOffer', { offerId: raw(args.offerId) });
|
|
581
|
+
if (row.status !== 'open')
|
|
582
|
+
throw new Error(`board offer ${args.offerId} is ${String(row.status)}, not open`);
|
|
583
|
+
if (typeof row.offer !== 'string' || !(await sameOffer(args.offer, row.offer))) {
|
|
584
|
+
throw new Error(`board offer ${args.offerId} carries another offer than the one passed: take the board's text, or drop offerId`);
|
|
585
|
+
}
|
|
990
586
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
...(args.expect ? { expect: { gives: legOut(args.expect.gives), wants: legOut(args.expect.wants) } } : {}),
|
|
995
|
-
});
|
|
996
|
-
if (args.submit !== true)
|
|
997
|
-
return taken;
|
|
998
|
-
let job;
|
|
999
|
-
try {
|
|
1000
|
-
job = await client.callAction('sponsorSwap', {
|
|
1001
|
-
...(args.offerId ? { offerId: args.offerId } : { makerHalfB64: taken.makerHalfB64 }),
|
|
1002
|
-
takerHalfB64: taken.takerHalfB64,
|
|
1003
|
-
idempotencyKey: args.idempotencyKey,
|
|
587
|
+
return takeOffer(config, {
|
|
588
|
+
offer: args.offer,
|
|
589
|
+
...(args.expect ? { expect: { gives: legOut(args.expect.gives), wants: legOut(args.expect.wants) } } : {}),
|
|
1004
590
|
});
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
591
|
+
};
|
|
592
|
+
if (args.submit !== true)
|
|
593
|
+
return take();
|
|
594
|
+
const key = callKey('takeSwapOffer', args.offer, args.expect, args.offerId);
|
|
595
|
+
const started = await startOnce(key, async () => {
|
|
596
|
+
const taken = await take();
|
|
597
|
+
let job;
|
|
598
|
+
try {
|
|
599
|
+
job = await client.callAction('sponsorSwap', {
|
|
600
|
+
...(args.offerId ? { offerId: args.offerId } : { makerHalfB64: taken.makerHalfB64 }),
|
|
601
|
+
takerHalfB64: taken.takerHalfB64,
|
|
602
|
+
idempotencyKey: args.idempotencyKey,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
if (!(err instanceof NightgateApiError))
|
|
607
|
+
throw err;
|
|
608
|
+
throw new NightgateApiError(err.status, err.code, err.message, { ...(err.detail ?? {}), halfId: taken.id });
|
|
609
|
+
}
|
|
610
|
+
trackHalfJob(taken.id, job.jobId);
|
|
611
|
+
return { ...job, id: taken.id, terms: taken.terms, bound: taken.bound, buildMs: taken.buildMs };
|
|
612
|
+
});
|
|
613
|
+
const settled = await awaitSwap(started, args.offerId, extra);
|
|
614
|
+
if (settled.status === 'failed')
|
|
615
|
+
forgetStart(key);
|
|
616
|
+
return settled;
|
|
1013
617
|
}));
|
|
1014
618
|
server.registerTool('revert_swap_offer', {
|
|
1015
619
|
description: 'Release the coins of a swap half this server built (build_swap_offer or take_swap_offer) ' +
|
|
1016
620
|
'that is not going to be used. Until then, or until the swap lands, the coins count as ' +
|
|
1017
621
|
'spent in this wallet. It does not recall an offer somebody already holds: to make a ' +
|
|
1018
622
|
'published offer worthless, spend its coins in another swap. Ids are listed by ' +
|
|
1019
|
-
'get_swap_wallet; a half whose swap landed leaves the list
|
|
623
|
+
'get_swap_wallet; a half whose swap landed leaves the list.',
|
|
1020
624
|
inputSchema: {
|
|
1021
625
|
id: z.string().regex(/^[0-9a-f]{32}$/, 'id must be the 32 hex characters a swap tool returned'),
|
|
1022
626
|
},
|
|
@@ -1036,7 +640,7 @@ export function registerTools(server, client, config) {
|
|
|
1036
640
|
'An offer fills once: if it was taken first by somebody else the job fails. Pass both ' +
|
|
1037
641
|
'halves, or `halfId` alone for an offer this server took (take_swap_offer keeps both ' +
|
|
1038
642
|
'halves); with `offerId` the maker half comes from the board, next to takerHalf or the ' +
|
|
1039
|
-
'halfId of the taken offer.
|
|
643
|
+
'halfId of the taken offer. ' + WAIT_HINT,
|
|
1040
644
|
inputSchema: {
|
|
1041
645
|
makerHalf: swapHalf("The maker's half (the offer)").optional(),
|
|
1042
646
|
takerHalf: swapHalf("The taker's half").optional(),
|
|
@@ -1047,7 +651,7 @@ export function registerTools(server, client, config) {
|
|
|
1047
651
|
offerId: z.string().uuid().optional()
|
|
1048
652
|
.describe('An offer on the server\'s board (list_swap_offers): the server takes the maker half from there; pass takerHalf or the halfId of the taken offer with it'),
|
|
1049
653
|
},
|
|
1050
|
-
}, run(async (args) => {
|
|
654
|
+
}, run(async (args, extra) => {
|
|
1051
655
|
let halves;
|
|
1052
656
|
if (args.offerId) {
|
|
1053
657
|
if (args.makerHalf)
|
|
@@ -1066,13 +670,17 @@ export function registerTools(server, client, config) {
|
|
|
1066
670
|
? { makerHalfB64: args.makerHalf, takerHalfB64: args.takerHalf }
|
|
1067
671
|
: takenHalves(args.halfId);
|
|
1068
672
|
}
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
idempotencyKey: args.idempotencyKey
|
|
673
|
+
const key = callKey('sponsorSwap', halves);
|
|
674
|
+
const started = await startOnce(key, async () => {
|
|
675
|
+
const job = await client.callAction('sponsorSwap', { ...halves, idempotencyKey: args.idempotencyKey });
|
|
676
|
+
if (args.halfId)
|
|
677
|
+
trackHalfJob(args.halfId, job?.jobId);
|
|
678
|
+
return job;
|
|
1072
679
|
});
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
680
|
+
const settled = await awaitSwap(started, args.offerId, extra);
|
|
681
|
+
if (settled.status === 'failed')
|
|
682
|
+
forgetStart(key);
|
|
683
|
+
return settled;
|
|
1076
684
|
}));
|
|
1077
685
|
server.registerTool('post_swap_offer', {
|
|
1078
686
|
description: 'Post a maker half on the server\'s offer board, so takers find it with list_swap_offers ' +
|
|
@@ -1143,66 +751,6 @@ export function registerTools(server, client, config) {
|
|
|
1143
751
|
offerId: z.string().uuid().describe('The board entry'),
|
|
1144
752
|
},
|
|
1145
753
|
}, run(async (args) => client.callAction('retireSwapOffer', { offerId: args.offerId })));
|
|
1146
|
-
server.registerTool('grant_disclosure_to_holders', {
|
|
1147
|
-
description: 'Disclose a document to everyone who holds a token: whoever registered a claim key on ' +
|
|
1148
|
-
'the holder-registry contract for tokenType by passing a coin through it can read the ' +
|
|
1149
|
-
'text with claim_disclosure, and the server never learns who. content must hash to ' +
|
|
1150
|
-
'payloadHash (blake2b-256 or sha256 of the UTF-8 text); it is stored encrypted. A repeat ' +
|
|
1151
|
-
'call for the same payload, token type and registry updates the grant (content kept ' +
|
|
1152
|
-
'when omitted). Returns { holderGrantId, hasContent, expiresAt, status }.',
|
|
1153
|
-
inputSchema: {
|
|
1154
|
-
payloadHash: hex64('payloadHash').describe('Hash of the document (64 hex)'),
|
|
1155
|
-
tokenType: hex64('tokenType').describe('The raw shielded token type whose holders may read (64 hex)'),
|
|
1156
|
-
registryAddress: hex64('registryAddress').describe('The holder-registry deployment holders registered on'),
|
|
1157
|
-
content: z.string().max(1_048_576).optional().describe('The document text; must hash to payloadHash'),
|
|
1158
|
-
contentType: z.string().max(100).optional().describe("Default 'text/plain'"),
|
|
1159
|
-
expiresAt: z.string().datetime().optional().describe('ISO timestamp, at most one year ahead'),
|
|
1160
|
-
},
|
|
1161
|
-
}, run(async (args) => client.callAction('grantDisclosureToHolders', {
|
|
1162
|
-
payloadHash: args.payloadHash.toLowerCase(),
|
|
1163
|
-
tokenType: args.tokenType.toLowerCase(),
|
|
1164
|
-
registryAddress: args.registryAddress.toLowerCase(),
|
|
1165
|
-
content: args.content,
|
|
1166
|
-
contentType: args.contentType,
|
|
1167
|
-
expiresAt: args.expiresAt,
|
|
1168
|
-
})));
|
|
1169
|
-
server.registerTool('revoke_holder_disclosure', {
|
|
1170
|
-
description: 'Revoke a disclosure to token holders. Only its grantor may. Returns { holderGrantId, status }.',
|
|
1171
|
-
inputSchema: {
|
|
1172
|
-
holderGrantId: z.string().uuid().describe('From grant_disclosure_to_holders'),
|
|
1173
|
-
},
|
|
1174
|
-
}, run(async (args) => client.callAction('revokeHolderDisclosure', { holderGrantId: args.holderGrantId })));
|
|
1175
|
-
server.registerTool('claim_disclosure', {
|
|
1176
|
-
description: 'Read a document disclosed to the holders of a token. claimSecret is the 32-byte secret ' +
|
|
1177
|
-
'behind the claim key registered on the holder-registry contract (holder_claim_key); the ' +
|
|
1178
|
-
'server reads the registry live and answers { entitled, content, contentType, ' +
|
|
1179
|
-
'contentHashKind, expiresAt } or { entitled: false, reason }. Needs a live indexer on ' +
|
|
1180
|
-
'the server.',
|
|
1181
|
-
inputSchema: {
|
|
1182
|
-
payloadHash: hex64('payloadHash').describe('Hash of the document (64 hex)'),
|
|
1183
|
-
tokenType: hex64('tokenType').describe('The raw shielded token type (64 hex)'),
|
|
1184
|
-
claimSecret: hex64('claimSecret').describe('The secret behind the registered claim key (64 hex)'),
|
|
1185
|
-
},
|
|
1186
|
-
}, run(async (args) => client.callAction('claimDisclosure', {
|
|
1187
|
-
payloadHash: args.payloadHash.toLowerCase(),
|
|
1188
|
-
tokenType: args.tokenType.toLowerCase(),
|
|
1189
|
-
claimSecret: args.claimSecret.toLowerCase(),
|
|
1190
|
-
})));
|
|
1191
|
-
server.registerTool('holder_claim_key', {
|
|
1192
|
-
description: 'The claim key a token holder registers on the holder-registry contract, derived from a ' +
|
|
1193
|
-
'32-byte secret (compute-only). Without a secret a fresh random one is generated: keep ' +
|
|
1194
|
-
'it, claim_disclosure needs it. Register the key with registerHolder(coin, claimKey) on ' +
|
|
1195
|
-
'the registry (one coin of the token type passes through the contract and comes back), ' +
|
|
1196
|
-
'e.g. through build_sponsorable_transaction on a builder that knows the registry.',
|
|
1197
|
-
inputSchema: {
|
|
1198
|
-
claimSecret: hex64('claimSecret').optional().describe('Existing secret (64 hex); omit to generate one'),
|
|
1199
|
-
},
|
|
1200
|
-
}, run(async (args) => {
|
|
1201
|
-
const holderClaimKey = await txExport('holderClaimKey', 'holder_claim_key', '0.10.0');
|
|
1202
|
-
const { randomBytes } = await import('node:crypto');
|
|
1203
|
-
const claimSecret = (args.claimSecret ?? randomBytes(32).toString('hex')).toLowerCase();
|
|
1204
|
-
return { claimSecret, claimKey: holderClaimKey(claimSecret) };
|
|
1205
|
-
}));
|
|
1206
754
|
server.registerTool('derive_token_type', {
|
|
1207
755
|
description: 'Derive the raw token type a minting contract produces (compute-only, no wallet, no ' +
|
|
1208
756
|
'chain access). A custom token is addressed by rawTokenType(domainSeparator, ' +
|
|
@@ -1219,18 +767,14 @@ export function registerTools(server, client, config) {
|
|
|
1219
767
|
domainSeparator: args.domainSeparator,
|
|
1220
768
|
})));
|
|
1221
769
|
server.registerTool('mint_token', {
|
|
1222
|
-
description: 'Mint a shielded token with a name of your own on a token-factory deployment.
|
|
1223
|
-
'
|
|
1224
|
-
'
|
|
1225
|
-
'
|
|
1226
|
-
'
|
|
1227
|
-
'
|
|
1228
|
-
'
|
|
1229
|
-
'
|
|
1230
|
-
'response names the token before the job runs ({ tokenType, domain, issuerKey }); a ' +
|
|
1231
|
-
'landed mint makes the type known to the sponsor (swaps, offer board). The recipient ' +
|
|
1232
|
-
'defaults to this wallet (get_attester_identity); another wallet takes both of its keys. ' +
|
|
1233
|
-
'Job result { txHash, transactionHash, ... }. Call get_job_status with the sessionId RETURNED by this call. ' + POLL_HINT,
|
|
770
|
+
description: 'Mint a shielded token with a name of your own on a token-factory deployment. This ' +
|
|
771
|
+
'process is the issuer: its issuer secret derives from the caller seed, the mint is ' +
|
|
772
|
+
'built, proven and signed here, and a sponsor pays; the seed never leaves this process. ' +
|
|
773
|
+
'The factory has to be on the sponsor\'s contract list and mint on its circuit list. ' +
|
|
774
|
+
'The same name from another issuer is another token. The answer names the token ' +
|
|
775
|
+
'({ tokenType, domain, issuerKey }); a landed mint makes the type known to the sponsor ' +
|
|
776
|
+
'(swaps, offer board). The recipient defaults to this wallet (get_attester_identity); ' +
|
|
777
|
+
'another wallet takes both of its keys. ' + WAIT_HINT,
|
|
1234
778
|
inputSchema: {
|
|
1235
779
|
contractAddress: hex64('contractAddress').describe('A token-factory deployment'),
|
|
1236
780
|
name: z.string().min(1).refine((s) => Buffer.byteLength(s, 'utf8') <= 32, 'name must be at most 32 UTF-8 bytes')
|
|
@@ -1242,92 +786,46 @@ export function registerTools(server, client, config) {
|
|
|
1242
786
|
recipientCoinPublicKey: hex64('recipientCoinPublicKey').optional()
|
|
1243
787
|
.describe('Zswap coin public key of the receiving wallet (64 hex); default this wallet\'s own'),
|
|
1244
788
|
recipientEncryptionPublicKey: hex64('recipientEncryptionPublicKey').optional()
|
|
1245
|
-
.describe('The receiving wallet\'s encryption public key (64 hex); required with a recipient other than this wallet
|
|
1246
|
-
sessionId: z.string().uuid().optional()
|
|
1247
|
-
.describe('Server way only: the wallet session that mints; it is the issuer'),
|
|
789
|
+
.describe('The receiving wallet\'s encryption public key (64 hex); required with a recipient other than this wallet'),
|
|
1248
790
|
idempotencyKey: z.string().optional().describe('Dedupes retries'),
|
|
1249
791
|
},
|
|
1250
|
-
}, run(async (args) => {
|
|
1251
|
-
if (args.sessionId) {
|
|
1252
|
-
let recipient = args.recipientCoinPublicKey;
|
|
1253
|
-
if (!recipient) {
|
|
1254
|
-
if (!config?.seedHex)
|
|
1255
|
-
throw new Error('mint_token needs recipientCoinPublicKey: no NIGHTGATE_SEED_HEX for a default');
|
|
1256
|
-
recipient = (await seedIdentity(config)).coinPublicKey;
|
|
1257
|
-
if (!recipient)
|
|
1258
|
-
throw new Error('mint_token needs recipientCoinPublicKey: the seed exposes no shielded keys');
|
|
1259
|
-
}
|
|
1260
|
-
return client.callAction('mintFactoryToken', {
|
|
1261
|
-
contractAddress: args.contractAddress.toLowerCase(),
|
|
1262
|
-
name: args.name,
|
|
1263
|
-
amount: String(args.amount),
|
|
1264
|
-
recipientCoinPublicKey: recipient.toLowerCase(),
|
|
1265
|
-
sessionId: args.sessionId,
|
|
1266
|
-
idempotencyKey: args.idempotencyKey,
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
792
|
+
}, run(async (args, extra) => {
|
|
1269
793
|
if (!config)
|
|
1270
794
|
throw new Error('local minting is not configured for this server instance');
|
|
1271
|
-
const
|
|
795
|
+
const input = {
|
|
1272
796
|
contractAddress: args.contractAddress,
|
|
1273
797
|
name: args.name,
|
|
1274
798
|
amount: String(args.amount),
|
|
1275
799
|
recipientCoinPublicKey: args.recipientCoinPublicKey,
|
|
1276
800
|
recipientEncryptionPublicKey: args.recipientEncryptionPublicKey,
|
|
801
|
+
};
|
|
802
|
+
const key = callKey('mintToken', input);
|
|
803
|
+
const started = await startOnce(key, async () => {
|
|
804
|
+
const { unboundTxB64, ...built } = await buildMint(config, input);
|
|
805
|
+
const job = await client.callAction('sponsorUnboundTransaction', { unboundTxB64, idempotencyKey: args.idempotencyKey });
|
|
806
|
+
return { ...built, ...job };
|
|
1277
807
|
});
|
|
1278
|
-
const job = await client
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
const { unboundTxB64: _tx, ...rest } = built;
|
|
1283
|
-
return { ...job, ...rest, channel: 'local' };
|
|
808
|
+
const job = await awaitJob(client, config, started, extra, { tool: 'get_swap_wallet', args: {} });
|
|
809
|
+
if (job.status === 'failed')
|
|
810
|
+
forgetStart(key);
|
|
811
|
+
return job;
|
|
1284
812
|
}));
|
|
1285
|
-
server.registerTool('
|
|
1286
|
-
description: 'What this key
|
|
1287
|
-
'
|
|
1288
|
-
'
|
|
813
|
+
server.registerTool('get_usage', {
|
|
814
|
+
description: 'What this key has left and what it spent. With a gateway key (oda_...): unitsLeft, ' +
|
|
815
|
+
'validUntil, status, usage today and over 30 days, and the last calls with the units each ' +
|
|
816
|
+
'one cost (ledger). Free. With an agent grant of a direct instance: jobs by kind and ' +
|
|
817
|
+
'status and the limits { jobsPerHour, jobsPerDay, classes }. Read it before a burst and ' +
|
|
818
|
+
'after a 429 (the error carries retryAfterSeconds).',
|
|
1289
819
|
inputSchema: {
|
|
1290
|
-
since: z.string().datetime().optional().describe('ISO timestamp
|
|
1291
|
-
until: z.string().datetime().optional().describe('ISO timestamp
|
|
1292
|
-
},
|
|
1293
|
-
}, run(async (args) => client.callFunction('getGrantUsage', { grantId: null, since: args.since ?? null, until: args.until ?? null })));
|
|
1294
|
-
server.registerTool('get_job_status', {
|
|
1295
|
-
description: 'Status of an async NIGHTGATE job (all submit actions return a jobId). ' +
|
|
1296
|
-
'status: pending | running | external_execution | submitted | reconciliation_required | ' +
|
|
1297
|
-
'succeeded | failed. The server holds the call up to waitMs (default 20 s) until the job ' +
|
|
1298
|
-
'settles, so call it again only if the status is still open; result carries the job outcome ' +
|
|
1299
|
-
'JSON, chainStatus tracks on-chain finalization independently. txHash and transactionId are ' +
|
|
1300
|
-
'the ledger identifier (stable, tracks the job); transactionHash is the hash explorers show, ' +
|
|
1301
|
-
'null until indexed. ' +
|
|
1302
|
-
'Sponsor jobs: failed + errorCode CHAIN_EXECUTION_FAILED = the transaction IS on-chain but ' +
|
|
1303
|
-
'the contract call did not apply (same-contract conflict) -> rebuild and sponsor again; ' +
|
|
1304
|
-
'reconciliation_required = broadcast outcome unknown to the server yet, the transaction ' +
|
|
1305
|
-
'identifier is in the error message, the server keeps resolving it via the indexer. ' +
|
|
1306
|
-
'Batches: once confirmed, chainSegments ([{ segment, calls, applied }]) says which calls ' +
|
|
1307
|
-
'applied; resend only the ones with applied false. Swaps: the result carries swap ' +
|
|
1308
|
-
'{ gives, wants }, what was exchanged from the maker\'s side. Mints: tokenType.',
|
|
1309
|
-
inputSchema: {
|
|
1310
|
-
jobId: z.string().uuid().describe('Job id returned by a submit action'),
|
|
1311
|
-
sessionId: z.string().uuid()
|
|
1312
|
-
.describe('Wallet session id the job belongs to; for sponsor jobs the sessionId RETURNED by the sponsor call'),
|
|
1313
|
-
waitMs: z.number().int().min(0).max(25_000).optional()
|
|
1314
|
-
.describe('ms the server waits for the job to settle before it answers; default 20000, 0 answers at once'),
|
|
820
|
+
since: z.string().datetime().optional().describe('Agent grants only: ISO timestamp, start of the window; default 30 days back'),
|
|
821
|
+
until: z.string().datetime().optional().describe('Agent grants only: ISO timestamp, end of the window'),
|
|
1315
822
|
},
|
|
1316
823
|
}, run(async (args) => {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
}
|
|
1323
|
-
catch (err) {
|
|
1324
|
-
if (!(waitMs > 0 && err instanceof NightgateApiError && err.status === 400 && /waitMs/.test(err.message)))
|
|
1325
|
-
throw err;
|
|
1326
|
-
jobWait = false;
|
|
1327
|
-
job = await client.callAction('getJobStatus', ask);
|
|
1328
|
-
}
|
|
1329
|
-
settleHalfJob(args.jobId, job?.status);
|
|
1330
|
-
return job;
|
|
824
|
+
if (!config?.token && !config?.username)
|
|
825
|
+
throw new Error('get_usage needs a key: set ODATANO_ACCESS_KEY (an oda_... key from https://api.odatano.dev)');
|
|
826
|
+
if (config && isGatewayKey(config))
|
|
827
|
+
return client.gatewayMe();
|
|
828
|
+
return client.callFunction('getGrantUsage', { grantId: null, since: args.since ?? null, until: args.until ?? null });
|
|
1331
829
|
}));
|
|
1332
830
|
}
|
|
1333
831
|
/** listSwapOffers wants all seven parameters, null for the unused ones. */
|
|
@@ -1354,20 +852,44 @@ function listOffers(client, args) {
|
|
|
1354
852
|
*/
|
|
1355
853
|
export function wrapHandler(_client) {
|
|
1356
854
|
return function run(fn) {
|
|
1357
|
-
return async (args) => {
|
|
855
|
+
return async (args, extra) => {
|
|
856
|
+
const notice = {};
|
|
857
|
+
const stopProgress = startProgress(extra);
|
|
1358
858
|
try {
|
|
1359
|
-
const result = await fn(args);
|
|
1360
|
-
|
|
859
|
+
const result = await seedNotice.run(notice, () => fn(args, extra));
|
|
860
|
+
const seed = notice.createdFile ? seedCreated(notice.createdFile) : undefined;
|
|
861
|
+
const out = seed && result && typeof result === 'object' && !Array.isArray(result) ? { ...result, seed } : result;
|
|
862
|
+
return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] };
|
|
1361
863
|
}
|
|
1362
864
|
catch (err) {
|
|
865
|
+
const seed = notice.createdFile ? seedCreated(notice.createdFile) : undefined;
|
|
1363
866
|
if (err instanceof NightgateApiError) {
|
|
1364
|
-
const detail = { httpStatus: err.status, code: err.code ?? null, message: err.message, ...(err.detail ?? {}) };
|
|
867
|
+
const detail = { httpStatus: err.status, code: err.code ?? null, message: err.message, ...(err.detail ?? {}), ...(seed ? { seed } : {}) };
|
|
1365
868
|
return { content: [{ type: 'text', text: JSON.stringify(detail, null, 2) }], isError: true };
|
|
1366
869
|
}
|
|
1367
870
|
const message = err instanceof Error ? err.message : String(err);
|
|
1368
|
-
return { content: [{ type: 'text', text: message }], isError: true };
|
|
871
|
+
return { content: [{ type: 'text', text: seed ? `${message}\n${JSON.stringify({ seed }, null, 2)}` : message }], isError: true };
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
stopProgress();
|
|
1369
875
|
}
|
|
1370
876
|
};
|
|
1371
877
|
};
|
|
1372
878
|
}
|
|
879
|
+
/** Progress every 5 s while a call runs, when the client asked for it with a progress token. */
|
|
880
|
+
function startProgress(extra) {
|
|
881
|
+
const token = extra?._meta?.progressToken;
|
|
882
|
+
if (token === undefined || !extra)
|
|
883
|
+
return () => { };
|
|
884
|
+
const t0 = Date.now();
|
|
885
|
+
let n = 0;
|
|
886
|
+
const timer = setInterval(() => {
|
|
887
|
+
extra.sendNotification({
|
|
888
|
+
method: 'notifications/progress',
|
|
889
|
+
params: { progressToken: token, progress: ++n, message: `working, ${Math.round((Date.now() - t0) / 1000)} s` },
|
|
890
|
+
}).catch(() => undefined);
|
|
891
|
+
}, 5_000);
|
|
892
|
+
timer.unref?.();
|
|
893
|
+
return () => clearInterval(timer);
|
|
894
|
+
}
|
|
1373
895
|
//# sourceMappingURL=tools.js.map
|