@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.
- package/dist/app/index.html +86 -81
- package/dist/{chunk-CS4B3GET.js → chunk-74KX5LXI.js} +150 -21
- package/dist/chunk-74KX5LXI.js.map +1 -0
- package/dist/{chunk-X4GKWZQP.js → chunk-LN64MLWP.js} +66 -16
- package/dist/chunk-LN64MLWP.js.map +1 -0
- package/dist/{chunk-UHITXU5V.js → chunk-W6T6ZK7U.js} +66 -3
- package/dist/chunk-W6T6ZK7U.js.map +1 -0
- package/dist/daemon.js +2 -2
- package/dist/index.d.ts +82 -10
- package/dist/index.js +3 -3
- package/dist/mcp.js +3 -3
- package/dist/mcp.js.map +1 -1
- 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-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.
|
|
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
|
-
|
|
1761
|
-
|
|
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
|
-
|
|
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
|
|
1941
|
-
|
|
1942
|
-
|
|
1969
|
+
const existing = this.fundJobs.get(chain);
|
|
1970
|
+
if (existing && existing.status === "pending") {
|
|
1971
|
+
return { ...existing };
|
|
1972
|
+
}
|
|
1973
|
+
const job = {
|
|
1943
1974
|
chain,
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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
|
|
2190
|
-
*
|
|
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
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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 () =>
|
|
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
|
|
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-
|
|
2794
|
+
//# sourceMappingURL=chunk-74KX5LXI.js.map
|