@toon-protocol/client-mcp 0.10.9 → 0.11.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/app/index.html +86 -81
- package/dist/{chunk-UHITXU5V.js → chunk-CMGJ3NFT.js} +11 -3
- package/dist/chunk-CMGJ3NFT.js.map +1 -0
- package/dist/{chunk-X4GKWZQP.js → chunk-JPQ4VCCF.js} +64 -14
- package/dist/chunk-JPQ4VCCF.js.map +1 -0
- package/dist/{chunk-CS4B3GET.js → chunk-KVK6OZVD.js} +139 -18
- package/dist/chunk-KVK6OZVD.js.map +1 -0
- package/dist/daemon.js +2 -2
- package/dist/index.d.ts +73 -10
- package/dist/index.js +3 -3
- package/dist/mcp.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-CS4B3GET.js.map +0 -1
- package/dist/chunk-UHITXU5V.js.map +0 -1
- package/dist/chunk-X4GKWZQP.js.map +0 -1
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
isEventExpired,
|
|
16
16
|
parseIlpPeerInfo,
|
|
17
17
|
readConfigFile
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-CMGJ3NFT.js";
|
|
19
19
|
import {
|
|
20
20
|
__require
|
|
21
21
|
} from "./chunk-F22GNSF6.js";
|
|
@@ -1398,17 +1398,37 @@ function delay(ms) {
|
|
|
1398
1398
|
|
|
1399
1399
|
// src/daemon/client-runner.ts
|
|
1400
1400
|
var MERGED_BUFFER = 5e3;
|
|
1401
|
+
var BALANCES_READ_TIMEOUT_MS = 5e3;
|
|
1402
|
+
var BALANCES_READ_ATTEMPTS = 2;
|
|
1401
1403
|
var ClientRunner = class {
|
|
1402
1404
|
config;
|
|
1403
1405
|
createClient;
|
|
1404
1406
|
createRelay;
|
|
1405
1407
|
log;
|
|
1406
1408
|
targetsPath;
|
|
1409
|
+
/**
|
|
1410
|
+
* Identity-level chain-read client. Reading your OWN on-chain wallet balance is
|
|
1411
|
+
* a pure (wallet keys + chain RPC) operation that has nothing to do with the
|
|
1412
|
+
* ILP/payment peer, so it lives at the daemon level rather than inside an apex.
|
|
1413
|
+
* Built once from the daemon's own `toonClientConfig` (the same keys + chain
|
|
1414
|
+
* RPC config every apex shares) and REUSED as the default apex's client, so a
|
|
1415
|
+
* funded apex's `start()` (which derives Solana/Mina keys) also benefits this
|
|
1416
|
+
* reader. `getBalances` uses it directly, so balances work even with zero
|
|
1417
|
+
* apexes registered (follow-up to #199/#200).
|
|
1418
|
+
*/
|
|
1419
|
+
identityClient;
|
|
1407
1420
|
startedAt = Date.now();
|
|
1408
1421
|
/** Apex write targets, keyed by btpUrl. */
|
|
1409
1422
|
apexes = /* @__PURE__ */ new Map();
|
|
1410
1423
|
/** Relay read targets, keyed by relayUrl. */
|
|
1411
1424
|
relays = /* @__PURE__ */ new Map();
|
|
1425
|
+
/**
|
|
1426
|
+
* Async faucet drip jobs, keyed by chain. A drip is launched in the background
|
|
1427
|
+
* (the Mina faucet legitimately takes ~75s — longer than the MCP host's ~60s
|
|
1428
|
+
* tool-call budget) and its terminal state is observed via {@link getFundStatus}
|
|
1429
|
+
* / re-reading balances rather than by blocking the caller.
|
|
1430
|
+
*/
|
|
1431
|
+
fundJobs = /* @__PURE__ */ new Map();
|
|
1412
1432
|
/** Runner-level merged read buffer across all relays (de-duped by event.id). */
|
|
1413
1433
|
merged = [];
|
|
1414
1434
|
mergedSeen = /* @__PURE__ */ new Set();
|
|
@@ -1438,9 +1458,10 @@ var ClientRunner = class {
|
|
|
1438
1458
|
decodeEvent: (raw) => decodeEventFromToon(new TextEncoder().encode(raw))
|
|
1439
1459
|
}));
|
|
1440
1460
|
this.registerRelay(this.defaultRelayUrl);
|
|
1461
|
+
this.identityClient = this.createClient(this.config.toonClientConfig);
|
|
1441
1462
|
const defaultApex = this.makeApex({
|
|
1442
1463
|
btpUrl: this.defaultBtpUrl,
|
|
1443
|
-
client: this.
|
|
1464
|
+
client: this.identityClient,
|
|
1444
1465
|
...this.config.apex ? { negotiation: this.config.apex } : {},
|
|
1445
1466
|
childPeers: this.config.apexChildPeers ?? [],
|
|
1446
1467
|
destination: this.config.destination,
|
|
@@ -1920,7 +1941,7 @@ var ClientRunner = class {
|
|
|
1920
1941
|
* (the typical "fund me before I open a channel" flow). The daemon holds the
|
|
1921
1942
|
* faucet URL + the keys, so the MCP caller never needs either.
|
|
1922
1943
|
*/
|
|
1923
|
-
|
|
1944
|
+
fundWallet(req = {}) {
|
|
1924
1945
|
const faucetUrl = this.config.faucetUrl;
|
|
1925
1946
|
if (!faucetUrl) {
|
|
1926
1947
|
throw new InvalidPayloadError(
|
|
@@ -1937,13 +1958,47 @@ var ClientRunner = class {
|
|
|
1937
1958
|
`no ${chain} address available to fund \u2014 pass an explicit address (this client has no ${chain} key configured).`
|
|
1938
1959
|
);
|
|
1939
1960
|
}
|
|
1940
|
-
const
|
|
1941
|
-
|
|
1942
|
-
|
|
1961
|
+
const existing = this.fundJobs.get(chain);
|
|
1962
|
+
if (existing && existing.status === "pending") {
|
|
1963
|
+
return { ...existing };
|
|
1964
|
+
}
|
|
1965
|
+
const job = {
|
|
1943
1966
|
chain,
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1967
|
+
address,
|
|
1968
|
+
faucetUrl,
|
|
1969
|
+
status: "pending",
|
|
1970
|
+
startedAt: Date.now()
|
|
1971
|
+
};
|
|
1972
|
+
this.fundJobs.set(chain, job);
|
|
1973
|
+
const faucetTimeout = this.config.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
|
|
1974
|
+
void fundWallet(faucetUrl, address, chain, { timeout: faucetTimeout }).then(({ response }) => {
|
|
1975
|
+
job.status = "success";
|
|
1976
|
+
job.response = response;
|
|
1977
|
+
job.finishedAt = Date.now();
|
|
1978
|
+
this.log(`[runner] faucet drip succeeded: ${chain} \u2192 ${address}`);
|
|
1979
|
+
}).catch((err) => {
|
|
1980
|
+
try {
|
|
1981
|
+
const msg = errMsg2(err);
|
|
1982
|
+
const timedOut = /timed out|timeout|aborted/i.test(msg);
|
|
1983
|
+
job.status = timedOut ? "timeout" : "error";
|
|
1984
|
+
job.error = timedOut ? `${msg} \u2014 the on-chain drip may still have settled; re-check balances before re-funding.` : msg;
|
|
1985
|
+
job.finishedAt = Date.now();
|
|
1986
|
+
this.log(
|
|
1987
|
+
`[runner] faucet drip ${timedOut ? "timed out" : "failed"}: ${chain} \u2192 ${address}: ${msg}`
|
|
1988
|
+
);
|
|
1989
|
+
} catch {
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
return { ...job };
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* Snapshots of tracked faucet drip jobs — all of them, or just the one for
|
|
1996
|
+
* `chain`. Lets a caller poll for the terminal state of an async drip without
|
|
1997
|
+
* re-dripping.
|
|
1998
|
+
*/
|
|
1999
|
+
getFundStatus(chain) {
|
|
2000
|
+
const jobs = chain ? this.fundJobs.has(chain) ? [{ ...this.fundJobs.get(chain) }] : [] : [...this.fundJobs.values()].map((j) => ({ ...j }));
|
|
2001
|
+
return { jobs };
|
|
1947
2002
|
}
|
|
1948
2003
|
/** Full registry of relay + apex targets with per-target status. */
|
|
1949
2004
|
getTargets() {
|
|
@@ -2186,14 +2241,39 @@ var ClientRunner = class {
|
|
|
2186
2241
|
}
|
|
2187
2242
|
/**
|
|
2188
2243
|
* On-chain wallet balances. The wallet is identity-level (same keys across
|
|
2189
|
-
* apexes), so
|
|
2190
|
-
*
|
|
2244
|
+
* apexes), so this reads from the daemon's {@link identityClient} — NOT an apex
|
|
2245
|
+
* — and therefore works even with zero apexes / no payment peer configured
|
|
2246
|
+
* (reading your own balance is a pure wallet-keys + chain-RPC operation).
|
|
2247
|
+
* Per-chain reads are best-effort inside the client (a failing chain is simply
|
|
2248
|
+
* omitted).
|
|
2249
|
+
*
|
|
2250
|
+
* Each underlying read hits per-chain RPC providers that can stall
|
|
2251
|
+
* indefinitely on devnet (a provider being `detail: "configured"` in
|
|
2252
|
+
* toon_status means it is WIRED, not that its RPC is live). A stall here used
|
|
2253
|
+
* to block the whole control request until the client aborted, surfacing as a
|
|
2254
|
+
* misleading "relay/apex unreachable" timeout (#199). Bound each attempt well
|
|
2255
|
+
* under the control-plane timeout and retry once so a single transient
|
|
2256
|
+
* provider stall FAST-FAILS with an honest "balances handler / provider
|
|
2257
|
+
* stalled" error instead of hanging.
|
|
2191
2258
|
*/
|
|
2192
2259
|
async getBalances() {
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2260
|
+
let lastErr;
|
|
2261
|
+
for (let attempt = 1; attempt <= BALANCES_READ_ATTEMPTS; attempt++) {
|
|
2262
|
+
try {
|
|
2263
|
+
const balances = await withTimeout(
|
|
2264
|
+
this.identityClient.getBalances(),
|
|
2265
|
+
BALANCES_READ_TIMEOUT_MS,
|
|
2266
|
+
`chain balance read timed out after ${BALANCES_READ_TIMEOUT_MS}ms`
|
|
2267
|
+
);
|
|
2268
|
+
return { balances };
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
lastErr = err;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
throw new BalancesUnavailableError(
|
|
2274
|
+
`the balances control handler's chain RPC/provider read did not return (${BALANCES_READ_ATTEMPTS} attempts, ${BALANCES_READ_TIMEOUT_MS}ms each) \u2014 the on-chain provider stalled, not the relay or apex. Retry shortly.`,
|
|
2275
|
+
lastErr instanceof Error ? lastErr.message : void 0
|
|
2276
|
+
);
|
|
2197
2277
|
}
|
|
2198
2278
|
/**
|
|
2199
2279
|
* Deposit additional collateral into an open channel. Routes to the apex whose
|
|
@@ -2345,12 +2425,37 @@ var InvalidPayloadError = class extends Error {
|
|
|
2345
2425
|
this.name = "InvalidPayloadError";
|
|
2346
2426
|
}
|
|
2347
2427
|
};
|
|
2428
|
+
var BalancesUnavailableError = class extends Error {
|
|
2429
|
+
retryable = true;
|
|
2430
|
+
/** The underlying provider error message, when one was captured. */
|
|
2431
|
+
providerError;
|
|
2432
|
+
constructor(message, providerError) {
|
|
2433
|
+
super(message);
|
|
2434
|
+
this.name = "BalancesUnavailableError";
|
|
2435
|
+
if (providerError !== void 0) this.providerError = providerError;
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2348
2438
|
function nowSeconds() {
|
|
2349
2439
|
return Math.floor(Date.now() / 1e3);
|
|
2350
2440
|
}
|
|
2351
2441
|
function delay2(ms) {
|
|
2352
2442
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2353
2443
|
}
|
|
2444
|
+
function withTimeout(promise, ms, message) {
|
|
2445
|
+
return new Promise((resolve2, reject) => {
|
|
2446
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
2447
|
+
promise.then(
|
|
2448
|
+
(value) => {
|
|
2449
|
+
clearTimeout(timer);
|
|
2450
|
+
resolve2(value);
|
|
2451
|
+
},
|
|
2452
|
+
(err) => {
|
|
2453
|
+
clearTimeout(timer);
|
|
2454
|
+
reject(err);
|
|
2455
|
+
}
|
|
2456
|
+
);
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2354
2459
|
function matchesFilter(event, filter) {
|
|
2355
2460
|
if (filter.ids && !filter.ids.includes(event.id)) return false;
|
|
2356
2461
|
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
@@ -2494,7 +2599,13 @@ function registerRoutes(app, runner) {
|
|
|
2494
2599
|
}
|
|
2495
2600
|
});
|
|
2496
2601
|
app.get("/channels", async () => runner.getChannels());
|
|
2497
|
-
app.get("/balances", async () =>
|
|
2602
|
+
app.get("/balances", async (_req, reply) => {
|
|
2603
|
+
try {
|
|
2604
|
+
return await runner.getBalances();
|
|
2605
|
+
} catch (err) {
|
|
2606
|
+
return mapError(reply, err);
|
|
2607
|
+
}
|
|
2608
|
+
});
|
|
2498
2609
|
app.post("/channels/deposit", async (req, reply) => {
|
|
2499
2610
|
try {
|
|
2500
2611
|
return await runner.depositToChannel(req.body);
|
|
@@ -2547,11 +2658,15 @@ function registerRoutes(app, runner) {
|
|
|
2547
2658
|
);
|
|
2548
2659
|
app.post("/fund-wallet", async (req, reply) => {
|
|
2549
2660
|
try {
|
|
2550
|
-
return
|
|
2661
|
+
return runner.fundWallet(req.body ?? {});
|
|
2551
2662
|
} catch (err) {
|
|
2552
2663
|
return mapError(reply, err);
|
|
2553
2664
|
}
|
|
2554
2665
|
});
|
|
2666
|
+
app.get(
|
|
2667
|
+
"/fund-wallet/status",
|
|
2668
|
+
async (req) => runner.getFundStatus(req.query?.chain)
|
|
2669
|
+
);
|
|
2555
2670
|
app.get("/targets", async () => runner.getTargets());
|
|
2556
2671
|
app.post("/relays", async (req, reply) => {
|
|
2557
2672
|
const url = req.body?.relayUrl;
|
|
@@ -2627,6 +2742,12 @@ function mapError(reply, err) {
|
|
|
2627
2742
|
if (err instanceof PublishRejectedError) {
|
|
2628
2743
|
return sendError(reply, 502, "rejected", { detail: err.message });
|
|
2629
2744
|
}
|
|
2745
|
+
if (err instanceof BalancesUnavailableError) {
|
|
2746
|
+
return sendError(reply, 504, "balances_unavailable", {
|
|
2747
|
+
detail: err.message,
|
|
2748
|
+
retryable: true
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2630
2751
|
if (err instanceof TargetError) {
|
|
2631
2752
|
const status = /no such/i.test(err.message) ? 404 : 400;
|
|
2632
2753
|
return sendError(reply, status, "invalid_target", { detail: err.message });
|
|
@@ -2662,4 +2783,4 @@ export {
|
|
|
2662
2783
|
PublishRejectedError,
|
|
2663
2784
|
registerRoutes
|
|
2664
2785
|
};
|
|
2665
|
-
//# sourceMappingURL=chunk-
|
|
2786
|
+
//# sourceMappingURL=chunk-KVK6OZVD.js.map
|