@toon-protocol/client-mcp 0.10.9 → 0.12.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-UHITXU5V.js";
18
+ } from "./chunk-W6T6ZK7U.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.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,
@@ -1757,9 +1778,17 @@ var ClientRunner = class {
1757
1778
  const cm = apex.client.channelManager;
1758
1779
  if (saved && cm && typeof cm.trackChannel === "function") {
1759
1780
  cm.trackChannel(saved.channelId, saved.context);
1760
- this.log(
1761
- `[runner] resumed apex channel ${saved.channelId} (tracked, no re-deposit)`
1762
- );
1781
+ if (saved.context.chainType === "evm") {
1782
+ await apex.client.rehydrateChannelDeposit?.(saved.channelId, {
1783
+ chain: `evm:${saved.context.chainId}`,
1784
+ tokenNetworkAddress: saved.context.tokenNetworkAddress
1785
+ }).catch(
1786
+ (err) => this.log(
1787
+ `[runner] deposit re-hydrate for ${saved.channelId} failed: ${errMsg2(err)}`
1788
+ )
1789
+ );
1790
+ }
1791
+ this.log(`[runner] resumed apex channel ${saved.channelId} (deposit re-read)`);
1763
1792
  return saved.channelId;
1764
1793
  }
1765
1794
  if (opts.resumeOnly) return void 0;
@@ -1920,7 +1949,7 @@ var ClientRunner = class {
1920
1949
  * (the typical "fund me before I open a channel" flow). The daemon holds the
1921
1950
  * faucet URL + the keys, so the MCP caller never needs either.
1922
1951
  */
1923
- async fundWallet(req = {}) {
1952
+ fundWallet(req = {}) {
1924
1953
  const faucetUrl = this.config.faucetUrl;
1925
1954
  if (!faucetUrl) {
1926
1955
  throw new InvalidPayloadError(
@@ -1937,13 +1966,47 @@ var ClientRunner = class {
1937
1966
  `no ${chain} address available to fund \u2014 pass an explicit address (this client has no ${chain} key configured).`
1938
1967
  );
1939
1968
  }
1940
- const { response } = await fundWallet(
1941
- faucetUrl,
1942
- address,
1969
+ const existing = this.fundJobs.get(chain);
1970
+ if (existing && existing.status === "pending") {
1971
+ return { ...existing };
1972
+ }
1973
+ const job = {
1943
1974
  chain,
1944
- this.config.faucetTimeoutMs !== void 0 ? { timeout: this.config.faucetTimeoutMs } : {}
1945
- );
1946
- return { chain, address, faucetUrl, response };
1975
+ address,
1976
+ faucetUrl,
1977
+ status: "pending",
1978
+ startedAt: Date.now()
1979
+ };
1980
+ this.fundJobs.set(chain, job);
1981
+ const faucetTimeout = this.config.faucetTimeoutMs ?? (chain === "mina" ? 13e4 : 9e4);
1982
+ void fundWallet(faucetUrl, address, chain, { timeout: faucetTimeout }).then(({ response }) => {
1983
+ job.status = "success";
1984
+ job.response = response;
1985
+ job.finishedAt = Date.now();
1986
+ this.log(`[runner] faucet drip succeeded: ${chain} \u2192 ${address}`);
1987
+ }).catch((err) => {
1988
+ try {
1989
+ const msg = errMsg2(err);
1990
+ const timedOut = /timed out|timeout|aborted/i.test(msg);
1991
+ job.status = timedOut ? "timeout" : "error";
1992
+ job.error = timedOut ? `${msg} \u2014 the on-chain drip may still have settled; re-check balances before re-funding.` : msg;
1993
+ job.finishedAt = Date.now();
1994
+ this.log(
1995
+ `[runner] faucet drip ${timedOut ? "timed out" : "failed"}: ${chain} \u2192 ${address}: ${msg}`
1996
+ );
1997
+ } catch {
1998
+ }
1999
+ });
2000
+ return { ...job };
2001
+ }
2002
+ /**
2003
+ * Snapshots of tracked faucet drip jobs — all of them, or just the one for
2004
+ * `chain`. Lets a caller poll for the terminal state of an async drip without
2005
+ * re-dripping.
2006
+ */
2007
+ getFundStatus(chain) {
2008
+ const jobs = chain ? this.fundJobs.has(chain) ? [{ ...this.fundJobs.get(chain) }] : [] : [...this.fundJobs.values()].map((j) => ({ ...j }));
2009
+ return { jobs };
1947
2010
  }
1948
2011
  /** Full registry of relay + apex targets with per-target status. */
1949
2012
  getTargets() {
@@ -2186,14 +2249,39 @@ var ClientRunner = class {
2186
2249
  }
2187
2250
  /**
2188
2251
  * 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).
2252
+ * apexes), so this reads from the daemon's {@link identityClient} NOT an apex
2253
+ * and therefore works even with zero apexes / no payment peer configured
2254
+ * (reading your own balance is a pure wallet-keys + chain-RPC operation).
2255
+ * Per-chain reads are best-effort inside the client (a failing chain is simply
2256
+ * omitted).
2257
+ *
2258
+ * Each underlying read hits per-chain RPC providers that can stall
2259
+ * indefinitely on devnet (a provider being `detail: "configured"` in
2260
+ * toon_status means it is WIRED, not that its RPC is live). A stall here used
2261
+ * to block the whole control request until the client aborted, surfacing as a
2262
+ * misleading "relay/apex unreachable" timeout (#199). Bound each attempt well
2263
+ * under the control-plane timeout and retry once so a single transient
2264
+ * provider stall FAST-FAILS with an honest "balances handler / provider
2265
+ * stalled" error instead of hanging.
2191
2266
  */
2192
2267
  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 };
2268
+ let lastErr;
2269
+ for (let attempt = 1; attempt <= BALANCES_READ_ATTEMPTS; attempt++) {
2270
+ try {
2271
+ const balances = await withTimeout(
2272
+ this.identityClient.getBalances(),
2273
+ BALANCES_READ_TIMEOUT_MS,
2274
+ `chain balance read timed out after ${BALANCES_READ_TIMEOUT_MS}ms`
2275
+ );
2276
+ return { balances };
2277
+ } catch (err) {
2278
+ lastErr = err;
2279
+ }
2280
+ }
2281
+ throw new BalancesUnavailableError(
2282
+ `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.`,
2283
+ lastErr instanceof Error ? lastErr.message : void 0
2284
+ );
2197
2285
  }
2198
2286
  /**
2199
2287
  * Deposit additional collateral into an open channel. Routes to the apex whose
@@ -2345,12 +2433,37 @@ var InvalidPayloadError = class extends Error {
2345
2433
  this.name = "InvalidPayloadError";
2346
2434
  }
2347
2435
  };
2436
+ var BalancesUnavailableError = class extends Error {
2437
+ retryable = true;
2438
+ /** The underlying provider error message, when one was captured. */
2439
+ providerError;
2440
+ constructor(message, providerError) {
2441
+ super(message);
2442
+ this.name = "BalancesUnavailableError";
2443
+ if (providerError !== void 0) this.providerError = providerError;
2444
+ }
2445
+ };
2348
2446
  function nowSeconds() {
2349
2447
  return Math.floor(Date.now() / 1e3);
2350
2448
  }
2351
2449
  function delay2(ms) {
2352
2450
  return new Promise((resolve2) => setTimeout(resolve2, ms));
2353
2451
  }
2452
+ function withTimeout(promise, ms, message) {
2453
+ return new Promise((resolve2, reject) => {
2454
+ const timer = setTimeout(() => reject(new Error(message)), ms);
2455
+ promise.then(
2456
+ (value) => {
2457
+ clearTimeout(timer);
2458
+ resolve2(value);
2459
+ },
2460
+ (err) => {
2461
+ clearTimeout(timer);
2462
+ reject(err);
2463
+ }
2464
+ );
2465
+ });
2466
+ }
2354
2467
  function matchesFilter(event, filter) {
2355
2468
  if (filter.ids && !filter.ids.includes(event.id)) return false;
2356
2469
  if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
@@ -2494,7 +2607,13 @@ function registerRoutes(app, runner) {
2494
2607
  }
2495
2608
  });
2496
2609
  app.get("/channels", async () => runner.getChannels());
2497
- app.get("/balances", async () => runner.getBalances());
2610
+ app.get("/balances", async (_req, reply) => {
2611
+ try {
2612
+ return await runner.getBalances();
2613
+ } catch (err) {
2614
+ return mapError(reply, err);
2615
+ }
2616
+ });
2498
2617
  app.post("/channels/deposit", async (req, reply) => {
2499
2618
  try {
2500
2619
  return await runner.depositToChannel(req.body);
@@ -2547,11 +2666,15 @@ function registerRoutes(app, runner) {
2547
2666
  );
2548
2667
  app.post("/fund-wallet", async (req, reply) => {
2549
2668
  try {
2550
- return await runner.fundWallet(req.body ?? {});
2669
+ return runner.fundWallet(req.body ?? {});
2551
2670
  } catch (err) {
2552
2671
  return mapError(reply, err);
2553
2672
  }
2554
2673
  });
2674
+ app.get(
2675
+ "/fund-wallet/status",
2676
+ async (req) => runner.getFundStatus(req.query?.chain)
2677
+ );
2555
2678
  app.get("/targets", async () => runner.getTargets());
2556
2679
  app.post("/relays", async (req, reply) => {
2557
2680
  const url = req.body?.relayUrl;
@@ -2627,6 +2750,12 @@ function mapError(reply, err) {
2627
2750
  if (err instanceof PublishRejectedError) {
2628
2751
  return sendError(reply, 502, "rejected", { detail: err.message });
2629
2752
  }
2753
+ if (err instanceof BalancesUnavailableError) {
2754
+ return sendError(reply, 504, "balances_unavailable", {
2755
+ detail: err.message,
2756
+ retryable: true
2757
+ });
2758
+ }
2630
2759
  if (err instanceof TargetError) {
2631
2760
  const status = /no such/i.test(err.message) ? 404 : 400;
2632
2761
  return sendError(reply, status, "invalid_target", { detail: err.message });
@@ -2662,4 +2791,4 @@ export {
2662
2791
  PublishRejectedError,
2663
2792
  registerRoutes
2664
2793
  };
2665
- //# sourceMappingURL=chunk-CS4B3GET.js.map
2794
+ //# sourceMappingURL=chunk-74KX5LXI.js.map