@toon-protocol/client-mcp 0.2.0 → 0.3.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.
@@ -13,10 +13,7 @@ import {
13
13
  isEventExpired,
14
14
  parseIlpPeerInfo,
15
15
  readConfigFile
16
- } from "./chunk-NP35YO5O.js";
17
- import {
18
- startManagedAnonProxy
19
- } from "./chunk-SKQTKZIH.js";
16
+ } from "./chunk-CQ2QIZ6Z.js";
20
17
  import {
21
18
  __require
22
19
  } from "./chunk-F22GNSF6.js";
@@ -33,10 +30,12 @@ function hasConfiguredIdentity(file) {
33
30
  );
34
31
  }
35
32
  var CONFIG_HELP = {
36
- transport: "Point btpUrl/relayUrl at your apex. For an .anyone hidden service use ws://<host>.anyone:3000/btp + ws://<host>.anyone:7100 \u2014 the daemon auto-routes through a managed anon proxy. For a direct apex use ws://<host>:3000/btp + ws://<host>:7100.",
37
- btpUrl: "REQUIRED. BTP WebSocket URL of the apex/connector for paid writes.",
33
+ transport: "Configure ONE uplink for paid writes: either `proxyUrl` (connector payment-proxy over ILP-over-HTTP, e.g. https://proxy.devnet.toonprotocol.dev) OR `btpUrl` (BTP WebSocket, e.g. ws://<host>:3000/btp). Reads are always free over relayUrl.",
34
+ 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).",
35
+ faucetUrl: "Devnet faucet base URL (e.g. https://faucet.devnet.toonprotocol.dev) used to drip test funds before publishing.",
36
+ btpUrl: "BTP WebSocket URL of the apex/connector for paid writes. Optional when `proxyUrl` is set.",
38
37
  relayUrl: "Town relay WebSocket URL for FREE reads. Default ws://localhost:7100.",
39
- managedAnonProxy: "Optional override \u2014 leave unset to auto-infer from btpUrl (.anyone host \u2192 managed anon proxy, anything else \u2192 direct).",
38
+ destination: "Default ILP publish destination (apex address). Default g.townhouse.town; set to g.proxy for the deployed devnet proxy.",
40
39
  keystorePath: "Auto-generated encrypted identity. Do not hand-edit; back up your seed phrase."
41
40
  };
42
41
  async function scaffoldFirstRun(opts = {}) {
@@ -75,13 +74,14 @@ async function scaffoldFirstRun(opts = {}) {
75
74
  if (!existsSync(configPath)) {
76
75
  updated = {
77
76
  _help: CONFIG_HELP,
77
+ proxyUrl: "",
78
78
  btpUrl: "",
79
79
  relayUrl: "ws://localhost:7100",
80
80
  ...updated
81
81
  };
82
82
  changed = true;
83
83
  log(
84
- `[toon-clientd] wrote starter config at ${configPath} \u2014 set "btpUrl" to your apex before publishing.`
84
+ `[toon-clientd] wrote starter config at ${configPath} \u2014 set "proxyUrl" (connector proxy, ILP-over-HTTP) or "btpUrl" (BTP) to your apex before publishing.`
85
85
  );
86
86
  }
87
87
  if (changed) writeConfigFile(configPath, updated);
@@ -124,7 +124,6 @@ var DEFAULT_MAX_MS = 3e4;
124
124
  var noop = () => void 0;
125
125
  var RelaySubscription = class {
126
126
  relayUrl;
127
- socksProxy;
128
127
  bufferSize;
129
128
  reconnectBaseMs;
130
129
  reconnectMaxMs;
@@ -147,12 +146,11 @@ var RelaySubscription = class {
147
146
  reconnectTimer = null;
148
147
  constructor(opts) {
149
148
  this.relayUrl = opts.relayUrl;
150
- this.socksProxy = opts.socksProxy;
151
149
  this.bufferSize = opts.bufferSize ?? DEFAULT_BUFFER;
152
150
  this.reconnectBaseMs = opts.reconnectBaseMs ?? DEFAULT_BASE_MS;
153
151
  this.reconnectMaxMs = opts.reconnectMaxMs ?? DEFAULT_MAX_MS;
154
152
  this.log = opts.logger ?? noop;
155
- this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory(this.socksProxy);
153
+ this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory();
156
154
  this.decodeEvent = opts.decodeEvent;
157
155
  this.onEvent = opts.onEvent;
158
156
  }
@@ -347,15 +345,10 @@ function toText(data) {
347
345
  function errMsg(err) {
348
346
  return err instanceof Error ? err.message : String(err);
349
347
  }
350
- function defaultWebSocketFactory(socksProxy) {
348
+ function defaultWebSocketFactory() {
351
349
  return (url) => {
352
350
  const WebSocketImpl = nodeRequire("ws");
353
- let agent;
354
- if (socksProxy) {
355
- const { SocksProxyAgent } = nodeRequire("socks-proxy-agent");
356
- agent = new SocksProxyAgent(socksProxy);
357
- }
358
- return new WebSocketImpl(url, agent ? { agent } : void 0);
351
+ return new WebSocketImpl(url);
359
352
  };
360
353
  }
361
354
 
@@ -1404,7 +1397,6 @@ var ClientRunner = class {
1404
1397
  config;
1405
1398
  createClient;
1406
1399
  createRelay;
1407
- startReadProxy;
1408
1400
  log;
1409
1401
  targetsPath;
1410
1402
  startedAt = Date.now();
@@ -1424,9 +1416,6 @@ var ClientRunner = class {
1424
1416
  subIdCounter = 0;
1425
1417
  defaultBtpUrl;
1426
1418
  defaultRelayUrl;
1427
- /** Teardown for the daemon-managed read proxy (btp-direct + relay-`.anyone`). */
1428
- stopReadProxy;
1429
- readProxyError;
1430
1419
  stopped = false;
1431
1420
  started = false;
1432
1421
  constructor(deps) {
@@ -1438,17 +1427,12 @@ var ClientRunner = class {
1438
1427
  this.defaultRelayUrl = deps.config.relayUrl;
1439
1428
  this.createRelay = deps.createRelay ?? ((opts) => new RelaySubscription({
1440
1429
  relayUrl: opts.relayUrl,
1441
- ...opts.socksProxy ? { socksProxy: opts.socksProxy } : {},
1442
1430
  ...opts.logger ? { logger: opts.logger } : {},
1443
1431
  onEvent: opts.onEvent,
1444
1432
  // The TOON relay sends events TOON-encoded (text) on reads, not JSON.
1445
1433
  decodeEvent: (raw) => decodeEventFromToon(new TextEncoder().encode(raw))
1446
1434
  }));
1447
- this.startReadProxy = deps.startReadProxy ?? ((opts) => startManagedAnonProxy({
1448
- socksPort: opts.socksPort,
1449
- ...opts.log ? { log: opts.log } : {}
1450
- }));
1451
- this.registerRelay(this.defaultRelayUrl, this.config.socksProxy);
1435
+ this.registerRelay(this.defaultRelayUrl);
1452
1436
  const defaultApex = this.makeApex({
1453
1437
  btpUrl: this.defaultBtpUrl,
1454
1438
  client: this.createClient(this.config.toonClientConfig),
@@ -1470,13 +1454,13 @@ var ClientRunner = class {
1470
1454
  start() {
1471
1455
  if (this.started) return;
1472
1456
  this.started = true;
1473
- if (this.config.manageReadProxy) void this.bringUpReadProxy();
1474
1457
  for (const relay of this.relays.values()) relay.start();
1475
1458
  void this.bootstrap();
1476
1459
  this.replayPersistedTargets();
1477
1460
  }
1478
1461
  /** Await the default apex's bootstrap (kicking it off if not already running). */
1479
1462
  bootstrap() {
1463
+ if (!this.config.hasUplink) return Promise.resolve();
1480
1464
  const apex = this.apexes.get(this.defaultBtpUrl);
1481
1465
  if (!apex) return Promise.resolve();
1482
1466
  return this.bootstrapApex(apex);
@@ -1487,12 +1471,11 @@ var ClientRunner = class {
1487
1471
  * merged buffer and replaying active fan-out subscriptions. Does NOT start the
1488
1472
  * socket — callers start it (so construction stays side-effect-free for tests).
1489
1473
  */
1490
- registerRelay(relayUrl, socksProxy) {
1474
+ registerRelay(relayUrl) {
1491
1475
  const existing = this.relays.get(relayUrl);
1492
1476
  if (existing) return existing;
1493
1477
  const relay = this.createRelay({
1494
1478
  relayUrl,
1495
- ...socksProxy ? { socksProxy } : {},
1496
1479
  logger: this.log,
1497
1480
  onEvent: (subId, event) => this.pushMerged(relayUrl, subId, event)
1498
1481
  });
@@ -1502,17 +1485,11 @@ var ClientRunner = class {
1502
1485
  return relay;
1503
1486
  }
1504
1487
  /**
1505
- * Add a relay read target at runtime. `.anyone` relays reuse the managed read
1506
- * proxy (started here if needed). Persisted unless `persist` is false.
1488
+ * Add a relay read target at runtime. Persisted unless `persist` is false.
1507
1489
  */
1508
1490
  async addRelay(relayUrl, persist = true) {
1509
1491
  if (this.relays.has(relayUrl)) return;
1510
- let socksProxy = this.config.socksProxy;
1511
- if (isAnyoneHost(relayUrl) && !socksProxy) {
1512
- await this.ensureReadProxy();
1513
- socksProxy = `socks5h://127.0.0.1:${this.config.readProxySocksPort ?? 9050}`;
1514
- }
1515
- const relay = this.registerRelay(relayUrl, socksProxy);
1492
+ const relay = this.registerRelay(relayUrl);
1516
1493
  relay.start();
1517
1494
  if (persist) saveRelayTarget(relayUrl, this.targetsPath);
1518
1495
  }
@@ -1616,14 +1593,20 @@ var ClientRunner = class {
1616
1593
  async doBootstrapApex(apex) {
1617
1594
  apex.bootstrapping = true;
1618
1595
  try {
1596
+ if (!apex.negotiation && this.config.proxyUrl) {
1597
+ await this.discoverApexNegotiation(apex);
1598
+ }
1619
1599
  await apex.client.start();
1620
1600
  this.injectApexNegotiation(apex);
1621
- apex.apexChannelId = await this.openOrResumeApexChannel(apex);
1601
+ const deferOpen = Boolean(this.config.proxyUrl);
1602
+ apex.apexChannelId = await this.openOrResumeApexChannel(apex, {
1603
+ resumeOnly: deferOpen
1604
+ });
1622
1605
  this.routeChildPeersThroughApexChannel(apex);
1623
1606
  apex.ready = true;
1624
1607
  apex.lastError = void 0;
1625
1608
  this.log(
1626
- `[runner] apex ${apex.btpUrl} ready; channel ${apex.apexChannelId}`
1609
+ `[runner] apex ${apex.btpUrl || apex.destination} ready; channel ${apex.apexChannelId ?? "(deferred \u2014 open on first write)"}`
1627
1610
  );
1628
1611
  } catch (err) {
1629
1612
  apex.lastError = err instanceof Error ? err.message : String(err);
@@ -1752,37 +1735,15 @@ var ClientRunner = class {
1752
1735
  );
1753
1736
  }
1754
1737
  }
1755
- // ── Shared read proxy ──────────────────────────────────────────────────────
1756
- async ensureReadProxy() {
1757
- if (this.stopReadProxy) return;
1758
- await this.bringUpReadProxy();
1759
- }
1760
- async bringUpReadProxy() {
1761
- if (this.stopReadProxy) return;
1762
- const socksPort = this.config.readProxySocksPort ?? 9050;
1763
- try {
1764
- this.log(
1765
- `[runner] starting managed read proxy on 127.0.0.1:${socksPort}`
1766
- );
1767
- const proxy = await this.startReadProxy({
1768
- socksPort,
1769
- log: (m) => this.log(`[anon] ${m}`)
1770
- });
1771
- if (this.stopped) {
1772
- await proxy.stop();
1773
- return;
1774
- }
1775
- this.stopReadProxy = () => proxy.stop();
1776
- this.readProxyError = void 0;
1777
- this.log("[runner] managed read proxy ready");
1778
- } catch (err) {
1779
- this.readProxyError = err instanceof Error ? err.message : String(err);
1780
- this.log(`[runner] managed read proxy failed: ${this.readProxyError}`);
1781
- }
1782
- }
1783
1738
  // ── Channel / negotiation helpers (per-apex) ───────────────────────────────
1784
- /** Open the apex channel — or, on a restart, RESUME the existing one. */
1785
- async openOrResumeApexChannel(apex) {
1739
+ /**
1740
+ * Open the apex channel — or, on a restart, RESUME the existing one.
1741
+ *
1742
+ * With `resumeOnly`, only a persisted channel is resumed (no on-chain open);
1743
+ * returns undefined when none exists so the caller can defer the open to the
1744
+ * first write (funded-after-start demo flow, #69).
1745
+ */
1746
+ async openOrResumeApexChannel(apex, opts = {}) {
1786
1747
  const { destination, chain } = apex;
1787
1748
  const { apexChannelStorePath } = this.config;
1788
1749
  const saved = loadApexChannel(apexChannelStorePath, destination, chain);
@@ -1794,10 +1755,24 @@ var ClientRunner = class {
1794
1755
  );
1795
1756
  return saved.channelId;
1796
1757
  }
1758
+ if (opts.resumeOnly) return void 0;
1797
1759
  const channelId = await apex.client.openChannel(destination);
1798
- if (apex.negotiation) {
1799
- const a = apex.negotiation;
1800
- saveApexChannel(apexChannelStorePath, destination, chain, {
1760
+ this.persistApexChannel(apex, channelId);
1761
+ return channelId;
1762
+ }
1763
+ /**
1764
+ * Persist a (lazily- or eagerly-) opened apex channel so a restart RESUMES it
1765
+ * (tracked, no re-deposit) rather than opening a second on-chain channel.
1766
+ * No-op when the apex carries no negotiation (nothing to key the store on).
1767
+ */
1768
+ persistApexChannel(apex, channelId) {
1769
+ const a = apex.negotiation;
1770
+ if (!a) return;
1771
+ saveApexChannel(
1772
+ this.config.apexChannelStorePath,
1773
+ apex.destination,
1774
+ apex.chain,
1775
+ {
1801
1776
  channelId,
1802
1777
  context: {
1803
1778
  chainType: a.chain,
@@ -1806,9 +1781,34 @@ var ClientRunner = class {
1806
1781
  ...a.tokenAddress ? { tokenAddress: a.tokenAddress } : {},
1807
1782
  recipient: a.settlementAddress
1808
1783
  }
1809
- });
1784
+ }
1785
+ );
1786
+ }
1787
+ /**
1788
+ * Discover the apex's settlement negotiation from its kind:10032 on the
1789
+ * default relay and attach it to the apex (proxy-mode fallback when no config
1790
+ * negotiation was supplied, #69). Throws ApexDiscoveryError on timeout/missing
1791
+ * settlement params so the apex's `lastError` reports exactly what is missing.
1792
+ */
1793
+ async discoverApexNegotiation(apex) {
1794
+ const relay = this.relays.get(this.defaultRelayUrl);
1795
+ if (!relay) {
1796
+ throw new TargetError(
1797
+ `Cannot discover apex "${apex.destination}": default relay ${this.defaultRelayUrl} is not registered.`
1798
+ );
1810
1799
  }
1811
- return channelId;
1800
+ relay.start();
1801
+ const discovered = await discoverApex({
1802
+ relay,
1803
+ ilpAddress: apex.destination,
1804
+ chain: apex.chain,
1805
+ ...apex.childPeers.length > 0 ? { childPeers: apex.childPeers } : {}
1806
+ });
1807
+ apex.negotiation = discovered.negotiation;
1808
+ if (discovered.apexChildPeers) apex.childPeers = discovered.apexChildPeers;
1809
+ this.log(
1810
+ `[runner] discovered apex negotiation for "${apex.destination}" (chain ${discovered.negotiation.chainKey}, settle ${discovered.negotiation.settlementAddress})`
1811
+ );
1812
1812
  }
1813
1813
  /** Inject the apex settlement negotiation directly into its ToonClient. */
1814
1814
  injectApexNegotiation(apex) {
@@ -1892,16 +1892,14 @@ var ClientRunner = class {
1892
1892
  minaAddress: safe(() => client?.getMinaAddress())
1893
1893
  },
1894
1894
  transport: {
1895
- type: this.config.socksProxy ? "socks5" : "direct",
1896
- ...this.config.socksProxy ? { socksProxy: this.config.socksProxy } : {},
1895
+ type: "direct",
1897
1896
  ...apex ? { btpUrl: apex.btpUrl } : {}
1898
1897
  },
1899
1898
  relay: {
1900
1899
  url: this.defaultRelayUrl,
1901
1900
  connected: relay?.isConnected() ?? false,
1902
1901
  buffered: relay?.bufferedCount() ?? 0,
1903
- subscriptions: relay?.activeSubscriptions() ?? [],
1904
- ...this.readProxyError ? { proxyError: this.readProxyError } : {}
1902
+ subscriptions: relay?.activeSubscriptions() ?? []
1905
1903
  },
1906
1904
  ...network ? { network } : {},
1907
1905
  ...apex?.lastError ? { lastError: apex.lastError } : {}
@@ -1935,7 +1933,14 @@ var ClientRunner = class {
1935
1933
  async publish(req) {
1936
1934
  const apex = this.selectApex(req.btpUrl);
1937
1935
  this.assertApexReady(apex);
1938
- const channelId = apex.apexChannelId ?? await apex.client.openChannel(req.destination);
1936
+ let channelId = apex.apexChannelId;
1937
+ if (!channelId) {
1938
+ channelId = await apex.client.openChannel(req.destination);
1939
+ if (!req.destination || req.destination === apex.destination) {
1940
+ apex.apexChannelId = channelId;
1941
+ this.persistApexChannel(apex, channelId);
1942
+ }
1943
+ }
1939
1944
  const fee = req.fee !== void 0 ? BigInt(req.fee) : apex.feePerEvent;
1940
1945
  const claim = await apex.client.signBalanceProof(channelId, fee);
1941
1946
  const result = await apex.client.publishEvent(req.event, {
@@ -2059,7 +2064,9 @@ var ClientRunner = class {
2059
2064
  destination ?? apex.destination
2060
2065
  );
2061
2066
  if (!destination || destination === apex.destination) {
2067
+ const firstOpen = apex.apexChannelId !== channelId;
2062
2068
  apex.apexChannelId = channelId;
2069
+ if (firstOpen) this.persistApexChannel(apex, channelId);
2063
2070
  }
2064
2071
  return { channelId };
2065
2072
  }
@@ -2136,19 +2143,11 @@ var ClientRunner = class {
2136
2143
  body: await res.text()
2137
2144
  };
2138
2145
  }
2139
- /** Graceful teardown: close every relay + stop every apex client + read proxy. */
2146
+ /** Graceful teardown: close every relay + stop every apex client. */
2140
2147
  async stop() {
2141
2148
  if (this.stopped) return;
2142
2149
  this.stopped = true;
2143
2150
  for (const relay of this.relays.values()) relay.close();
2144
- if (this.stopReadProxy) {
2145
- try {
2146
- await this.stopReadProxy();
2147
- } catch (err) {
2148
- this.log(`[runner] read proxy stop error: ${errMsg2(err)}`);
2149
- }
2150
- this.stopReadProxy = void 0;
2151
- }
2152
2151
  for (const apex of this.apexes.values()) {
2153
2152
  try {
2154
2153
  await apex.client.stop();
@@ -2169,9 +2168,14 @@ var ClientRunner = class {
2169
2168
  return def;
2170
2169
  }
2171
2170
  assertApexReady(apex) {
2171
+ if (!this.config.hasUplink) {
2172
+ throw new TargetError(
2173
+ "No write uplink configured \u2014 this daemon is read-only. Set TOON_CLIENT_PROXY_URL (connector proxy) or TOON_CLIENT_BTP_URL to enable paid writes."
2174
+ );
2175
+ }
2172
2176
  if (!apex.ready) {
2173
2177
  throw new NotReadyError(
2174
- apex.bootstrapping ? "Apex is still bootstrapping (BTP/anon coming up) \u2014 retry shortly." : apex.lastError ?? "Apex is not ready."
2178
+ apex.bootstrapping ? "Apex is still bootstrapping (transport/channel coming up) \u2014 retry shortly." : apex.lastError ?? "Apex is not ready."
2175
2179
  );
2176
2180
  }
2177
2181
  }
@@ -2259,13 +2263,6 @@ function errMsg2(err) {
2259
2263
  function sanitize(s) {
2260
2264
  return s.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
2261
2265
  }
2262
- function isAnyoneHost(url) {
2263
- try {
2264
- return new URL(url).hostname.endsWith(".anyone");
2265
- } catch {
2266
- return false;
2267
- }
2268
- }
2269
2266
 
2270
2267
  // src/daemon/routes.ts
2271
2268
  function registerRoutes(app, runner) {
@@ -2492,4 +2489,4 @@ export {
2492
2489
  PublishRejectedError,
2493
2490
  registerRoutes
2494
2491
  };
2495
- //# sourceMappingURL=chunk-L6F2B3GX.js.map
2492
+ //# sourceMappingURL=chunk-QFHCXJ2V.js.map