@toon-protocol/rig 3.5.2 → 3.6.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.
@@ -7,11 +7,11 @@ import {
7
7
  resolveIdentity,
8
8
  topologyCacheKey,
9
9
  topologyCacheTtlMs
10
- } from "./chunk-FFQXFV4W.js";
10
+ } from "./chunk-PWNZWQOH.js";
11
11
  import {
12
12
  StandalonePublisher,
13
13
  deriveRouteDestinations
14
- } from "./chunk-4CAOEXGQ.js";
14
+ } from "./chunk-IZ6KLYQ6.js";
15
15
  import {
16
16
  ChannelMapStore,
17
17
  RIG_CHANNEL_MAP_FILENAME,
@@ -32,13 +32,13 @@ import {
32
32
  resolveChainSettlement,
33
33
  selectSettlementChain,
34
34
  solanaTokenBalance
35
- } from "./chunk-TTJCBGKW.js";
35
+ } from "./chunk-UZZ2RN7A.js";
36
36
  import {
37
37
  fetchRemoteState
38
38
  } from "./chunk-2KJJ7LSO.js";
39
39
 
40
40
  // src/cli/standalone-mode.ts
41
- import { readFileSync } from "fs";
41
+ import { readFileSync as readFileSync2 } from "fs";
42
42
  import { homedir } from "os";
43
43
  import { join } from "path";
44
44
  import {
@@ -51,8 +51,101 @@ import {
51
51
  encodeEventToToon,
52
52
  resolveClientNetwork
53
53
  } from "@toon-protocol/core";
54
- var OFFICIAL_PROXY_URL = "https://proxy.devnet.toonprotocol.dev/rust/ilp";
54
+
55
+ // src/standalone/notice.ts
56
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
57
+ import { dirname } from "path";
58
+ var NOTICE_STORE_FILENAME = "rig-seen-notices.json";
59
+ function normalizeNotice(value) {
60
+ if (typeof value !== "object" || value === null) return void 0;
61
+ const { id, severity, summary, url } = value;
62
+ if (typeof id !== "string" || id.length === 0) return void 0;
63
+ if (typeof summary !== "string" || summary.length === 0) return void 0;
64
+ if (typeof url !== "string" || url.length === 0) return void 0;
65
+ return {
66
+ id,
67
+ summary,
68
+ url,
69
+ severity: severity === "action-required" ? "action-required" : "info"
70
+ };
71
+ }
72
+ function trustedNoticeFrom(peer, seedPubkeys) {
73
+ if (!peer || !seedPubkeys.includes(peer.pubkey)) return void 0;
74
+ return normalizeNotice(peer.info.notice);
75
+ }
76
+ var ACTION_REQUIRED_BORDER = "=".repeat(70);
77
+ function formatNoticeLines(notice) {
78
+ if (notice.severity === "action-required") {
79
+ return [
80
+ "",
81
+ ACTION_REQUIRED_BORDER,
82
+ `rig: ACTION REQUIRED \u2014 ${notice.summary}`,
83
+ `rig: ${notice.url}`,
84
+ ACTION_REQUIRED_BORDER,
85
+ ""
86
+ ];
87
+ }
88
+ return [`rig: notice \u2014 ${notice.summary} (${notice.url})`];
89
+ }
90
+ var NoticeStore = class {
91
+ path;
92
+ constructor(options) {
93
+ this.path = options.path;
94
+ }
95
+ hasSeen(id) {
96
+ return this.readFile().seenIds.includes(id);
97
+ }
98
+ /** Best-effort; a failed write just means the notice reprints next run. */
99
+ markSeen(id) {
100
+ const file = this.readFile();
101
+ if (file.seenIds.includes(id)) return;
102
+ try {
103
+ mkdirSync(dirname(this.path), { recursive: true });
104
+ writeFileSync(
105
+ this.path,
106
+ JSON.stringify(
107
+ {
108
+ version: 1,
109
+ seenIds: [...file.seenIds, id]
110
+ },
111
+ null,
112
+ 2
113
+ ),
114
+ { mode: 384 }
115
+ );
116
+ } catch {
117
+ }
118
+ }
119
+ readFile() {
120
+ const empty = { version: 1, seenIds: [] };
121
+ let raw;
122
+ try {
123
+ raw = readFileSync(this.path, "utf8");
124
+ } catch {
125
+ return empty;
126
+ }
127
+ try {
128
+ const parsed = JSON.parse(raw);
129
+ if (typeof parsed !== "object" || parsed === null || parsed.version !== 1 || !Array.isArray(parsed.seenIds) || !parsed.seenIds.every((x) => typeof x === "string")) {
130
+ return empty;
131
+ }
132
+ return parsed;
133
+ } catch {
134
+ return empty;
135
+ }
136
+ }
137
+ };
138
+ function showOperatorNoticeOnce(peer, seedPubkeys, store, warn) {
139
+ const notice = trustedNoticeFrom(peer, seedPubkeys);
140
+ if (!notice || store.hasSeen(notice.id)) return;
141
+ for (const line of formatNoticeLines(notice)) warn(line);
142
+ store.markSeen(notice.id);
143
+ }
144
+
145
+ // src/cli/standalone-mode.ts
146
+ var OFFICIAL_PROXY_URL = "https://proxy.relay.devnet.toonprotocol.dev/ilp";
55
147
  var OFFICIAL_PUBLISH_DESTINATION = "g.toon.relay";
148
+ var OFFICIAL_BTP_URL = "wss://proxy.relay.devnet.toonprotocol.dev/ilp/btp";
56
149
  var MissingUplinkError = class extends Error {
57
150
  constructor(configPath, relayUrl) {
58
151
  const discovered = relayUrl ? `no announce with a btp/http endpoint was found on ${relayUrl} and the genesis seed has none; ` : "";
@@ -67,7 +160,7 @@ function configDir(env) {
67
160
  }
68
161
  function readClientConfig(path) {
69
162
  try {
70
- return JSON.parse(readFileSync(path, "utf8"));
163
+ return JSON.parse(readFileSync2(path, "utf8"));
71
164
  } catch (err) {
72
165
  if (err.code === "ENOENT") return {};
73
166
  throw new Error(
@@ -181,18 +274,38 @@ async function resolveNetworkTopology(inputs) {
181
274
  ...file.tokenNetworks ? { tokenNetworks: file.tokenNetworks } : {}
182
275
  };
183
276
  let proxyUrl = explicitProxyUrl;
184
- const btpUrl = explicitBtpUrl;
277
+ let btpUrl = explicitBtpUrl;
185
278
  if (!proxyUrl && !btpUrl) {
186
279
  proxyUrl = OFFICIAL_PROXY_URL;
187
280
  }
281
+ let btpUrlDerived = false;
282
+ if (!explicitProxyUrl && !explicitBtpUrl) {
283
+ btpUrl = announce?.info.btpEndpoint || OFFICIAL_BTP_URL;
284
+ btpUrlDerived = true;
285
+ }
188
286
  const destination = explicitDestination ?? announce?.info.ilpAddress ?? genesisSeed?.ilpAddress ?? "g.proxy";
189
287
  const publishDestination = explicitPublish ?? announce?.routes?.publish ?? (proxyUrl === OFFICIAL_PROXY_URL ? OFFICIAL_PUBLISH_DESTINATION : void 0);
190
288
  const storeDestination = explicitStore ?? announce?.routes?.store;
289
+ const explicitStoreBtpUrl = env["TOON_CLIENT_STORE_BTP_URL"] ?? file.storeBtpUrl;
290
+ let storeBtpUrl;
291
+ if (storeDestination && storeDestination !== publishDestination) {
292
+ const storePeer = inputs.peers?.find(
293
+ (peer) => peer.info.ilpAddress === storeDestination
294
+ );
295
+ storeBtpUrl = explicitStoreBtpUrl || storePeer?.info.btpEndpoint || void 0;
296
+ if (!storeBtpUrl && inputs.peers?.length) {
297
+ warn(
298
+ `rig: no announce found for the store route "${storeDestination}" \u2014 store writes (git objects, the rig page) will be attempted on the publish uplink and the terminating connector will refuse them if it does not hold that channel`
299
+ );
300
+ }
301
+ }
191
302
  let routePrices;
192
- const capabilities = announce?.capabilities;
193
- if (capabilities && capabilities.length > 0) {
303
+ {
194
304
  const derived = deriveRouteDestinations(destination);
195
- const priceOf = (address) => capabilities.find((c) => c.address === address)?.price;
305
+ const priceOf = (address) => {
306
+ const owner = inputs.peers?.find((p) => p.info.ilpAddress === address) ?? announce;
307
+ return owner?.routePrices?.[address] ?? owner?.capabilities?.find((c) => c.address === address)?.price;
308
+ };
196
309
  const publishPrice = priceOf(publishDestination ?? derived.publish);
197
310
  const storePrice = priceOf(storeDestination ?? derived.store);
198
311
  if (publishPrice !== void 0 || storePrice !== void 0) {
@@ -386,10 +499,11 @@ async function resolveNetworkTopology(inputs) {
386
499
  }
387
500
  return {
388
501
  ...proxyUrl ? { proxyUrl } : {},
389
- ...btpUrl ? { btpUrl } : {},
502
+ ...btpUrl ? { btpUrl, ...btpUrlDerived ? { btpUrlDerived } : {} } : {},
390
503
  destination,
391
504
  ...publishDestination ? { publishDestination } : {},
392
505
  ...storeDestination ? { storeDestination } : {},
506
+ ...storeBtpUrl ? { storeBtpUrl } : {},
393
507
  ...routePrices ? { routePrices } : {},
394
508
  knownPeers,
395
509
  ...selection ? { selection } : {},
@@ -401,6 +515,49 @@ async function resolveNetworkTopology(inputs) {
401
515
  ...minaChannel ? { minaChannel } : {}
402
516
  };
403
517
  }
518
+ function resolveUplinkConfig(topo) {
519
+ return {
520
+ // validateConfig requires connectorUrl OR proxyUrl; with BTP-only
521
+ // config a dummy connectorUrl satisfies it (unused at runtime — same
522
+ // convention as the daemon).
523
+ ...topo.proxyUrl ? { proxyUrl: topo.proxyUrl } : { connectorUrl: "http://127.0.0.1:1" },
524
+ ...topo.btpUrl ? {
525
+ btpUrl: topo.btpUrl,
526
+ btpAuthToken: "",
527
+ preferBtpForPaidWrites: true
528
+ } : {}
529
+ };
530
+ }
531
+ function isBtpTransportError(err) {
532
+ const name = err instanceof Error ? err.name : "";
533
+ return name === "BtpConnectionError" || name === "BtpAuthError";
534
+ }
535
+ function bootstrapRecoveries(args) {
536
+ let active = args.topology;
537
+ const detail = (err) => err instanceof Error ? err.message : String(err);
538
+ const recoveries = [];
539
+ const reresolve = args.reresolve;
540
+ if (reresolve) {
541
+ recoveries.push({
542
+ applies: () => true,
543
+ describe: (err) => `rig: bootstrap with the cached network topology failed (${detail(err)}) \u2014 invalidating the cache and re-resolving live`,
544
+ rebuild: async () => {
545
+ active = await reresolve();
546
+ return args.buildPublisher(active);
547
+ }
548
+ });
549
+ }
550
+ recoveries.push({
551
+ applies: (err) => active.btpUrlDerived === true && isBtpTransportError(err),
552
+ describe: (err) => `rig: the discovered BTP uplink ${active.btpUrl} is unreachable (${detail(err)}) \u2014 continuing over the HTTP uplink alone; paid writes an edge accepts only over BTP will still fail, so pin a reachable endpoint with \`rig entry <url>\` if they do`,
553
+ rebuild: async () => {
554
+ const { btpUrl: _btpUrl, btpUrlDerived: _derived, ...http } = active;
555
+ active = http;
556
+ return args.buildPublisher(active);
557
+ }
558
+ });
559
+ return recoveries;
560
+ }
404
561
  function isNetworkTopology(value) {
405
562
  if (typeof value !== "object" || value === null) return false;
406
563
  const t = value;
@@ -417,11 +574,11 @@ var NON_RECOVERABLE_START_ERRORS = /* @__PURE__ */ new Set([
417
574
  ]);
418
575
  var TopologyRecoveringPublisher = class {
419
576
  inner;
420
- rebuild;
577
+ recoveries;
421
578
  warn;
422
- constructor(inner, rebuild, warn) {
579
+ constructor(inner, recoveries, warn) {
423
580
  this.inner = inner;
424
- this.rebuild = rebuild;
581
+ this.recoveries = [...recoveries];
425
582
  this.warn = warn;
426
583
  }
427
584
  getPublicKey() {
@@ -431,30 +588,31 @@ var TopologyRecoveringPublisher = class {
431
588
  return this.inner.getFeeRates();
432
589
  }
433
590
  /**
434
- * Run the bootstrap step, recovering ONCE from a stale cached topology.
435
- * A rebuild failure surfaces the LIVE resolution's error — that is the
436
- * network's real state, strictly more actionable than the cached failure.
591
+ * Run the bootstrap step, retrying against a repaired publisher for as long
592
+ * as an unused recovery applies. The LAST attempt's error is what surfaces
593
+ * — after a live re-resolution or an HTTP-only rebuild that is the
594
+ * network's real state, strictly more actionable than the failure that
595
+ * triggered the retry.
437
596
  */
438
597
  async ensure(start) {
439
- try {
440
- await start(this.inner);
441
- return this.inner;
442
- } catch (err) {
443
- const rebuild = this.rebuild;
444
- this.rebuild = void 0;
445
- const name = err instanceof Error ? err.name : "";
446
- if (!rebuild || NON_RECOVERABLE_START_ERRORS.has(name)) throw err;
447
- this.warn(
448
- `rig: bootstrap with the cached network topology failed (${err instanceof Error ? err.message : String(err)}) \u2014 invalidating the cache and re-resolving live`
449
- );
450
- const fresh = await rebuild();
598
+ for (; ; ) {
451
599
  try {
452
- await this.inner.stop();
453
- } catch {
600
+ await start(this.inner);
601
+ return this.inner;
602
+ } catch (err) {
603
+ const name = err instanceof Error ? err.name : "";
604
+ if (NON_RECOVERABLE_START_ERRORS.has(name)) throw err;
605
+ const index = this.recoveries.findIndex((r) => r.applies(err));
606
+ if (index < 0) throw err;
607
+ const [recovery] = this.recoveries.splice(index, 1);
608
+ this.warn(recovery.describe(err));
609
+ const fresh = await recovery.rebuild();
610
+ try {
611
+ await this.inner.stop();
612
+ } catch {
613
+ }
614
+ this.inner = fresh;
454
615
  }
455
- this.inner = fresh;
456
- await start(this.inner);
457
- return this.inner;
458
616
  }
459
617
  }
460
618
  async publishEvent(event, relayUrls) {
@@ -519,6 +677,10 @@ async function createStandaloneContext(options) {
519
677
  // shared public devnet relay rather than a dead local default.
520
678
  "wss://relay-ws.devnet.toonprotocol.dev";
521
679
  const channelStorePath = file.channelStorePath ?? join(dir, "channels.json");
680
+ const storeChannelStorePath = channelStorePath.replace(
681
+ /(\.json)?$/,
682
+ ".store.json"
683
+ );
522
684
  const channelMap = new ChannelMapStore({
523
685
  mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),
524
686
  watermarkPath: channelStorePath
@@ -536,6 +698,9 @@ async function createStandaloneContext(options) {
536
698
  file
537
699
  )
538
700
  });
701
+ const noticeStore = new NoticeStore({
702
+ path: join(dir, NOTICE_STORE_FILENAME)
703
+ });
539
704
  const resolveLiveTopology = async () => {
540
705
  const solanaExplicitlyComplete = (file.supportedChains ?? []).every(
541
706
  (chain) => !chain.startsWith("solana:") || Boolean(file.solanaChannel) || Boolean(
@@ -545,21 +710,28 @@ async function createStandaloneContext(options) {
545
710
  const minaExplicitlyComplete = (file.supportedChains ?? []).every(
546
711
  (chain) => !chain.startsWith("mina:") || Boolean(file.minaChannel)
547
712
  );
713
+ const explicitStoreDest = env["TOON_CLIENT_STORE_DESTINATION"] ?? file.storeDestination;
714
+ const explicitPublishDest = env["TOON_CLIENT_PUBLISH_DESTINATION"] ?? file.publishDestination;
715
+ const storeUplinkKnown = !explicitStoreDest || explicitStoreDest === explicitPublishDest || Boolean(env["TOON_CLIENT_STORE_BTP_URL"] ?? file.storeBtpUrl);
548
716
  const fullyExplicit = Boolean(
549
717
  (env["TOON_CLIENT_PROXY_URL"] ?? file.proxyUrl) || (env["TOON_CLIENT_BTP_URL"] ?? file.btpUrl)
550
- ) && Boolean(env["TOON_CLIENT_DESTINATION"] ?? file.destination) && Boolean(file.supportedChains?.length) && solanaExplicitlyComplete && minaExplicitlyComplete;
718
+ ) && Boolean(env["TOON_CLIENT_DESTINATION"] ?? file.destination) && Boolean(file.supportedChains?.length) && storeUplinkKnown && solanaExplicitlyComplete && minaExplicitlyComplete;
551
719
  let announce;
720
+ let discoveredPeers;
552
721
  if (!fullyExplicit) {
553
722
  try {
554
723
  const peers = await discoverAnnouncedPeers(relayUrl, {
555
724
  timeoutMs: DISCOVERY_TIMEOUT_MS
556
725
  });
557
- announce = pickPaymentPeer(peers, genesisSeedPubkeys());
726
+ discoveredPeers = peers;
727
+ const seedPubkeys = genesisSeedPubkeys();
728
+ announce = pickPaymentPeer(peers, seedPubkeys);
558
729
  if (!announce) {
559
730
  warn(
560
731
  `rig: no payment-peer announce (kind:10032) found on ${relayUrl} \u2014 falling back to the genesis peer seed`
561
732
  );
562
733
  }
734
+ showOperatorNoticeOnce(announce, seedPubkeys, noticeStore, warn);
563
735
  } catch (err) {
564
736
  warn(
565
737
  `rig: announce discovery on ${relayUrl} failed (${err instanceof Error ? err.message : String(err)}) \u2014 falling back to the genesis peer seed`
@@ -572,6 +744,7 @@ async function createStandaloneContext(options) {
572
744
  configPath,
573
745
  relayUrl,
574
746
  announce,
747
+ ...discoveredPeers ? { peers: discoveredPeers } : {},
575
748
  genesisSeed,
576
749
  identity: {
577
750
  mnemonic: identity.mnemonic,
@@ -595,10 +768,7 @@ async function createStandaloneContext(options) {
595
768
  const eventFee = BigInt(file.feePerEvent ?? "1");
596
769
  const buildPublisher = (topo) => {
597
770
  const clientConfig = {
598
- // validateConfig requires connectorUrl OR proxyUrl; with BTP-only
599
- // config a dummy connectorUrl satisfies it (unused at runtime — same
600
- // convention as the daemon).
601
- ...topo.proxyUrl ? { proxyUrl: topo.proxyUrl } : { connectorUrl: "http://127.0.0.1:1" },
771
+ ...resolveUplinkConfig(topo),
602
772
  mnemonic: identity.mnemonic,
603
773
  mnemonicAccountIndex: identity.accountIndex,
604
774
  ilpInfo: {
@@ -610,7 +780,6 @@ async function createStandaloneContext(options) {
610
780
  },
611
781
  toonEncoder: encodeEventToToon,
612
782
  toonDecoder: decodeEventFromToon,
613
- ...topo.btpUrl ? { btpUrl: topo.btpUrl, btpAuthToken: "" } : {},
614
783
  destinationAddress: topo.destination,
615
784
  // The embedded client bootstraps against the known peer above; its
616
785
  // `relayUrl` config only seeds ArDrive-merged peers, so it stays unset.
@@ -646,8 +815,31 @@ async function createStandaloneContext(options) {
646
815
  }
647
816
  } : {}
648
817
  };
818
+ const storeClientConfig = topo.storeBtpUrl ? {
819
+ ...clientConfig,
820
+ proxyUrl: void 0,
821
+ connectorUrl: "http://127.0.0.1:1",
822
+ btpUrl: topo.storeBtpUrl,
823
+ btpAuthToken: "",
824
+ preferBtpForPaidWrites: true,
825
+ // Its OWN watermark file. A client's channel store and its
826
+ // `.peers.json` binding sidecar are one unit that the client
827
+ // rewrites wholesale, so pointing two live clients at one path has
828
+ // them clobber each other's watermarks — observed as the publish
829
+ // leg losing its channel ("is not being tracked", then F01) the
830
+ // moment the store leg opened. Separate files; the legs stay
831
+ // correlated through rig's own channel map, which is keyed by
832
+ // anchor and records both.
833
+ channelStorePath: storeChannelStorePath,
834
+ ilpInfo: {
835
+ ...clientConfig.ilpInfo,
836
+ btpEndpoint: topo.storeBtpUrl
837
+ },
838
+ destinationAddress: topo.storeDestination ?? topo.destination
839
+ } : void 0;
649
840
  return new StandalonePublisher({
650
841
  clientConfig,
842
+ ...storeClientConfig ? { storeClientConfig } : {},
651
843
  eventFee,
652
844
  channelMap,
653
845
  warn,
@@ -680,10 +872,18 @@ async function createStandaloneContext(options) {
680
872
  };
681
873
  const publisher = new TopologyRecoveringPublisher(
682
874
  buildPublisher(topology),
683
- cached ? async () => {
684
- cache.invalidate(cacheKey);
685
- return buildPublisher(await resolveLiveTopology());
686
- } : void 0,
875
+ bootstrapRecoveries({
876
+ topology,
877
+ buildPublisher,
878
+ // Only a CACHE-sourced publisher gets the re-resolve recovery;
879
+ // live-resolved ones have nothing staler to fall back from.
880
+ ...cached ? {
881
+ reresolve: async () => {
882
+ cache.invalidate(cacheKey);
883
+ return resolveLiveTopology();
884
+ }
885
+ } : {}
886
+ }),
687
887
  warn
688
888
  );
689
889
  const walletViewNetwork = env["TOON_CLIENT_NETWORK"] ?? file.network;
@@ -715,10 +915,14 @@ async function createStandaloneContext(options) {
715
915
  }
716
916
  export {
717
917
  MissingUplinkError,
918
+ OFFICIAL_BTP_URL,
718
919
  OFFICIAL_PROXY_URL,
719
920
  OFFICIAL_PUBLISH_DESTINATION,
921
+ bootstrapRecoveries,
720
922
  buildMinaAutoDeploy,
721
923
  createStandaloneContext,
722
- resolveNetworkTopology
924
+ isBtpTransportError,
925
+ resolveNetworkTopology,
926
+ resolveUplinkConfig
723
927
  };
724
- //# sourceMappingURL=standalone-mode-LBWGU7NV.js.map
928
+ //# sourceMappingURL=standalone-mode-5MNNJC2D.js.map