@odatano/nightgate-mcp 0.8.0 → 0.9.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/swap.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { NightgateMcpConfig } from './config.js';
2
+ export interface SwapLeg {
3
+ tokenType: string;
4
+ amount: string;
5
+ }
6
+ export interface SwapTermsOut {
7
+ gives: SwapLeg;
8
+ wants: SwapLeg;
9
+ inputs?: number;
10
+ outputs?: number;
11
+ }
12
+ interface TxSwapModule {
13
+ createSwapWallet: (opts: Record<string, unknown>) => Promise<any>;
14
+ readSwapTerms: (tx: unknown) => any;
15
+ decodeOffer: (input: string) => Promise<{
16
+ tx: unknown;
17
+ bound: boolean;
18
+ bytes: Uint8Array;
19
+ }>;
20
+ }
21
+ /** What an offer gives and wants, read from the transaction it carries. No wallet, no network. */
22
+ export declare function readOffer(offer: string): Promise<SwapTermsOut & {
23
+ bound: boolean;
24
+ serializedBytes: number;
25
+ }>;
26
+ export declare function swapWalletInfo(config: NightgateMcpConfig): Promise<Record<string, unknown>>;
27
+ export interface BuiltOffer {
28
+ id: string;
29
+ offer?: string;
30
+ halfB64: string;
31
+ bound: boolean;
32
+ serializedBytes: number;
33
+ terms: SwapTermsOut;
34
+ buildMs: number;
35
+ }
36
+ /** One half of a swap from this wallet; bound with its offer file unless `bind` is false. */
37
+ export declare function buildOffer(config: NightgateMcpConfig, input: {
38
+ give: SwapLeg;
39
+ want: SwapLeg;
40
+ bind?: boolean;
41
+ }): Promise<BuiltOffer>;
42
+ export interface TakenOfferOut {
43
+ id: string;
44
+ makerHalfB64: string;
45
+ takerHalfB64: string;
46
+ bound: boolean;
47
+ terms: SwapTermsOut;
48
+ buildMs: number;
49
+ }
50
+ /** The mirror half of an offer from this wallet, after its terms were read and compared with `expect`. */
51
+ export declare function takeOffer(config: NightgateMcpConfig, input: {
52
+ offer: string;
53
+ expect?: {
54
+ gives: SwapLeg;
55
+ wants: SwapLeg;
56
+ };
57
+ }): Promise<TakenOfferOut>;
58
+ /** Releases the coins of a half built here that is not going to be handed over. */
59
+ export declare function revertHalf(config: NightgateMcpConfig, id: string): Promise<{
60
+ id: string;
61
+ reverted: true;
62
+ }>;
63
+ /** Both halves of an offer taken here, for a submission by id. */
64
+ export declare function takenHalves(id: string): {
65
+ makerHalfB64: string;
66
+ takerHalfB64: string;
67
+ };
68
+ /** Remembers the sponsor job a half went into. An id this process does not hold is ignored. */
69
+ export declare function trackHalfJob(id: string, jobId: unknown): void;
70
+ /** A landed swap spent its coins: its half is no longer pending. A failed one stays, so it can be reverted. */
71
+ export declare function settleHalfJob(jobId: string, status: unknown): void;
72
+ /** Test seam / shutdown. */
73
+ export declare function closeSwapWallet(): Promise<void>;
74
+ /** Test seam: replace the module the swap functions load. */
75
+ export declare function __setSwapModuleForTests(mod: TxSwapModule | undefined): void;
76
+ export {};
package/dist/swap.js ADDED
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Shielded swaps, the local side: one wallet that holds the shielded coins of
3
+ * `NIGHTGATE_SEED_HEX` builds swap halves and takes offers. It wraps the swap
4
+ * wallet of `@odatano/nightgate-tx` (0.8.0 or later); the fee is the sponsor's,
5
+ * so the wallet needs neither NIGHT nor dust.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
9
+ import { dirname } from 'node:path';
10
+ let modulePromise;
11
+ async function loadSwapModule() {
12
+ modulePromise ??= (async () => {
13
+ let tx;
14
+ try {
15
+ tx = await import('@odatano/nightgate-tx/txbuilder');
16
+ }
17
+ catch (err) {
18
+ modulePromise = undefined;
19
+ throw new Error('swaps need @odatano/nightgate-tx >= 0.8.0 next to the MCP server (npm install @odatano/nightgate-tx): ' +
20
+ (err instanceof Error ? err.message : String(err)));
21
+ }
22
+ if (typeof tx.createSwapWallet !== 'function' || typeof tx.decodeOffer !== 'function') {
23
+ modulePromise = undefined;
24
+ throw new Error('swaps need @odatano/nightgate-tx >= 0.8.0; the installed version has no swap wallet (npm install @odatano/nightgate-tx@latest)');
25
+ }
26
+ return { createSwapWallet: tx.createSwapWallet, readSwapTerms: tx.readSwapTerms, decodeOffer: tx.decodeOffer };
27
+ })();
28
+ return modulePromise;
29
+ }
30
+ function defaults(network) {
31
+ return {
32
+ indexerHttpUrl: `https://indexer.${network}.midnight.network/api/v4/graphql`,
33
+ indexerWsUrl: `wss://indexer.${network}.midnight.network/api/v4/graphql/ws`,
34
+ };
35
+ }
36
+ const termsOut = (terms) => ({
37
+ gives: { tokenType: String(terms.gives.tokenType), amount: String(terms.gives.amount) },
38
+ wants: { tokenType: String(terms.wants.tokenType), amount: String(terms.wants.amount) },
39
+ ...(Number.isInteger(terms.inputs) ? { inputs: terms.inputs } : {}),
40
+ ...(Number.isInteger(terms.outputs) ? { outputs: terms.outputs } : {}),
41
+ });
42
+ /** What an offer gives and wants, read from the transaction it carries. No wallet, no network. */
43
+ export async function readOffer(offer) {
44
+ const { decodeOffer, readSwapTerms } = await loadSwapModule();
45
+ const decoded = await decodeOffer(offer);
46
+ return { ...termsOut(readSwapTerms(decoded.tx)), bound: decoded.bound, serializedBytes: decoded.bytes.length };
47
+ }
48
+ let open;
49
+ /** Halves built here whose swap has not landed, by id. */
50
+ const pending = new Map();
51
+ const halfId = (halfB64) => createHash('sha256').update(Buffer.from(halfB64, 'base64')).digest('hex').slice(0, 32);
52
+ async function readState(path) {
53
+ if (!path)
54
+ return undefined;
55
+ try {
56
+ const text = (await readFile(path, 'utf8')).trim();
57
+ return text.length > 0 ? text : undefined;
58
+ }
59
+ catch (err) {
60
+ if (err?.code === 'ENOENT')
61
+ return undefined;
62
+ throw new Error(`swap wallet state ${path} is not readable: ${err.message}`);
63
+ }
64
+ }
65
+ /** The state holds the wallet's coins: written next to itself, then renamed, readable by the owner only. */
66
+ async function writeState(path, wallet) {
67
+ if (!path)
68
+ return;
69
+ const state = String(await wallet.serializeState());
70
+ await mkdir(dirname(path), { recursive: true });
71
+ const tmp = `${path}.part`;
72
+ await writeFile(tmp, state, { mode: 0o600 });
73
+ await rename(tmp, path);
74
+ await chmod(path, 0o600).catch(() => { });
75
+ }
76
+ async function openWallet(config) {
77
+ if (!config.seedHex) {
78
+ throw new Error('swaps need NIGHTGATE_SEED_HEX in the MCP server environment; it is never a tool argument');
79
+ }
80
+ open ??= (async () => {
81
+ try {
82
+ const { createSwapWallet } = await loadSwapModule();
83
+ const d = defaults(config.network);
84
+ const walletState = await readState(config.swapStateFile);
85
+ const wallet = await createSwapWallet({
86
+ seedHex: config.seedHex,
87
+ networkId: config.network,
88
+ indexerHttpUrl: config.indexerHttpUrl ?? d.indexerHttpUrl,
89
+ indexerWsUrl: config.indexerWsUrl ?? d.indexerWsUrl,
90
+ ...(config.nodeUrl ? { nodeUrl: config.nodeUrl } : {}),
91
+ ...(config.proofServerUrl ? { provingMode: 'server', proofServerUrl: config.proofServerUrl } : {}),
92
+ ...(config.swapMaxInputs ? { maxInputs: config.swapMaxInputs } : {}),
93
+ ...(walletState ? { walletState } : {}),
94
+ });
95
+ const entry = { wallet, synced: Promise.resolve(), isSynced: false, syncError: null, startedAt: Date.now(), resumed: !!walletState };
96
+ entry.synced = (async () => {
97
+ try {
98
+ await wallet.sync();
99
+ await writeState(config.swapStateFile, wallet).catch((err) => console.error(`nightgate-mcp: swap wallet state not saved: ${err.message}`));
100
+ entry.isSynced = true;
101
+ }
102
+ catch (err) {
103
+ entry.syncError = err instanceof Error ? err : new Error(String(err));
104
+ }
105
+ })();
106
+ return entry;
107
+ }
108
+ catch (err) {
109
+ open = undefined;
110
+ throw err;
111
+ }
112
+ })();
113
+ return open;
114
+ }
115
+ /** The wallet once it has caught up; a wallet that needs longer than `waitMs` says so instead of blocking the call. */
116
+ async function syncedWallet(config, waitMs) {
117
+ const entry = await openWallet(config);
118
+ if (!entry.isSynced && !entry.syncError) {
119
+ await Promise.race([entry.synced, new Promise((resolve) => setTimeout(resolve, waitMs))]);
120
+ }
121
+ if (entry.syncError) {
122
+ open = undefined;
123
+ throw new Error(`the swap wallet could not sync: ${entry.syncError.message}`);
124
+ }
125
+ if (!entry.isSynced) {
126
+ const seconds = Math.round((Date.now() - entry.startedAt) / 1000);
127
+ throw new Error(`the swap wallet is still syncing its shielded coins (${seconds} s so far; a first sync from genesis takes about 5 minutes` +
128
+ `${config.swapStateFile ? '' : ', set NIGHTGATE_SWAP_STATE_FILE to resume after a restart'}); call again shortly`);
129
+ }
130
+ return entry.wallet;
131
+ }
132
+ /** How long a tool call waits for a wallet that is still syncing. */
133
+ const SYNC_WAIT_MS = 20_000;
134
+ export async function swapWalletInfo(config) {
135
+ const entry = await openWallet(config);
136
+ const { wallet } = entry;
137
+ const base = {
138
+ address: String(wallet.address),
139
+ coinPublicKey: String(wallet.coinPublicKey),
140
+ encryptionPublicKey: String(wallet.encryptionPublicKey),
141
+ network: config.network,
142
+ maxInputs: Number(wallet.maxInputs),
143
+ provingMode: String(wallet.provingMode),
144
+ synced: entry.isSynced,
145
+ resumedFromState: entry.resumed,
146
+ };
147
+ if (entry.syncError)
148
+ throw new Error(`the swap wallet could not sync: ${entry.syncError.message}`);
149
+ if (!entry.isSynced) {
150
+ await Promise.race([entry.synced, new Promise((resolve) => setTimeout(resolve, SYNC_WAIT_MS))]);
151
+ if (!entry.isSynced)
152
+ return { ...base, syncingSeconds: Math.round((Date.now() - entry.startedAt) / 1000) };
153
+ }
154
+ const coins = await wallet.coins();
155
+ const byType = new Map();
156
+ for (const c of coins) {
157
+ const t = byType.get(c.tokenType) ?? { balance: 0n, coins: 0 };
158
+ byType.set(c.tokenType, { balance: t.balance + c.amount, coins: t.coins + 1 });
159
+ }
160
+ const tokens = [];
161
+ for (const [tokenType, t] of byType) {
162
+ tokens.push({ tokenType, balance: t.balance.toString(), coins: t.coins, spendable: String(await wallet.spendable(tokenType)) });
163
+ }
164
+ const pendingHalves = [...pending.entries()].map(([id, p]) => ({ id, role: p.role, ...(p.jobId ? { jobId: p.jobId } : {}), ...p.terms }));
165
+ return { ...base, synced: true, tokens, pendingHalves };
166
+ }
167
+ /** One half of a swap from this wallet; bound with its offer file unless `bind` is false. */
168
+ export async function buildOffer(config, input) {
169
+ const t0 = Date.now();
170
+ const wallet = await syncedWallet(config, SYNC_WAIT_MS);
171
+ const half = await wallet.buildHalf({ give: input.give, want: input.want, bind: input.bind !== false });
172
+ const terms = termsOut(half.terms);
173
+ const id = halfId(half.halfB64);
174
+ pending.set(id, { revert: half.revert, terms, role: 'maker' });
175
+ await writeState(config.swapStateFile, wallet).catch(() => { });
176
+ return {
177
+ id, ...(half.offer ? { offer: half.offer } : {}), halfB64: half.halfB64, bound: half.bound === true,
178
+ serializedBytes: Number(half.serializedBytes), terms, buildMs: Date.now() - t0,
179
+ };
180
+ }
181
+ /** The mirror half of an offer from this wallet, after its terms were read and compared with `expect`. */
182
+ export async function takeOffer(config, input) {
183
+ const t0 = Date.now();
184
+ const wallet = await syncedWallet(config, SYNC_WAIT_MS);
185
+ const taken = await wallet.takeOffer({ offer: input.offer, ...(input.expect ? { expect: input.expect } : {}) });
186
+ const terms = termsOut(taken.terms);
187
+ const id = halfId(taken.takerHalfB64);
188
+ pending.set(id, { revert: taken.revert, terms, role: 'taker', halves: { makerHalfB64: taken.makerHalfB64, takerHalfB64: taken.takerHalfB64 } });
189
+ await writeState(config.swapStateFile, wallet).catch(() => { });
190
+ return { id, makerHalfB64: taken.makerHalfB64, takerHalfB64: taken.takerHalfB64, bound: taken.bound === true, terms, buildMs: Date.now() - t0 };
191
+ }
192
+ /** Releases the coins of a half built here that is not going to be handed over. */
193
+ export async function revertHalf(config, id) {
194
+ const half = pending.get(id);
195
+ if (!half)
196
+ throw new Error(`no half with id ${id} was built by this server process (ids are listed by get_swap_wallet)`);
197
+ await half.revert();
198
+ pending.delete(id);
199
+ const entry = await openWallet(config);
200
+ await writeState(config.swapStateFile, entry.wallet).catch(() => { });
201
+ return { id, reverted: true };
202
+ }
203
+ /** Both halves of an offer taken here, for a submission by id. */
204
+ export function takenHalves(id) {
205
+ const half = pending.get(id);
206
+ if (!half)
207
+ throw new Error(`no half with id ${id} is held by this server process (ids are listed by get_swap_wallet)`);
208
+ if (!half.halves)
209
+ throw new Error(`half ${id} is an offer built here, not a taken one: pass both halves`);
210
+ return half.halves;
211
+ }
212
+ /** Remembers the sponsor job a half went into. An id this process does not hold is ignored. */
213
+ export function trackHalfJob(id, jobId) {
214
+ const half = pending.get(id);
215
+ if (half && typeof jobId === 'string')
216
+ half.jobId = jobId;
217
+ }
218
+ /** A landed swap spent its coins: its half is no longer pending. A failed one stays, so it can be reverted. */
219
+ export function settleHalfJob(jobId, status) {
220
+ if (status !== 'succeeded')
221
+ return;
222
+ for (const [id, half] of pending)
223
+ if (half.jobId === jobId)
224
+ pending.delete(id);
225
+ }
226
+ /** Test seam / shutdown. */
227
+ export async function closeSwapWallet() {
228
+ const current = open;
229
+ open = undefined;
230
+ pending.clear();
231
+ if (!current)
232
+ return;
233
+ try {
234
+ await (await current).wallet.close?.();
235
+ }
236
+ catch { /* best effort */ }
237
+ }
238
+ /** Test seam: replace the module the swap functions load. */
239
+ export function __setSwapModuleForTests(mod) {
240
+ modulePromise = mod ? Promise.resolve(mod) : undefined;
241
+ open = undefined;
242
+ pending.clear();
243
+ }
244
+ //# sourceMappingURL=swap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swap.js","sourceRoot":"","sources":["../src/swap.ts"],"names":[],"mappings":"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"}
package/dist/tools.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { int64, NightgateApiError } from './client.js';
3
3
  import { BUILDABLE_CALLS, BUILDABLE_ARTIFACTS, buildSponsorable, attesterIdentity } from './builder.js';
4
+ import { buildOffer, readOffer, revertHalf, settleHalfJob, swapWalletInfo, takenHalves, takeOffer, trackHalfJob } from './swap.js';
4
5
  const HEX64 = /^[0-9a-fA-F]{64}$/;
5
6
  const hex64 = (what) => z.string().regex(HEX64, `${what} must be 64 hex characters`);
6
7
  /** Scaled non-negative integer, as string (preferred, exact) or JS number. */
@@ -19,6 +20,21 @@ const setDirs = z.array(z.boolean()).length(6)
19
20
  const POLL_HINT = 'Async: returns { jobId, status } immediately; poll get_job_status until succeeded or failed.';
20
21
  /** NIGHTGATE's platform sponsor pool id (0.17.2+): the server picks a free pool sponsor. */
21
22
  const PLATFORM_POOL_SENTINEL = '00000000-0000-0000-0000-706f6f6c0000';
23
+ /** One side of a swap: a raw token type and an amount in atoms. */
24
+ const swapLeg = (what) => z.object({
25
+ tokenType: hex64(`${what}.tokenType`).describe('Raw token type (64 hex), what derive_token_type returns'),
26
+ amount: z.union([
27
+ z.string().regex(/^[1-9]\d*$/, `${what}.amount must be a positive integer (decimal string)`),
28
+ z.number().int().positive(),
29
+ ]).describe('Amount in atoms; a decimal string is exact'),
30
+ });
31
+ const legOut = (leg) => ({ tokenType: leg.tokenType.toLowerCase(), amount: String(leg.amount) });
32
+ /** A swap half as it is handed over: offer file text or base64 of the serialized transaction. */
33
+ const swapHalf = (what) => z.string().min(16)
34
+ .describe(`${what}: offer file text (swapoffer1...) or base64 of the serialized, proven transaction`);
35
+ const SWAP_HINT = 'A swap settles two shielded tokens between two wallets without a contract: each side builds one half ' +
36
+ '(it spends the coin it gives and creates the coin it wants), the two mirrored halves merge into one transaction, ' +
37
+ 'and a sponsor pays the fee.';
22
38
  /**
23
39
  * Per-slot salt of a proof field. Content-tree leaves are SALTED, so every
24
40
  * single-field proof needs the slot's salt from prepare_document_proof
@@ -809,8 +825,10 @@ export function registerTools(server, client, config) {
809
825
  }));
810
826
  server.registerTool('get_attester_identity', {
811
827
  description: 'The attester id this MCP server builds under (derived from NIGHTGATE_SEED_HEX via the ' +
812
- 'txbuilder), plus the network. Use it to check verify_attestation results against the ' +
813
- 'identity that will appear on-chain; builds nothing and submits nothing.',
828
+ 'txbuilder), plus the network, the shielded address and the two public shielded keys ' +
829
+ '(what a sender needs to create a coin for this wallet). Use it to check ' +
830
+ 'verify_attestation results against the identity that will appear on-chain; builds ' +
831
+ 'nothing and submits nothing.',
814
832
  inputSchema: {},
815
833
  }, run(async () => {
816
834
  if (!config)
@@ -870,6 +888,148 @@ export function registerTools(server, client, config) {
870
888
  sponsorSessionId: args.sponsorSessionId,
871
889
  idempotencyKey: args.idempotencyKey,
872
890
  })));
891
+ server.registerTool('get_swap_wallet', {
892
+ description: 'The shielded wallet this MCP server swaps from (seed from NIGHTGATE_SEED_HEX): address, ' +
893
+ 'public keys, and per token type the balance, the number of free coins and `spendable`, ' +
894
+ 'the most ONE swap half can give (what its largest coins hold, up to maxInputs coins). ' +
895
+ 'Also lists the halves built here that are not handed over yet. The wallet syncs its ' +
896
+ 'shielded coins only; a first sync from genesis takes about 5 minutes, during which the ' +
897
+ 'answer is { synced: false, syncingSeconds }: call again. Spends nothing.',
898
+ inputSchema: {},
899
+ }, run(async () => {
900
+ if (!config)
901
+ throw new Error('swaps are not configured for this server instance');
902
+ return swapWalletInfo(config);
903
+ }));
904
+ server.registerTool('read_swap_offer', {
905
+ description: SWAP_HINT + ' This reads what an offer gives and wants FROM THE TRANSACTION it carries, ' +
906
+ 'never from what its maker says: gives / wants (token type and amount, from the ' +
907
+ "maker's side), the coins it carries, and whether it is bound. Refuses anything that is " +
908
+ 'not a plain swap half (a contract call, unshielded value, more than one token type ' +
909
+ 'given or wanted). Needs no wallet and no network. Check the terms here before take_swap_offer.',
910
+ inputSchema: {
911
+ offer: swapHalf('The offer'),
912
+ },
913
+ }, run(async (args) => readOffer(args.offer)));
914
+ server.registerTool('build_swap_offer', {
915
+ description: SWAP_HINT + ' This builds and proves ONE half from this server\'s wallet: it spends `give` ' +
916
+ 'and creates `want` (and the change) for this wallet. Returns the offer file ' +
917
+ '(`offer`, text starting with swapoffer1, about 25 000 characters) to publish, its `id`, ' +
918
+ 'and the terms read back from the built transaction. Anyone holding the wanted token can ' +
919
+ 'take the offer; the first swap that lands consumes it. The coins stay reserved until ' +
920
+ 'the swap lands: withdraw an offer that is not going to be used with revert_swap_offer. ' +
921
+ 'An offer refers to a recent chain state and expires with it, so publish it promptly. ' +
922
+ '`give.amount` may not exceed `spendable` of get_swap_wallet. Proving takes about 200 s ' +
923
+ 'in-process, 11 to 17 s on a proof server (NIGHTGATE_PROOF_SERVER_URL). Spends no fee.',
924
+ inputSchema: {
925
+ give: swapLeg('give'),
926
+ want: swapLeg('want'),
927
+ bind: z.boolean().optional()
928
+ .describe('true (default): bound, with its offer file. false: unbound, base64 only (halfB64), for a taker that builds unbound too'),
929
+ },
930
+ }, run(async (args) => {
931
+ if (!config)
932
+ throw new Error('swaps are not configured for this server instance');
933
+ return buildOffer(config, { give: legOut(args.give), want: legOut(args.want), bind: args.bind });
934
+ }));
935
+ server.registerTool('take_swap_offer', {
936
+ description: SWAP_HINT + ' This takes an offer: it reads the offer\'s terms from the transaction, ' +
937
+ 'refuses when they differ from `expect`, builds and proves the mirror half from this ' +
938
+ 'server\'s wallet (it spends what the offer wants and creates what the offer gives). ' +
939
+ 'With sponsorSessionId the two halves go to the sponsor at once and the answer is the ' +
940
+ 'job ({ jobId, status, sessionId, id, terms }): poll get_job_status with the returned ' +
941
+ 'sessionId. Without it the answer carries both halves for sponsor_swap. ALWAYS pass ' +
942
+ '`expect` with the terms you agreed to: an offer is text from someone else. A sponsor ' +
943
+ 'that refuses the swap leaves the proven half held under `halfId`: send it again with ' +
944
+ 'sponsor_swap({ halfId, sponsorSessionId }) or release its coins with revert_swap_offer. ' +
945
+ 'If the offer was taken by somebody else first, the job fails: revert_swap_offer.',
946
+ inputSchema: {
947
+ offer: swapHalf('The offer to take'),
948
+ expect: z.object({ gives: swapLeg('expect.gives'), wants: swapLeg('expect.wants') }).optional()
949
+ .describe("The terms you agreed to, from the MAKER's side: what the offer gives and what it wants"),
950
+ sponsorSessionId: z.string().uuid().optional()
951
+ .describe('Sponsor session or the platform sponsor POOL id ' + `${PLATFORM_POOL_SENTINEL}` + '; when given the swap is submitted at once'),
952
+ idempotencyKey: z.string().optional().describe('Dedupes retries of the same submission'),
953
+ },
954
+ }, run(async (args) => {
955
+ if (!config)
956
+ throw new Error('swaps are not configured for this server instance');
957
+ const taken = await takeOffer(config, {
958
+ offer: args.offer,
959
+ ...(args.expect ? { expect: { gives: legOut(args.expect.gives), wants: legOut(args.expect.wants) } } : {}),
960
+ });
961
+ if (!args.sponsorSessionId)
962
+ return taken;
963
+ let job;
964
+ try {
965
+ job = await client.callAction('sponsorSwap', {
966
+ makerHalfB64: taken.makerHalfB64,
967
+ takerHalfB64: taken.takerHalfB64,
968
+ sponsorSessionId: args.sponsorSessionId,
969
+ idempotencyKey: args.idempotencyKey,
970
+ });
971
+ }
972
+ catch (err) {
973
+ if (!(err instanceof NightgateApiError))
974
+ throw err;
975
+ throw new NightgateApiError(err.status, err.code, err.message, { ...(err.detail ?? {}), halfId: taken.id });
976
+ }
977
+ trackHalfJob(taken.id, job.jobId);
978
+ return { ...job, id: taken.id, terms: taken.terms, bound: taken.bound, buildMs: taken.buildMs };
979
+ }));
980
+ server.registerTool('revert_swap_offer', {
981
+ description: 'Release the coins of a swap half this server built (build_swap_offer or take_swap_offer) ' +
982
+ 'that is not going to be used. Until then, or until the swap lands, the coins count as ' +
983
+ 'spent in this wallet. It does not recall an offer somebody already holds: to make a ' +
984
+ 'published offer worthless, spend its coins in another swap. Ids are listed by ' +
985
+ 'get_swap_wallet; a half whose swap landed leaves the list when get_job_status reports it.',
986
+ inputSchema: {
987
+ id: z.string().regex(/^[0-9a-f]{32}$/, 'id must be the 32 hex characters a swap tool returned'),
988
+ },
989
+ }, run(async (args) => {
990
+ if (!config)
991
+ throw new Error('swaps are not configured for this server instance');
992
+ return revertHalf(config, args.id);
993
+ }));
994
+ server.registerTool('sponsor_swap', {
995
+ description: SWAP_HINT + ' This hands the two halves to the sponsor, which checks them, merges them, ' +
996
+ 'pays the dust and submits: each half carries an offer and nothing else, gives exactly ' +
997
+ 'one token type and wants exactly one other, both on the sponsor\'s token allow-list and ' +
998
+ 'never NIGHT, at most 4 inputs and 2 outputs per half, and the halves mirror each other ' +
999
+ 'in types and amounts. A half may be bound or unbound and is passed as offer file text ' +
1000
+ 'or base64. Off on a server by default (NIGHTGATE_SPONSOR_ALLOW_SWAPS); an agent grant ' +
1001
+ 'needs sponsorSwap in its allowedActions. Job result { txHash, swap: { gives, wants } }. ' +
1002
+ 'An offer fills once: if it was taken first by somebody else the job fails. Pass both ' +
1003
+ 'halves, or `halfId` alone for an offer this server took (take_swap_offer keeps both ' +
1004
+ 'halves). Poll get_job_status with the sessionId RETURNED by this call. ' + POLL_HINT,
1005
+ inputSchema: {
1006
+ makerHalf: swapHalf("The maker's half (the offer)").optional(),
1007
+ takerHalf: swapHalf("The taker's half").optional(),
1008
+ sponsorSessionId: z.string().uuid()
1009
+ .describe('Wallet session that pays the dust and submits, or the platform sponsor POOL id ' +
1010
+ `${PLATFORM_POOL_SENTINEL}`),
1011
+ idempotencyKey: z.string().optional()
1012
+ .describe('Dedupes retries of the SAME two halves'),
1013
+ halfId: z.string().regex(/^[0-9a-f]{32}$/, 'halfId must be the 32 hex characters a swap tool returned').optional()
1014
+ .describe('Id of the half this server built for the swap: alone it names a taken offer, next to both halves it ties the job to the half'),
1015
+ },
1016
+ }, run(async (args) => {
1017
+ if (!!args.makerHalf !== !!args.takerHalf)
1018
+ throw new Error('sponsor_swap needs makerHalf AND takerHalf, or halfId alone');
1019
+ if (!args.makerHalf && !args.halfId)
1020
+ throw new Error('sponsor_swap needs makerHalf and takerHalf, or the halfId of an offer this server took');
1021
+ const halves = args.makerHalf && args.takerHalf
1022
+ ? { makerHalfB64: args.makerHalf, takerHalfB64: args.takerHalf }
1023
+ : takenHalves(args.halfId);
1024
+ const job = await client.callAction('sponsorSwap', {
1025
+ ...halves,
1026
+ sponsorSessionId: args.sponsorSessionId,
1027
+ idempotencyKey: args.idempotencyKey,
1028
+ });
1029
+ if (args.halfId)
1030
+ trackHalfJob(args.halfId, job?.jobId);
1031
+ return job;
1032
+ }));
873
1033
  server.registerTool('derive_token_type', {
874
1034
  description: 'Derive the raw token type a minting contract produces (compute-only, no wallet, no ' +
875
1035
  'chain access). A custom token is addressed by rawTokenType(domainSeparator, ' +
@@ -893,16 +1053,23 @@ export function registerTools(server, client, config) {
893
1053
  'Sponsor jobs: failed + errorCode CHAIN_EXECUTION_FAILED = the transaction IS on-chain but ' +
894
1054
  'the contract call did not apply (same-contract conflict) -> rebuild and sponsor again; ' +
895
1055
  'reconciliation_required = broadcast outcome unknown to the server yet, the transaction ' +
896
- 'identifier is in the error message, the server keeps resolving it via the indexer.',
1056
+ 'identifier is in the error message, the server keeps resolving it via the indexer. ' +
1057
+ 'Batches: once confirmed, chainSegments ([{ segment, calls, applied }]) says which calls ' +
1058
+ 'applied; resend only the ones with applied false. Swaps: the result carries swap ' +
1059
+ '{ gives, wants }, what was exchanged from the maker\'s side.',
897
1060
  inputSchema: {
898
1061
  jobId: z.string().uuid().describe('Job id returned by a submit action'),
899
1062
  sessionId: z.string().uuid()
900
1063
  .describe('Wallet session id the job belongs to; for sponsor jobs the sessionId RETURNED by the sponsor call'),
901
1064
  },
902
- }, run(async (args) => client.callAction('getJobStatus', {
903
- jobId: args.jobId,
904
- sessionId: args.sessionId,
905
- })));
1065
+ }, run(async (args) => {
1066
+ const job = await client.callAction('getJobStatus', {
1067
+ jobId: args.jobId,
1068
+ sessionId: args.sessionId,
1069
+ });
1070
+ settleHalfJob(args.jobId, job?.status);
1071
+ return job;
1072
+ }));
906
1073
  }
907
1074
  /**
908
1075
  * Uniform handler wrapper: JSON success payloads, API errors reported as