@toon-protocol/client-mcp 0.10.8 → 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.
@@ -15,7 +15,7 @@ import {
15
15
  isEventExpired,
16
16
  parseIlpPeerInfo,
17
17
  readConfigFile
18
- } from "./chunk-3ZBC2HUB.js";
18
+ } from "./chunk-CMGJ3NFT.js";
19
19
  import {
20
20
  __require
21
21
  } from "./chunk-F22GNSF6.js";
@@ -36,7 +36,7 @@ var CONFIG_HELP = {
36
36
  proxyUrl: "Connector-proxy base URL (deployed devnet/testnet edge). When set, paid writes route through `POST /ilp` and no BTP socket is needed. Set `destination` to the apex ILP address (e.g. g.proxy for devnet).",
37
37
  faucetUrl: "Devnet faucet base URL (e.g. https://faucet.devnet.toonprotocol.dev) used to drip test funds before publishing.",
38
38
  btpUrl: "BTP WebSocket URL of the apex/connector for paid writes. Optional when `proxyUrl` is set.",
39
- relayUrl: "Town relay WebSocket URL for FREE reads. Default ws://localhost:7100.",
39
+ relayUrl: "Relay WebSocket URL for FREE reads. Default ws://localhost:7100.",
40
40
  destination: "Default ILP publish destination (apex address). Default g.proxy.",
41
41
  keystorePath: "Auto-generated encrypted identity. Do not hand-edit; back up your seed phrase."
42
42
  };
@@ -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.createClient(this.config.toonClientConfig),
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
- async fundWallet(req = {}) {
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 { response } = await fundWallet(
1941
- faucetUrl,
1942
- address,
1961
+ const existing = this.fundJobs.get(chain);
1962
+ if (existing && existing.status === "pending") {
1963
+ return { ...existing };
1964
+ }
1965
+ const job = {
1943
1966
  chain,
1944
- this.config.faucetTimeoutMs !== void 0 ? { timeout: this.config.faucetTimeoutMs } : {}
1945
- );
1946
- return { chain, address, faucetUrl, response };
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 read from the first available apex's client; per-chain reads are
2190
- * best-effort inside the client (a failing chain is simply omitted).
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
- const apex = this.apexes.values().next().value;
2194
- if (!apex) return { balances: [] };
2195
- const balances = await apex.client.getBalances();
2196
- return { balances };
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 () => runner.getBalances());
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 await runner.fundWallet(req.body ?? {});
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-ADBNZA5O.js.map
2786
+ //# sourceMappingURL=chunk-KVK6OZVD.js.map