@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.
@@ -25,6 +25,12 @@ function deriveRouteDestinations(anchor) {
25
25
  }
26
26
  return { publish: anchor, store: anchor };
27
27
  }
28
+ function channelBindingKey(peerId, chain, tokenNetwork) {
29
+ return `${peerId}|${chain}|${tokenNetwork}`;
30
+ }
31
+ function peerIdFromBindingKey(key) {
32
+ return key.split("|")[0] ?? key;
33
+ }
28
34
  function channelInternals(client) {
29
35
  const c = client;
30
36
  const negotiations = c.peerNegotiations instanceof Map ? c.peerNegotiations : void 0;
@@ -53,6 +59,12 @@ var StandalonePublisher = class {
53
59
  channelAnchor;
54
60
  negotiationFallbacks;
55
61
  warn;
62
+ storeClientConfig;
63
+ injectedStoreClient;
64
+ /** Lazily built on the first store write — see {@link storeLeg}. */
65
+ storeClient;
66
+ storeChannelId;
67
+ storeLegPromise;
56
68
  lock;
57
69
  channelId;
58
70
  readyPromise;
@@ -90,6 +102,13 @@ var StandalonePublisher = class {
90
102
  this.fetchImpl = options.fetchImpl;
91
103
  this.channelMap = options.channelMap;
92
104
  this.channelAnchor = anchor;
105
+ if (options.storeClient && options.storeClientConfig) {
106
+ throw new Error(
107
+ "StandalonePublisher: provide either `storeClientConfig` or `storeClient`, not both"
108
+ );
109
+ }
110
+ this.storeClientConfig = options.storeClientConfig;
111
+ this.injectedStoreClient = options.storeClient;
93
112
  this.negotiationFallbacks = options.negotiationFallbacks;
94
113
  this.warn = options.warn ?? ((line) => process.stderr.write(`${line}
95
114
  `));
@@ -194,32 +213,59 @@ var StandalonePublisher = class {
194
213
  * file throws BEFORE anything is opened — never a silent duplicate open.
195
214
  */
196
215
  async openOrResumeChannel() {
216
+ return this.openOrResumeChannelOn(this.client, this.channelDestination, {
217
+ anchor: this.channelAnchor,
218
+ trackResumeState: true
219
+ });
220
+ }
221
+ /**
222
+ * {@link openOrResumeChannel} for an arbitrary client + destination, so
223
+ * the store leg gets the same resume-before-open guarantee (and the same
224
+ * corruption check) as the publish leg rather than a second, weaker path.
225
+ *
226
+ * `anchor` defaults to the destination: a leg other than the publish one
227
+ * is keyed in the channel map by the route it pays for.
228
+ * `trackResumeState` is publish-only — {@link lastOpenResumed} feeds the
229
+ * `resumed` field of this publisher's channel status, which describes the
230
+ * publish channel and would be wrong to overwrite from a store open.
231
+ */
232
+ async openOrResumeChannelOn(client, destination, opts = {}) {
197
233
  const map = this.channelMap;
234
+ const trackResumeState = opts.trackResumeState ?? false;
198
235
  if (!map) {
199
- return this.client.openChannel(this.channelDestination);
236
+ return client.openChannel(destination);
200
237
  }
201
- if (!this.channelAnchor) {
238
+ const anchorFor = opts.anchor ?? destination;
239
+ if (!anchorFor) {
202
240
  this.warn(
203
241
  "rig: no channel anchor destination configured \u2014 the peer\u2192channel mapping cannot be persisted, so this run may open a fresh channel"
204
242
  );
205
- return this.client.openChannel(this.channelDestination);
243
+ return client.openChannel(destination);
206
244
  }
207
- const anchor = this.channelAnchor;
208
- const identity = this.client.getPublicKey();
245
+ const anchor = anchorFor;
246
+ const identity = client.getPublicKey();
209
247
  const candidates = map.listFor(identity, anchor);
210
- const internals = channelInternals(this.client);
248
+ const internals = channelInternals(client);
211
249
  const resumed = await this.resumeRecordedChannel(
212
250
  map,
213
251
  candidates,
214
- internals
252
+ internals,
253
+ client
215
254
  );
216
- const channelId = await this.client.openChannel(this.channelDestination);
255
+ const channelId = await client.openChannel(destination);
217
256
  if (resumed && channelId === resumed.channelId) {
218
- this.lastOpenResumed = true;
257
+ if (trackResumeState) this.lastOpenResumed = true;
219
258
  map.touch(recordKey(resumed));
220
259
  } else {
221
- this.lastOpenResumed = false;
222
- this.recordOpenedChannel(map, internals, identity, anchor, channelId);
260
+ if (trackResumeState) this.lastOpenResumed = false;
261
+ this.recordOpenedChannel(
262
+ map,
263
+ internals,
264
+ identity,
265
+ anchor,
266
+ channelId,
267
+ client
268
+ );
223
269
  }
224
270
  return channelId;
225
271
  }
@@ -228,7 +274,7 @@ var StandalonePublisher = class {
228
274
  * still negotiated on the SAME chain + tokenNetwork and whose watermark
229
275
  * does not show it closed/settled. Returns the resumed record, if any.
230
276
  */
231
- async resumeRecordedChannel(map, candidates, internals) {
277
+ async resumeRecordedChannel(map, candidates, internals, client) {
232
278
  if (candidates.length === 0) return void 0;
233
279
  const cm = internals.channelManager;
234
280
  if (!cm?.trackChannel || !cm.peerChannels) {
@@ -252,10 +298,13 @@ var StandalonePublisher = class {
252
298
  );
253
299
  }
254
300
  cm.trackChannel(record.channelId, record.context);
255
- cm.peerChannels.set(record.peerId, record.channelId);
256
- if (record.context.chainType === "evm" && record.depositTotal === void 0 && this.client.rehydrateChannelDeposit) {
301
+ cm.peerChannels.set(
302
+ channelBindingKey(record.peerId, record.chain, record.tokenNetwork),
303
+ record.channelId
304
+ );
305
+ if (record.context.chainType === "evm" && record.depositTotal === void 0 && client.rehydrateChannelDeposit) {
257
306
  try {
258
- const deposit = await this.client.rehydrateChannelDeposit(
307
+ const deposit = await client.rehydrateChannelDeposit(
259
308
  record.channelId,
260
309
  {
261
310
  chain: record.chain,
@@ -278,11 +327,11 @@ var StandalonePublisher = class {
278
327
  return void 0;
279
328
  }
280
329
  /** Record a freshly opened channel (+ seed its claim watermark at 0/0). */
281
- recordOpenedChannel(map, internals, identity, destination, channelId) {
330
+ recordOpenedChannel(map, internals, identity, destination, channelId, client) {
282
331
  let peerId;
283
- for (const [peer, channel] of internals.channelManager?.peerChannels ?? []) {
332
+ for (const [key, channel] of internals.channelManager?.peerChannels ?? []) {
284
333
  if (channel === channelId) {
285
- peerId = peer;
334
+ peerId = peerIdFromBindingKey(key);
286
335
  break;
287
336
  }
288
337
  }
@@ -295,7 +344,7 @@ var StandalonePublisher = class {
295
344
  }
296
345
  let depositTotal;
297
346
  try {
298
- depositTotal = this.client.getChannelDepositTotal?.(channelId);
347
+ depositTotal = client.getChannelDepositTotal?.(channelId);
299
348
  } catch {
300
349
  }
301
350
  map.record({
@@ -325,6 +374,13 @@ var StandalonePublisher = class {
325
374
  this.readyPromise = void 0;
326
375
  this.clientReadyPromise = void 0;
327
376
  this.channelId = void 0;
377
+ if (this.storeClient) {
378
+ const storeClient = this.storeClient;
379
+ this.storeClient = void 0;
380
+ this.storeLegPromise = void 0;
381
+ this.storeChannelId = void 0;
382
+ await storeClient.stop();
383
+ }
328
384
  if (this.ownsClient && this.client.isStarted?.() !== false) {
329
385
  await this.client.stop();
330
386
  }
@@ -383,7 +439,10 @@ var StandalonePublisher = class {
383
439
  const cm = internals.channelManager;
384
440
  if (cm?.trackChannel && cm.peerChannels) {
385
441
  cm.trackChannel(record.channelId, record.context);
386
- cm.peerChannels.set(record.peerId, record.channelId);
442
+ cm.peerChannels.set(
443
+ channelBindingKey(record.peerId, record.chain, record.tokenNetwork),
444
+ record.channelId
445
+ );
387
446
  }
388
447
  const contextCache = internals.onChainChannelClient?.channelContext;
389
448
  if (contextCache && !contextCache.has(record.channelId)) {
@@ -494,10 +553,10 @@ var StandalonePublisher = class {
494
553
  );
495
554
  }
496
555
  await this.start();
497
- const channelId = this.requireChannel();
556
+ const leg = await this.storeLeg();
498
557
  const contentType = upload.type === "blob" ? contentTypeForPath(upload.path) : DEFAULT_CONTENT_TYPE;
499
558
  const fee = this.uploadFee();
500
- const event = this.client.signEvent({
559
+ const event = leg.client.signEvent({
501
560
  kind: 5094,
502
561
  content: "",
503
562
  created_at: nowSeconds(),
@@ -510,8 +569,8 @@ var StandalonePublisher = class {
510
569
  ["Repo", upload.repoId]
511
570
  ]
512
571
  });
513
- const claim = await this.client.signBalanceProof(channelId, fee);
514
- const result = await this.client.publishEvent(event, {
572
+ const claim = await leg.client.signBalanceProof(leg.channelId, fee);
573
+ const result = await leg.client.publishEvent(event, {
515
574
  ...this.storeDestination ? { destination: this.storeDestination } : {},
516
575
  claim,
517
576
  ilpAmount: fee,
@@ -545,9 +604,9 @@ var StandalonePublisher = class {
545
604
  );
546
605
  }
547
606
  await this.start();
548
- const channelId = this.requireChannel();
607
+ const leg = await this.storeLeg();
549
608
  const fee = this.uploadFee();
550
- const event = this.client.signEvent({
609
+ const event = leg.client.signEvent({
551
610
  kind: 5094,
552
611
  content: "",
553
612
  created_at: nowSeconds(),
@@ -558,8 +617,8 @@ var StandalonePublisher = class {
558
617
  ...upload.repoId ? [["Repo", upload.repoId]] : []
559
618
  ]
560
619
  });
561
- const claim = await this.client.signBalanceProof(channelId, fee);
562
- const result = await this.client.publishEvent(event, {
620
+ const claim = await leg.client.signBalanceProof(leg.channelId, fee);
621
+ const result = await leg.client.publishEvent(event, {
563
622
  ...this.storeDestination ? { destination: this.storeDestination } : {},
564
623
  claim,
565
624
  ilpAmount: fee,
@@ -617,6 +676,40 @@ var StandalonePublisher = class {
617
676
  }
618
677
  return this.channelId;
619
678
  }
679
+ /**
680
+ * The client + channel a STORE write must be paid on.
681
+ *
682
+ * With no `storeClientConfig` this is the publish leg unchanged — the
683
+ * single-node case, where the store terminates on the connector that
684
+ * already holds the channel. With one, the store node is a different
685
+ * connector holding a different channel, and paying it from the publish
686
+ * channel is precisely the `F01 - claim rejected: names a channel this
687
+ * connector has no record of` failure this exists to prevent.
688
+ *
689
+ * Built on FIRST STORE WRITE, not in `start()`: `rig push` with nothing to
690
+ * upload, and every read-only command, should not open a second on-chain
691
+ * channel (gas) for a leg they never use. Memoised on the promise so
692
+ * concurrent uploads share one open rather than racing two.
693
+ */
694
+ async storeLeg() {
695
+ const injected = this.injectedStoreClient;
696
+ const config = this.storeClientConfig;
697
+ if (!config && !injected) {
698
+ return { client: this.client, channelId: this.requireChannel() };
699
+ }
700
+ this.storeLegPromise ??= (async () => {
701
+ const client = injected ?? new ToonClient(config);
702
+ this.storeClient = client;
703
+ await client.start();
704
+ const channelId = await this.openOrResumeChannelOn(
705
+ client,
706
+ this.storeDestination
707
+ );
708
+ this.storeChannelId = channelId;
709
+ return { client, channelId };
710
+ })();
711
+ return this.storeLegPromise;
712
+ }
620
713
  };
621
714
  function nowSeconds() {
622
715
  return Math.floor(Date.now() / 1e3);
@@ -628,4 +721,4 @@ export {
628
721
  deriveRouteDestinations,
629
722
  StandalonePublisher
630
723
  };
631
- //# sourceMappingURL=chunk-4CAOEXGQ.js.map
724
+ //# sourceMappingURL=chunk-IZ6KLYQ6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/standalone/standalone-publisher.ts"],"sourcesContent":["/**\n * StandalonePublisher — impl 2 of the {@link Publisher} seam (#228): an\n * EMBEDDED ToonClient constructed from the caller's config (mnemonic +\n * account index, the exact `packages/client/src/config.ts` shape) instead of\n * routing through a running toon-clientd. Made for CI jobs, servers, and\n * one-shot CLI runs where no daemon exists.\n *\n * Paid-write mechanics mirror the production daemon path\n * (`client-mcp/src/daemon/client-runner.ts`) and the proven seed pipeline\n * (`rig/tests/e2e/seed/lib/{publish,git-builder}.ts`):\n *\n * - one signed balance-proof CLAIM per write (`signBalanceProof(channelId,\n * fee)` — the ChannelManager accumulates the cumulative watermark),\n * - publishes route to the relay write destination with a flat per-event\n * fee (the daemon's `feePerEvent` convention),\n * - git objects upload as kind:5094 store writes tagged\n * Git-SHA/Git-Type/Repo, priced at bytes × per-byte rate (the seed\n * pipeline's bid), routed to the store destination via `proxyPath:\n * '/store'`, with the Arweave txId decoded from the FULFILL data.\n *\n * Because the embedded client signs claims on the SAME channels a daemon on\n * the same identity would, every paid operation is preceded by the nonce\n * guard (./nonce-guard.ts): refuse if a toon-clientd holds this identity,\n * and hold an exclusive per-pubkey lockfile against other standalone\n * processes for the lifetime of this publisher.\n *\n * Channel REUSE (#262): with a `channelMap` configured, start() resumes the\n * channel recorded for (identity, channel anchor) — `trackChannel` rehydrates\n * the cumulative-claim watermark from the client's channels.json — and\n * records any fresh lazy open, so sequential CLI invocations share ONE\n * on-chain channel instead of stranding a deposit per run (./channel-map.ts).\n */\n\nimport type {\n PublishEventResult,\n SignedBalanceProof,\n ToonClientConfig,\n} from '@toon-protocol/client';\nimport { ToonClient, extractArweaveTxId } from '@toon-protocol/client';\nimport { MAX_OBJECT_SIZE } from '../objects.js';\nimport { contentTypeForPath, DEFAULT_CONTENT_TYPE } from '../mime.js';\nimport type { UnsignedEvent } from '../nip34-events.js';\nimport {\n type BlobUpload,\n type FeeRates,\n type GitObjectUpload,\n type PublishReceipt,\n type Publisher,\n type UploadReceipt,\n} from '../publisher.js';\nimport {\n recordKey,\n type ChannelMapRecord,\n type ChannelMapStore,\n type PersistedChannelContext,\n} from './channel-map.js';\nimport type {\n ChannelCloseOutcome,\n ChannelOpenOutcome,\n ChannelSettleOutcome,\n WalletChainBalanceInfo,\n} from './money.js';\nimport { checkDaemonIdentity, NonceLock } from './nonce-guard.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A fully-signed Nostr event (structural subset of nostr-tools' NostrEvent). */\nexport interface SignedNostrEvent extends UnsignedEvent {\n id: string;\n pubkey: string;\n sig: string;\n}\n\n/**\n * The slice of `ToonClient` the publisher drives. Kept structural so tests\n * inject a mock and alternative client builds stay compatible.\n */\nexport interface ToonClientLike {\n start(): Promise<unknown>;\n stop(): Promise<void>;\n isStarted?(): boolean;\n getPublicKey(): string;\n signEvent(template: UnsignedEvent): SignedNostrEvent;\n openChannel(destination?: string): Promise<string>;\n signBalanceProof(\n channelId: string,\n amount: bigint\n ): Promise<SignedBalanceProof>;\n publishEvent(\n event: SignedNostrEvent,\n options?: {\n destination?: string;\n claim?: SignedBalanceProof;\n ilpAmount?: bigint;\n proxyPath?: string;\n }\n ): Promise<PublishEventResult>;\n /** On-chain deposit total for a tracked channel (channel-map bookkeeping). */\n getChannelDepositTotal?(channelId: string): bigint;\n /** Re-read a RESUMED channel's on-chain deposit (persisted state omits it). */\n rehydrateChannelDeposit?(\n channelId: string,\n opts: { chain: string; tokenNetworkAddress: string }\n ): Promise<bigint | undefined>;\n // ── money lifecycle (#263) — optional, matches ToonClient's surface ──────\n /** Deposit extra collateral (base-unit delta) into an open channel. */\n depositToChannel?(\n channelId: string,\n amount: string | bigint\n ): Promise<{ channelId: string; txHash?: string; depositTotal: string }>;\n /** Close a channel — starts the settlement challenge window (on-chain). */\n closeChannel?(channelId: string): Promise<{\n channelId: string;\n txHash?: string;\n /** Unix SECONDS, string-encoded. */\n closedAt: string;\n settleableAt: string;\n }>;\n /** Settle a closed channel after its window — releases funds (on-chain). */\n settleChannel?(\n channelId: string\n ): Promise<{ channelId: string; txHash?: string }>;\n /**\n * Free FULL multi-chain wallet view (#299) — native coin + configured tokens\n * per chain — works on an UNSTARTED client (Solana/Mina addresses are derived\n * from the mnemonic on demand). `fallback` supplies Solana/Mina channel params\n * to read WHEN the client config has none, without touching settlement config.\n */\n getWalletBalances?(\n fallback?: WalletViewFallback\n ): Promise<WalletChainBalanceInfo[]>;\n}\n\n/**\n * Wallet-view-only Solana/Mina channel defaults (#299): RPC/GraphQL + token used\n * to READ the balance of a chain the identity has no configured channel for, so\n * `rig balance` shows all three chains (0 for a not-yet-on-chain account). These\n * NEVER enter the client's settlement config — they cannot influence chain\n * negotiation — so a preset default here is safe even for a chain the wallet\n * cannot yet settle on.\n */\nexport interface WalletViewFallback {\n solanaChannel?: ToonClientConfig['solanaChannel'];\n minaChannel?: ToonClientConfig['minaChannel'];\n}\n\n/**\n * The client + channel one leg of the write path pays on. Two legs exist\n * only when the store route terminates on a different node than publishes;\n * otherwise both names point at the same client and channel.\n */\ninterface StoreLeg {\n client: ToonClientLike;\n channelId: string;\n}\n\nexport interface StandalonePublisherOptions {\n /**\n * ToonClient config (mnemonic + mnemonicAccountIndex + proxy/BTP uplink +\n * settlement fields — see `packages/client/src/config.ts`). Exactly one of\n * `clientConfig` | `client` is required.\n */\n clientConfig?: ToonClientConfig;\n /** Pre-built client (tests / advanced callers). */\n client?: ToonClientLike;\n /**\n * ILP route for event publishes (relay `/write`). Default: derived from the\n * config's `destinationAddress` anchor (`<base>.relay.store` → `<base>.relay`,\n * matching the daemon's route derivation), else the anchor itself.\n */\n publishDestination?: string;\n /**\n * ILP route for git-object uploads (store `/store` → Arweave). Default:\n * derived like `publishDestination` (`<base>.relay.store` → `<base>.store`).\n */\n storeDestination?: string;\n /**\n * ILP destination the payment channel anchors to. Default: the client\n * config's `destinationAddress`.\n */\n channelDestination?: string;\n /**\n * Client config for a SECOND uplink dedicated to store writes, when the\n * store route terminates on a different node than publishes.\n *\n * A balance-proof claim is only verifiable by the connector holding that\n * channel — one with no record of it refuses the packet (`F01 - claim\n * rejected: names a channel this connector has no record of`). The store\n * node has its own settlement address and its own channel, and on the\n * two-box fleet the publish node has no route to it, so a store write\n * cannot ride the publish session. Given this, store writes go out on\n * their own client, against their own channel, recorded under their own\n * anchor so `rig channel` still sees it.\n *\n * A separate CLIENT rather than a second destination on the existing one\n * because `@toon-protocol/client` carries a single `btpUrl` per instance —\n * there is no per-destination uplink to configure. Omitted (the\n * single-node case) keeps the historical behaviour: one client, one\n * channel, store writes on the publish session.\n */\n storeClientConfig?: ToonClientConfig;\n /**\n * Pre-built store-leg client (tests / advanced callers) — the store-side\n * counterpart of `client`, and mutually exclusive with\n * `storeClientConfig` in the same way.\n */\n storeClient?: ToonClientLike;\n /** Flat fee per published event (daemon `feePerEvent` convention). Default 1n. */\n eventFee?: bigint;\n /**\n * FLAT per-packet route prices for the publish/store destinations, as the\n * payment peer announces them (kind:10032 `capabilities` — e.g.\n * `{capability:'os.publish', address:'g.proxy.relay', price:'1000'}`) or as\n * the terminating connector reports them from `GET /ilp/routes/price`.\n *\n * The connector gates every paid packet at the destination route's price: a\n * balance-proof claim advancing the channel by less is rejected (F06). Since\n * ADR 0020 (toon-client#452) a price is flat per handler, so `store` is the\n * WHOLE upload fee rather than a floor under a per-byte computation — an\n * upload of any size costs it. `publish` still floors {@link eventFee},\n * which remains a caller-chosen flat figure.\n *\n * A destination without a known price prices its packets at 0 here and lets\n * the connector be the one to refuse — this class never invents a rate.\n */\n routePrices?: { publish?: bigint; store?: bigint };\n /** Daemon control API port probed by the nonce guard. */\n daemonPort?: number;\n /**\n * Skip Guard 1 (the same-identity daemon refusal, {@link checkDaemonIdentity})\n * — the force-standalone override ({@link standaloneForced} / RIG_STANDALONE\n * / `rig --standalone`). Guard 2, the exclusive per-identity {@link NonceLock},\n * ALWAYS runs regardless, so two standalone processes still can't race the\n * channel watermark. Default false (daemon-aware).\n */\n skipDaemonCheck?: boolean;\n /** Directory for the per-identity advisory lockfile. */\n lockDir?: string;\n /** Fetch impl for the daemon probe (tests). */\n fetchImpl?: typeof fetch;\n /**\n * Peer→channel map store (#262): start() resumes the channel recorded for\n * (identity, channel anchor) instead of opening a fresh one, and records\n * any fresh lazy open for the next invocation. Absent (embedded callers\n * managing their own channel lifecycle): the historical behaviour — open\n * lazily every run, record nothing.\n */\n channelMap?: ChannelMapStore;\n /**\n * Per-chain settlement parameters to BACK-FILL into the client's peer\n * negotiations after start (#264/#260): peers whose kind:10032 announce\n * carries no `tokenNetworks`/`preferredTokens` negotiate with those fields\n * empty, and the on-chain channel open then fails (\"tokenNetwork address\n * is required\"). Values here never override what a peer DID announce —\n * they only fill gaps, keyed by the negotiated chain id.\n */\n negotiationFallbacks?: {\n tokenNetworks?: Record<string, string>;\n preferredTokens?: Record<string, string>;\n };\n /** Sink for non-fatal channel-persistence warnings (default: stderr). */\n warn?: (line: string) => void;\n}\n\n/** A relay/store rejected a paid write (fee NOT spent iff the claim failed too). */\nexport class StandalonePublishError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'StandalonePublishError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Route derivation (duplicated from client-mcp daemon/config.ts — that\n// package depends on this one for #227, so importing it back would be\n// circular; keep in sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Derive publish/store routes from the channel anchor. Behind the devnet\n * proxy the anchor is `<base>.relay.store` (e.g. `g.proxy.relay.store`):\n * publishes terminate at `<base>.relay`, uploads at `<base>.store`. Anchors\n * not matching the convention pass through unchanged.\n */\nexport function deriveRouteDestinations(anchor: string): {\n publish: string;\n store: string;\n} {\n const segs = anchor.split('.');\n if (segs.at(-1) === 'store' && segs.at(-2) === 'relay') {\n const base = segs.slice(0, -2).join('.');\n return { publish: `${base}.relay`, store: `${base}.store` };\n }\n return { publish: anchor, store: anchor };\n}\n\n// ---------------------------------------------------------------------------\n// FULFILL → Arweave txId\n// ---------------------------------------------------------------------------\n\n/**\n * Decode the Arweave txId from a store-write answer.\n *\n * Re-exported from `@toon-protocol/client` rather than reimplemented: this\n * file used to carry its own copy because the client's extractor was thought\n * not to be exported. It is (and always was), and now that the answer is a\n * sealed response envelope rather than HTTP text there is exactly one shape\n * to read — a second reader of it could only drift.\n */\nexport { extractArweaveTxId };\n\n// ---------------------------------------------------------------------------\n// Channel-resume introspection (#262)\n//\n// The peer negotiation table and the ChannelManager's peer→channel map are\n// PRIVATE on ToonClient, so resuming a channel reaches into them through a\n// structural runtime cast — the exact pattern the daemon uses\n// (client-mcp/src/daemon/client-runner.ts, openOrResumeApexChannel /\n// routeChildPeersThroughApexChannel). Keep the shapes in sync with\n// @toon-protocol/client's ToonClient/ChannelManager.\n// ---------------------------------------------------------------------------\n\n/** The slice of a `PeerNegotiation` the channel map records. */\ninterface NegotiationLike {\n chain: string;\n chainType: string;\n chainId: number | string;\n settlementAddress: string;\n tokenAddress?: string;\n tokenNetwork?: string;\n}\n\ninterface ChannelInternals {\n peerNegotiations?: Map<string, NegotiationLike>;\n channelManager?: {\n trackChannel?: (\n channelId: string,\n context: PersistedChannelContext\n ) => void;\n peerChannels?: Map<string, string>;\n };\n /**\n * The client's on-chain channel client caches per-channel chain context at\n * OPEN time only; close/settle/deposit on a channel resumed from the map\n * would throw \"No on-chain context\" without re-seeding it (#263). Same\n * structural-cast contract as the rest of this block — keep in sync with\n * `@toon-protocol/client`'s OnChainChannelClient.\n */\n onChainChannelClient?: {\n channelContext?: Map<\n string,\n { chain: string; tokenNetworkAddress: string; tokenAddress?: string }\n >;\n };\n}\n\n/**\n * The key `ChannelManager` binds a peer's channel under —\n * `<peerId>|<chain>|<tokenNetwork>` (toon-client#489: the same peer on a\n * second chain, or a redeployed token network, is a DIFFERENT channel).\n * `peerChannels` used to be keyed by the bare peer id, so rig both wrote\n * resumed channels under a key `ensureChannel` never looks up (a fresh\n * on-chain channel per invocation) and read the composite key back as if it\n * were a peer id (so `peerNegotiations.get(...)` missed and nothing was ever\n * recorded). Keep in sync with `@toon-protocol/client`'s\n * `ChannelManager.bindingKey`.\n */\nfunction channelBindingKey(\n peerId: string,\n chain: string,\n tokenNetwork: string\n): string {\n return `${peerId}|${chain}|${tokenNetwork}`;\n}\n\n/**\n * The peer id inside a {@link channelBindingKey}. A pre-#489 bare-peer-id key\n * has no separator and passes through unchanged, so this reads either shape.\n */\nfunction peerIdFromBindingKey(key: string): string {\n return key.split('|')[0] ?? key;\n}\n\n/** Best-effort access to the client's private negotiation/channel state. */\nfunction channelInternals(client: ToonClientLike): ChannelInternals {\n const c = client as unknown as ChannelInternals;\n const negotiations =\n c.peerNegotiations instanceof Map ? c.peerNegotiations : undefined;\n const cm =\n c.channelManager &&\n typeof c.channelManager.trackChannel === 'function' &&\n c.channelManager.peerChannels instanceof Map\n ? c.channelManager\n : undefined;\n const onChain =\n c.onChainChannelClient &&\n c.onChainChannelClient.channelContext instanceof Map\n ? c.onChainChannelClient\n : undefined;\n return {\n ...(negotiations ? { peerNegotiations: negotiations } : {}),\n ...(cm ? { channelManager: cm } : {}),\n ...(onChain ? { onChainChannelClient: onChain } : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// StandalonePublisher\n// ---------------------------------------------------------------------------\n\nexport class StandalonePublisher implements Publisher {\n private readonly client: ToonClientLike;\n private readonly ownsClient: boolean;\n private readonly publishDestination: string | undefined;\n private readonly storeDestination: string | undefined;\n private readonly channelDestination: string | undefined;\n private readonly eventFee: bigint;\n private readonly routePrices:\n | StandalonePublisherOptions['routePrices']\n | undefined;\n private readonly daemonPort: number | undefined;\n private readonly skipDaemonCheck: boolean;\n private readonly lockDir: string | undefined;\n private readonly fetchImpl: typeof fetch | undefined;\n private readonly channelMap: ChannelMapStore | undefined;\n /** ILP anchor the channel is keyed by in the map (peer/apex destination). */\n private readonly channelAnchor: string | undefined;\n private readonly negotiationFallbacks:\n | StandalonePublisherOptions['negotiationFallbacks']\n | undefined;\n private readonly warn: (line: string) => void;\n\n private readonly storeClientConfig: ToonClientConfig | undefined;\n private readonly injectedStoreClient: ToonClientLike | undefined;\n /** Lazily built on the first store write — see {@link storeLeg}. */\n private storeClient: ToonClientLike | undefined;\n private storeChannelId: string | undefined;\n private storeLegPromise: Promise<StoreLeg> | undefined;\n\n private lock: NonceLock | undefined;\n private channelId: string | undefined;\n private readyPromise: Promise<void> | undefined;\n /** Guard + client start only (no channel) — see {@link startClientOnly}. */\n private clientReadyPromise: Promise<void> | undefined;\n /** True when the last start() RESUMED the recorded channel (#263). */\n private lastOpenResumed = false;\n\n constructor(options: StandalonePublisherOptions) {\n if (options.client && options.clientConfig) {\n throw new Error(\n 'StandalonePublisher: provide either `clientConfig` or `client`, not both'\n );\n }\n if (options.client) {\n this.client = options.client;\n this.ownsClient = false;\n } else if (options.clientConfig) {\n this.client = new ToonClient(options.clientConfig);\n this.ownsClient = true;\n } else {\n throw new Error(\n 'StandalonePublisher: one of `clientConfig` (mnemonic-based ToonClient config) or `client` is required'\n );\n }\n\n // Routes: explicit option → derived from the channel anchor (same\n // `<base>.relay.store` convention the daemon resolves).\n const anchor =\n options.channelDestination ?? options.clientConfig?.destinationAddress;\n const routes = anchor ? deriveRouteDestinations(anchor) : undefined;\n this.publishDestination = options.publishDestination ?? routes?.publish;\n this.storeDestination = options.storeDestination ?? routes?.store;\n this.channelDestination = options.channelDestination;\n\n this.eventFee = options.eventFee ?? 1n;\n this.routePrices = options.routePrices;\n this.daemonPort = options.daemonPort;\n this.skipDaemonCheck = options.skipDaemonCheck ?? false;\n this.lockDir = options.lockDir;\n this.fetchImpl = options.fetchImpl;\n this.channelMap = options.channelMap;\n this.channelAnchor = anchor;\n if (options.storeClient && options.storeClientConfig) {\n throw new Error(\n 'StandalonePublisher: provide either `storeClientConfig` or `storeClient`, not both'\n );\n }\n this.storeClientConfig = options.storeClientConfig;\n this.injectedStoreClient = options.storeClient;\n this.negotiationFallbacks = options.negotiationFallbacks;\n this.warn = options.warn ?? ((line) => process.stderr.write(`${line}\\n`));\n }\n\n /** Hex Nostr pubkey of the embedded identity (available before start). */\n getPublicKey(): string {\n return this.client.getPublicKey();\n }\n\n /**\n * Run the nonce guard, start the embedded client, and open (or resume) the\n * payment channel. Called lazily by the first paid operation; safe to call\n * eagerly to fail fast. Idempotent.\n */\n start(): Promise<void> {\n this.readyPromise ??= (async () => {\n await this.startClientOnly();\n try {\n this.channelId = await this.openOrResumeChannel();\n } catch (err) {\n // Release the identity lock on a failed channel open (pre-#263\n // behaviour): nothing holds claims yet, so another process may go.\n this.lock?.release();\n this.lock = undefined;\n this.clientReadyPromise = undefined;\n throw err;\n }\n })().catch((err: unknown) => {\n // Let a later call retry (e.g. after the conflicting daemon stops).\n this.readyPromise = undefined;\n throw err;\n });\n return this.readyPromise;\n }\n\n /**\n * Run the nonce guard and start the embedded client WITHOUT touching the\n * payment channel (#263): `channel close`/`settle` operate on a RECORDED\n * channel and must never open a fresh one as a side effect of starting.\n * Idempotent; `start()` layers the channel open/resume on top of this.\n */\n startClientOnly(): Promise<void> {\n this.clientReadyPromise ??= this.doStartClient().catch((err: unknown) => {\n this.clientReadyPromise = undefined;\n throw err;\n });\n return this.clientReadyPromise;\n }\n\n private async doStartClient(): Promise<void> {\n const pubkey = this.client.getPublicKey();\n\n // Guard 1: refuse while a toon-clientd holds this identity — UNLESS the\n // force-standalone override is set (RIG_STANDALONE / `rig --standalone`),\n // in which case the caller has explicitly chosen to sign for itself and\n // ignore the daemon. Guard 2 (the NonceLock below) still runs either way.\n if (!this.skipDaemonCheck) {\n await checkDaemonIdentity(pubkey, {\n ...(this.daemonPort !== undefined ? { port: this.daemonPort } : {}),\n ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),\n });\n }\n\n // Guard 2: exclusive advisory lock against other standalone processes.\n this.lock = await NonceLock.acquire(pubkey, {\n ...(this.lockDir !== undefined ? { dir: this.lockDir } : {}),\n });\n\n try {\n if (this.client.isStarted?.() !== true) {\n await this.client.start();\n }\n // #264: back-fill negotiation gaps as soon as the client's bootstrap\n // negotiation exists — before any channel open (start()) or recorded-\n // channel operation reads them.\n this.applyNegotiationFallbacks();\n } catch (err) {\n this.lock.release();\n this.lock = undefined;\n throw err;\n }\n }\n\n /**\n * Back-fill negotiated peer metadata the announce did not carry\n * (#264/#260 root cause 3): after the client's bootstrap negotiation, any\n * peer negotiation missing `tokenNetwork`/`tokenAddress` gets the derived\n * per-chain fallback for its negotiated chain, BEFORE the lazy channel\n * open reads them. Announced values are never overridden.\n */\n private applyNegotiationFallbacks(): void {\n const fallbacks = this.negotiationFallbacks;\n if (!fallbacks) return;\n const negotiations = channelInternals(this.client).peerNegotiations;\n if (!negotiations) {\n this.warn(\n 'rig: settlement fallbacks configured but the client does not ' +\n 'expose negotiation internals — on-chain channel opening may fail ' +\n 'if the peer announced no TokenNetwork'\n );\n return;\n }\n for (const negotiation of negotiations.values()) {\n if (!negotiation.tokenNetwork) {\n const tokenNetwork = fallbacks.tokenNetworks?.[negotiation.chain];\n if (tokenNetwork) negotiation.tokenNetwork = tokenNetwork;\n }\n if (!negotiation.tokenAddress) {\n const tokenAddress = fallbacks.preferredTokens?.[negotiation.chain];\n if (tokenAddress) negotiation.tokenAddress = tokenAddress;\n }\n }\n }\n\n /**\n * Open the payment channel — or, when the channel map has a record for\n * (identity, anchor), RESUME the recorded on-chain channel (#262).\n *\n * Resume seeds the client's ChannelManager (`trackChannel` rehydrates the\n * cumulative-claim watermark from channels.json; `peerChannels` makes the\n * subsequent `openChannel` return the same id instead of opening on-chain).\n * A fresh open is RECORDED so the next invocation resumes it. A corrupt map\n * file throws BEFORE anything is opened — never a silent duplicate open.\n */\n private async openOrResumeChannel(): Promise<string> {\n return this.openOrResumeChannelOn(this.client, this.channelDestination, {\n anchor: this.channelAnchor,\n trackResumeState: true,\n });\n }\n\n /**\n * {@link openOrResumeChannel} for an arbitrary client + destination, so\n * the store leg gets the same resume-before-open guarantee (and the same\n * corruption check) as the publish leg rather than a second, weaker path.\n *\n * `anchor` defaults to the destination: a leg other than the publish one\n * is keyed in the channel map by the route it pays for.\n * `trackResumeState` is publish-only — {@link lastOpenResumed} feeds the\n * `resumed` field of this publisher's channel status, which describes the\n * publish channel and would be wrong to overwrite from a store open.\n */\n private async openOrResumeChannelOn(\n client: ToonClientLike,\n destination: string | undefined,\n opts: { anchor?: string; trackResumeState?: boolean } = {}\n ): Promise<string> {\n const map = this.channelMap;\n const trackResumeState = opts.trackResumeState ?? false;\n if (!map) {\n // No persistence configured: historical lazy open, nothing recorded.\n return client.openChannel(destination);\n }\n const anchorFor = opts.anchor ?? destination;\n if (!anchorFor) {\n this.warn(\n 'rig: no channel anchor destination configured — the peer→channel ' +\n 'mapping cannot be persisted, so this run may open a fresh channel'\n );\n return client.openChannel(destination);\n }\n\n const anchor = anchorFor;\n const identity = client.getPublicKey();\n // Corruption check happens HERE, before any on-chain open (throws).\n const candidates = map.listFor(identity, anchor);\n const internals = channelInternals(client);\n const resumed = await this.resumeRecordedChannel(\n map,\n candidates,\n internals,\n client\n );\n\n // Idempotent — returns the (resumed or existing) channel for the peer if\n // one is tracked, else opens lazily on-chain.\n const channelId = await client.openChannel(destination);\n\n if (resumed && channelId === resumed.channelId) {\n if (trackResumeState) this.lastOpenResumed = true;\n map.touch(recordKey(resumed));\n } else {\n if (trackResumeState) this.lastOpenResumed = false;\n this.recordOpenedChannel(\n map,\n internals,\n identity,\n anchor,\n channelId,\n client\n );\n }\n return channelId;\n }\n\n /**\n * Try to resume one recorded channel: the first candidate whose peer is\n * still negotiated on the SAME chain + tokenNetwork and whose watermark\n * does not show it closed/settled. Returns the resumed record, if any.\n */\n private async resumeRecordedChannel(\n map: ChannelMapStore,\n candidates: ChannelMapRecord[],\n internals: ChannelInternals,\n // The client this leg pays on. Deposit bookkeeping must read from the\n // SAME client that tracks the channel — reading the publish client for a\n // store channel reports \"is not being tracked\" and records a deposit of\n // undefined against a real, funded channel.\n client: ToonClientLike\n ): Promise<ChannelMapRecord | undefined> {\n if (candidates.length === 0) return undefined;\n const cm = internals.channelManager;\n if (!cm?.trackChannel || !cm.peerChannels) {\n this.warn(\n 'rig: a recorded channel exists but the client does not expose ' +\n 'channel internals to resume it — a fresh channel may be opened'\n );\n return undefined;\n }\n\n for (const record of candidates) {\n // The peer must still be negotiated on the recorded chain/tokenNetwork;\n // a rotated peer identity or re-negotiated settlement gets a fresh\n // channel (recorded under its own key) instead of stale claims.\n const negotiation = internals.peerNegotiations?.get(record.peerId);\n if (\n !negotiation ||\n negotiation.chain !== record.chain ||\n (negotiation.tokenNetwork ?? '') !== record.tokenNetwork\n ) {\n continue;\n }\n\n // Never resume a channel the withdraw flow already closed/settled.\n const watermark = map.readWatermark(record.channelId);\n if (\n watermark?.closedAt !== undefined ||\n watermark?.settledAt !== undefined\n ) {\n continue;\n }\n if (!watermark) {\n // Fresh channels are seeded at record time, so a missing entry means\n // the watermark store was lost. Resuming from nonce 0 fails SAFE: a\n // regressed cumulative claim is rejected by the peer (no double\n // spend), but warn so the failure is diagnosable.\n this.warn(\n `rig: resuming channel ${record.channelId} with no local claim ` +\n `watermark (${map.watermarkPath}) — if this channel was claimed ` +\n 'against before, the peer will reject the stale claims; remove ' +\n `its entry from ${map.mapPath} to open a fresh channel instead`\n );\n }\n\n // trackChannel rehydrates nonce/cumulative from the watermark store;\n // seeding peerChannels makes ensureChannel/openChannel reuse the id —\n // but ONLY under the composite binding key it looks up.\n cm.trackChannel(record.channelId, record.context);\n cm.peerChannels.set(\n channelBindingKey(record.peerId, record.chain, record.tokenNetwork),\n record.channelId\n );\n\n // Persisted channel state omits the on-chain deposit — re-read it so\n // fee/balance accounting is right (EVM only; mirrors the daemon).\n // #279 latency trim: skipped when the record already carries a\n // depositTotal — deposits only change through rig's own flows (open\n // --deposit / a later resume), which update the record; this is\n // display/accounting state, not the claim watermark, so a stale value\n // can never double-spend (the peer enforces the cumulative claim).\n if (\n record.context.chainType === 'evm' &&\n record.depositTotal === undefined &&\n client.rehydrateChannelDeposit\n ) {\n try {\n const deposit = await client.rehydrateChannelDeposit(\n record.channelId,\n {\n chain: record.chain,\n tokenNetworkAddress: record.context.tokenNetworkAddress,\n }\n );\n if (deposit !== undefined) {\n map.touch(recordKey(record), {\n depositTotal: deposit.toString(),\n });\n }\n } catch (err) {\n this.warn(\n `rig: deposit re-read for resumed channel ${record.channelId} ` +\n `failed: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n return record;\n }\n return undefined;\n }\n\n /** Record a freshly opened channel (+ seed its claim watermark at 0/0). */\n private recordOpenedChannel(\n map: ChannelMapStore,\n internals: ChannelInternals,\n identity: string,\n destination: string,\n channelId: string,\n // The client this leg pays on — the deposit must be read from the client\n // that actually tracks the channel, not from whichever one happens to be\n // the publish client.\n client: ToonClientLike\n ): void {\n // `peerChannels` is keyed by the COMPOSITE binding key, not the peer id\n // (toon-client#489) — read the peer id back out of it.\n let peerId: string | undefined;\n for (const [key, channel] of internals.channelManager?.peerChannels ??\n []) {\n if (channel === channelId) {\n peerId = peerIdFromBindingKey(key);\n break;\n }\n }\n const negotiation =\n peerId !== undefined\n ? internals.peerNegotiations?.get(peerId)\n : undefined;\n if (peerId === undefined || !negotiation) {\n this.warn(\n `rig: opened channel ${channelId} but could not record the ` +\n 'peer→channel mapping (client does not expose negotiation ' +\n 'internals) — the NEXT invocation may open another channel'\n );\n return;\n }\n\n let depositTotal: bigint | undefined;\n try {\n depositTotal = client.getChannelDepositTotal?.(channelId);\n } catch {\n // deposit unknown — recorded without it; a later resume re-reads it\n }\n\n map.record({\n channelId,\n peerId,\n identity,\n destination,\n chain: negotiation.chain,\n tokenNetwork: negotiation.tokenNetwork ?? '',\n // Context mirrors the daemon's persistApexChannel shape — exactly what\n // trackChannel needs on resume.\n context: {\n chainType: negotiation.chainType,\n chainId:\n typeof negotiation.chainId === 'number' ? negotiation.chainId : 0,\n tokenNetworkAddress: negotiation.tokenNetwork ?? '',\n ...(negotiation.tokenAddress\n ? { tokenAddress: negotiation.tokenAddress }\n : {}),\n recipient: negotiation.settlementAddress,\n },\n ...(depositTotal !== undefined && depositTotal > 0n\n ? { depositTotal: depositTotal.toString() }\n : {}),\n });\n // Seed nonce 0 / cumulative 0 so a later resume can tell \"never claimed\n // against\" apart from \"watermark lost\" (which only fails claim-rejected).\n map.seedWatermark(channelId);\n }\n\n /** Release the identity lock and stop the embedded client (if we own it). */\n async stop(): Promise<void> {\n this.lock?.release();\n this.lock = undefined;\n this.readyPromise = undefined;\n this.clientReadyPromise = undefined;\n this.channelId = undefined;\n // Never stop a client that was never started (`ToonClient.stop()` throws\n // INVALID_STATE) — e.g. after a free `rig balance` read (#263).\n // The store leg owns its client outright (it is only ever built here),\n // so it is stopped regardless of `ownsClient` — that flag describes the\n // caller-injectable publish client.\n if (this.storeClient) {\n const storeClient = this.storeClient;\n this.storeClient = undefined;\n this.storeLegPromise = undefined;\n this.storeChannelId = undefined;\n await storeClient.stop();\n }\n if (this.ownsClient && this.client.isStarted?.() !== false) {\n await this.client.stop();\n }\n }\n\n // ── Money lifecycle (#263) ──────────────────────────────────────────────────\n\n /**\n * Explicit `rig channel open`: the SAME resume-or-open path the lazy paid\n * writes use (guard → start → resume the recorded channel or open + record\n * a fresh one), surfaced with a receipt — plus an optional extra collateral\n * deposit on top of the open/resume.\n */\n async openChannelExplicit(opts?: {\n /** Extra collateral to deposit AFTER the open/resume (base units). */\n deposit?: bigint;\n }): Promise<ChannelOpenOutcome> {\n await this.start();\n const channelId = this.requireChannel();\n const identity = this.client.getPublicKey();\n const destination = this.channelAnchor ?? '';\n const record =\n this.channelMap && this.channelAnchor\n ? this.channelMap\n .listFor(identity, this.channelAnchor)\n .find((r) => r.channelId === channelId)\n : undefined;\n\n const outcome: ChannelOpenOutcome = {\n channelId,\n resumed: this.lastOpenResumed,\n destination,\n ...(record ? { chain: record.chain, peerId: record.peerId } : {}),\n ...(record?.depositTotal !== undefined\n ? { depositTotal: record.depositTotal }\n : {}),\n };\n\n if (opts?.deposit !== undefined && opts.deposit > 0n) {\n if (!this.client.depositToChannel) {\n throw new StandalonePublishError(\n 'this client build does not support channel deposits ' +\n '(depositToChannel is unavailable)'\n );\n }\n const deposited = await this.client.depositToChannel(\n channelId,\n opts.deposit\n );\n outcome.depositAdded = opts.deposit.toString();\n outcome.depositTotal = deposited.depositTotal;\n if (deposited.txHash) outcome.depositTxHash = deposited.txHash;\n if (record) {\n this.channelMap?.touch(recordKey(record), {\n depositTotal: deposited.depositTotal,\n });\n }\n }\n return outcome;\n }\n\n /**\n * Adopt a RECORDED channel into the running client so on-chain close/\n * settle/deposit can act on it: `trackChannel` rehydrates the claim\n * watermark + withdraw timers from channels.json, `peerChannels` binds the\n * peer, and the on-chain client's context cache is re-seeded (it only\n * learns context at open time — a resumed channel would otherwise throw\n * \"No on-chain context\").\n */\n private adoptRecordedChannel(record: ChannelMapRecord): void {\n const internals = channelInternals(this.client);\n const cm = internals.channelManager;\n if (cm?.trackChannel && cm.peerChannels) {\n cm.trackChannel(record.channelId, record.context);\n cm.peerChannels.set(\n channelBindingKey(record.peerId, record.chain, record.tokenNetwork),\n record.channelId\n );\n }\n const contextCache = internals.onChainChannelClient?.channelContext;\n if (contextCache && !contextCache.has(record.channelId)) {\n contextCache.set(record.channelId, {\n chain: record.chain,\n tokenNetworkAddress: record.context.tokenNetworkAddress,\n ...(record.context.tokenAddress\n ? { tokenAddress: record.context.tokenAddress }\n : {}),\n });\n }\n }\n\n /**\n * Close a recorded channel: starts the on-chain settlement challenge\n * window. The client persists `closedAt`/`settleableAt` into the claim\n * watermark store, which is exactly where `rig channel list`/`balance`\n * derive the closing/settleable/settled status from. Guard + client start,\n * but NEVER a channel open ({@link startClientOnly}).\n */\n async closeRecordedChannel(\n record: ChannelMapRecord\n ): Promise<ChannelCloseOutcome> {\n await this.startClientOnly();\n if (!this.client.closeChannel) {\n throw new StandalonePublishError(\n 'this client build does not support closing channels ' +\n '(closeChannel is unavailable)'\n );\n }\n this.adoptRecordedChannel(record);\n const result = await this.client.closeChannel(record.channelId);\n this.channelMap?.touch(recordKey(record));\n return result;\n }\n\n /**\n * Settle a recorded channel after its challenge window — releases the\n * remaining collateral. The client enforces the `now >= settleableAt` time\n * guard BEFORE spending gas (a too-early call throws its retryable\n * SettleTooEarlyError) and persists `settledAt` into the watermark store.\n */\n async settleRecordedChannel(\n record: ChannelMapRecord\n ): Promise<ChannelSettleOutcome> {\n await this.startClientOnly();\n if (!this.client.settleChannel) {\n throw new StandalonePublishError(\n 'this client build does not support settling channels ' +\n '(settleChannel is unavailable)'\n );\n }\n this.adoptRecordedChannel(record);\n const result = await this.client.settleChannel(record.channelId);\n this.channelMap?.touch(recordKey(record));\n return result;\n }\n\n /**\n * The full multi-chain wallet view (#299) for the embedded identity — native\n * coin + configured tokens (USDC) per chain — a FREE read on the UNSTARTED\n * client (no nonce guard, no uplink, no channel). The client derives the\n * Solana/Mina addresses from the mnemonic on demand, so ALL configured chains\n * appear even before a start. Best-effort per chain (an unreachable RPC yields\n * an `unreadable` chain, not a failure). `fallback` supplies Solana/Mina RPC\n * defaults (e.g. the network preset) for chains the config has no channel for.\n */\n async readWalletChainBalances(\n fallback?: WalletViewFallback\n ): Promise<WalletChainBalanceInfo[]> {\n if (!this.client.getWalletBalances) return [];\n return await this.client.getWalletBalances(fallback);\n }\n\n // ── Publisher ─────────────────────────────────────────────────────────────\n\n /**\n * Fee rates for `planPush` estimation, both flat per packet (ADR 0020),\n * with the announced route prices folded in so the ESTIMATE always equals\n * the CLAIMS: `eventFee` is returned pre-floored at the publish route price,\n * and `uploadFee` IS the store route price.\n */\n getFeeRates(): Promise<FeeRates> {\n return Promise.resolve({\n uploadFee: this.uploadFee(),\n eventFee: this.effectiveEventFee(),\n });\n }\n\n /**\n * The flat per-event claim: `eventFee`, floored at the publish route's\n * announced price (the connector rejects a claim below it — F06).\n */\n private effectiveEventFee(): bigint {\n const min = this.routePrices?.publish ?? 0n;\n return this.eventFee > min ? this.eventFee : min;\n }\n\n /**\n * The per-upload claim: the store route's flat price, whatever the object's\n * size. Byte-proportional pricing has no successor (ADR 0020) — the route\n * table is the price list — and the connector charges this figure whether\n * the body is 100 bytes or 100 KB.\n */\n private uploadFee(): bigint {\n return this.routePrices?.store ?? 0n;\n }\n\n /**\n * Upload one git object as a kind:5094 store write (Git-SHA/Git-Type/Repo\n * tagged — the proven seed-pipeline shape), signing one balance-proof claim\n * for the store route's flat price\n * ({@link StandalonePublisherOptions.routePrices}) — ADR 0020 prices a\n * packet per handler, not per byte.\n *\n * #368: a blob's `Content-Type` is derived from its path extension (else\n * octet-stream) and sent in the `output` tag, which the store forwards onto\n * the Arweave upload's Content-Type — so a gateway serves `index.html` as\n * `text/html`, not `application/octet-stream`. Non-blob objects (trees,\n * commits, tags) are never served as files, so they stay octet-stream.\n */\n async uploadGitObject(upload: GitObjectUpload): Promise<UploadReceipt> {\n if (upload.body.length > MAX_OBJECT_SIZE) {\n throw new StandalonePublishError(\n `git object ${upload.sha} exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`\n );\n }\n await this.start();\n const leg = await this.storeLeg();\n\n const contentType =\n upload.type === 'blob'\n ? contentTypeForPath(upload.path)\n : DEFAULT_CONTENT_TYPE;\n\n const fee = this.uploadFee();\n const event = leg.client.signEvent({\n kind: 5094,\n content: '',\n created_at: nowSeconds(),\n tags: [\n ['i', upload.body.toString('base64'), 'blob'],\n ['bid', fee.toString(), 'usdc'],\n ['output', contentType],\n ['Git-SHA', upload.sha],\n ['Git-Type', upload.type],\n ['Repo', upload.repoId],\n ],\n });\n\n const claim = await leg.client.signBalanceProof(leg.channelId, fee);\n const result = await leg.client.publishEvent(event, {\n ...(this.storeDestination ? { destination: this.storeDestination } : {}),\n claim,\n ilpAmount: fee,\n // The store backend serves POST /store (not the relay's /write).\n proxyPath: '/store',\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `git-object upload rejected (${upload.sha}): ${result.error ?? 'store rejected the write'}`\n );\n }\n if (!result.response) {\n throw new StandalonePublishError(\n `git-object upload FULFILL carried no sealed response (${upload.sha}); expected the Arweave tx ID`\n );\n }\n return { txId: extractArweaveTxId(result.response), feePaid: fee };\n }\n\n /**\n * Upload one raw blob (no git envelope) with an explicit `Content-Type`\n * (#368) — a kind:5094 store write carrying just the `i`/`bid`/`output`\n * tags (and an optional `Repo` provenance tag), NOT the Git-SHA/Git-Type\n * tags. Without those the store keeps the bytes verbatim instead of\n * re-deriving a git envelope, which is exactly what the ar.io path manifest\n * needs. One balance-proof claim for the store route's flat price.\n */\n async uploadBlob(upload: BlobUpload): Promise<UploadReceipt> {\n if (upload.body.length > MAX_OBJECT_SIZE) {\n throw new StandalonePublishError(\n `blob exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`\n );\n }\n await this.start();\n const leg = await this.storeLeg();\n\n const fee = this.uploadFee();\n const event = leg.client.signEvent({\n kind: 5094,\n content: '',\n created_at: nowSeconds(),\n tags: [\n ['i', upload.body.toString('base64'), 'blob'],\n ['bid', fee.toString(), 'usdc'],\n ['output', upload.contentType],\n ...(upload.repoId ? [['Repo', upload.repoId]] : []),\n ],\n });\n\n const claim = await leg.client.signBalanceProof(leg.channelId, fee);\n const result = await leg.client.publishEvent(event, {\n ...(this.storeDestination ? { destination: this.storeDestination } : {}),\n claim,\n ilpAmount: fee,\n proxyPath: '/store',\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `blob upload rejected: ${result.error ?? 'store rejected the write'}`\n );\n }\n if (!result.response) {\n throw new StandalonePublishError(\n 'blob upload FULFILL carried no sealed response; expected the Arweave tx ID'\n );\n }\n return { txId: extractArweaveTxId(result.response), feePaid: fee };\n }\n\n /**\n * Sign the event with the embedded identity and pay-to-publish it through\n * the relay write route, one claim for the flat per-event fee.\n *\n * `relayUrls` is the interface's plural forward-compat surface (parked\n * #84): the standalone impl routes over ILP to its single configured\n * publish destination, so more than one relay is refused rather than\n * silently half-published.\n */\n async publishEvent(\n event: UnsignedEvent,\n relayUrls: string[]\n ): Promise<PublishReceipt> {\n if (relayUrls.length > 1) {\n throw new StandalonePublishError(\n `multi-relay publish is not supported yet (got ${relayUrls.length} relays) — the standalone publisher routes to a single relay destination (#84 parked)`\n );\n }\n await this.start();\n const channelId = this.requireChannel();\n\n const signed = this.client.signEvent(event);\n const fee = this.effectiveEventFee();\n const claim = await this.client.signBalanceProof(channelId, fee);\n const result = await this.client.publishEvent(signed, {\n ...(this.publishDestination\n ? { destination: this.publishDestination }\n : {}),\n claim,\n ilpAmount: fee,\n });\n if (!result.success) {\n throw new StandalonePublishError(\n `publish rejected (kind ${event.kind}): ${result.error ?? 'relay rejected the event'}`\n );\n }\n return { eventId: result.eventId ?? signed.id, feePaid: fee };\n }\n\n private requireChannel(): string {\n if (!this.channelId) {\n throw new StandalonePublishError(\n 'no payment channel open — start() did not complete'\n );\n }\n return this.channelId;\n }\n\n /**\n * The client + channel a STORE write must be paid on.\n *\n * With no `storeClientConfig` this is the publish leg unchanged — the\n * single-node case, where the store terminates on the connector that\n * already holds the channel. With one, the store node is a different\n * connector holding a different channel, and paying it from the publish\n * channel is precisely the `F01 - claim rejected: names a channel this\n * connector has no record of` failure this exists to prevent.\n *\n * Built on FIRST STORE WRITE, not in `start()`: `rig push` with nothing to\n * upload, and every read-only command, should not open a second on-chain\n * channel (gas) for a leg they never use. Memoised on the promise so\n * concurrent uploads share one open rather than racing two.\n */\n private async storeLeg(): Promise<StoreLeg> {\n const injected = this.injectedStoreClient;\n const config = this.storeClientConfig;\n if (!config && !injected) {\n return { client: this.client, channelId: this.requireChannel() };\n }\n this.storeLegPromise ??= (async (): Promise<StoreLeg> => {\n const client = injected ?? new ToonClient(config as ToonClientConfig);\n this.storeClient = client;\n await client.start();\n // The store node is a distinct peer: its channel is recorded under the\n // store destination as its own anchor, so `rig channel list/close/\n // settle` enumerate it alongside the publish channel instead of it\n // becoming invisible collateral.\n const channelId = await this.openOrResumeChannelOn(\n client,\n this.storeDestination\n );\n this.storeChannelId = channelId;\n return { client, channelId };\n })();\n return this.storeLegPromise;\n }\n}\n\nfunction nowSeconds(): number {\n return Math.floor(Date.now() / 1000);\n}\n"],"mappings":"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","names":[]}
@@ -189,10 +189,14 @@ function topologyCacheTtlMs(env) {
189
189
  const parsed = Number(raw);
190
190
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_TOPOLOGY_TTL_MS;
191
191
  }
192
+ var TOPOLOGY_SCHEMA_VERSION = 3;
192
193
  function topologyCacheKey(args) {
193
- return createHash("sha256").update(`${args.relayUrl}
194
+ return createHash("sha256").update(
195
+ `v${TOPOLOGY_SCHEMA_VERSION}
196
+ ${args.relayUrl}
194
197
  ${args.identity}
195
- ${args.fingerprint}`).digest("hex");
198
+ ` + args.fingerprint
199
+ ).digest("hex");
196
200
  }
197
201
  function explicitConfigFingerprint(env, file) {
198
202
  const envKeys = [
@@ -397,4 +401,4 @@ export {
397
401
  TopologyCache,
398
402
  MinaZkAppStore
399
403
  };
400
- //# sourceMappingURL=chunk-FFQXFV4W.js.map
404
+ //# sourceMappingURL=chunk-PWNZWQOH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/identity.ts","../src/standalone/topology-cache.ts","../src/standalone/mina-zkapp-store.ts"],"sourcesContent":["/**\n * Identity resolution for the standalone-only `rig` CLI (#248).\n *\n * One precedence chain, used by every command that signs or pays:\n *\n * 1. `RIG_MNEMONIC` environment variable\n * 2. `TOON_CLIENT_MNEMONIC` environment variable — DEPRECATED alias of the\n * env tier (a one-line stderr warning fires when it is the source)\n * 3. project-local `.env` — walked up from the working directory (through\n * the repo root to the filesystem root); ONLY the `RIG_MNEMONIC` line is\n * parsed out of it (never loaded into the process env, never required\n * to exist)\n * 4. the shared `~/.toon-client` state dir (`TOON_CLIENT_HOME` override):\n * encrypted keystore (`keystorePath` + `TOON_CLIENT_KEYSTORE_PASSWORD`,\n * auto-password fallback for daemon-provisioned keystores), then the\n * plain `mnemonic` config field — the pre-#248 standalone-context logic\n *\n * The BIP-44 account index follows the same env-over-config precedence:\n *\n * 1. `RIG_ACCOUNT_INDEX` environment variable (applies to every source)\n * 2. `mnemonicAccountIndex` from the shared config file — but ONLY when the\n * phrase itself came from that shared state dir (keystore/config tiers).\n * An env- or `.env`-sourced phrase derives at index 0 unless\n * `RIG_ACCOUNT_INDEX` says otherwise, so an explicit `RIG_MNEMONIC`\n * pins the same identity no matter which `TOON_CLIENT_HOME` is active\n * (#384 — a per-home config must never silently shift an explicitly\n * provided identity).\n * 3. account 0\n *\n * The phrase itself is never written anywhere by rig (not to git config, not\n * to any repo file) and never printed — callers report only the SOURCE and\n * the derived pubkey.\n *\n * Key derivation and keystore decryption live in the `@toon-protocol/client`\n * dependency, loaded via dynamic import so this module can be statically\n * imported by every command without dragging the client into runs that fail\n * earlier (usage errors, missing git repo, …).\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n/** Where the mnemonic came from (reported in output; the phrase never is). */\nexport type IdentitySourceKind =\n /** `RIG_MNEMONIC` environment variable. */\n | 'env'\n /** `TOON_CLIENT_MNEMONIC` environment variable (deprecated alias). */\n | 'env-alias'\n /** `RIG_MNEMONIC` parsed out of a project-local `.env` file. */\n | 'dotenv'\n /** Encrypted keystore referenced by the shared client config. */\n | 'keystore'\n /** Plain `mnemonic` field of the shared client config file. */\n | 'config';\n\n/** A resolved signing identity: source + derived pubkey (never the phrase). */\nexport interface ResolvedIdentity {\n /** The BIP-39 phrase. Handle with care; never log or persist it. */\n mnemonic: string;\n /** BIP-44 account index used for derivation — `RIG_ACCOUNT_INDEX` env,\n * else `mnemonicAccountIndex` from the shared config (home-sourced phrases\n * only), else 0. */\n accountIndex: number;\n source: IdentitySourceKind;\n /** Human-facing source label, e.g. `RIG_MNEMONIC env` or a file path. */\n sourceLabel: string;\n /** Derived Nostr pubkey (hex) — the repo owner / `toon.owner` value. */\n pubkey: string;\n}\n\n/** No mnemonic could be resolved anywhere on the chain. */\nexport class MissingIdentityError extends Error {\n constructor(configPath: string) {\n super(\n 'no identity found — rig needs a BIP-39 seed phrase to sign and pay.\\n' +\n 'Generate one now (no BIP-39 tooling needed):\\n' +\n ' • `rig identity create` — mint a fresh identity into the encrypted ' +\n 'keystore (shows the phrase once to back up)\\n' +\n 'Or point rig at an existing phrase (highest precedence first):\\n' +\n ' • RIG_MNEMONIC environment variable\\n' +\n ' • RIG_MNEMONIC=<phrase> in a project-local .env file (gitignore it!)\\n' +\n ` • the shared client config at ${configPath} (mnemonic or ` +\n 'keystorePath + TOON_CLIENT_KEYSTORE_PASSWORD)'\n );\n this.name = 'MissingIdentityError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared client config conventions (duplicated from client-mcp — the same\n// note as standalone/nonce-guard.ts: no @toon-protocol/client-mcp import)\n// ---------------------------------------------------------------------------\n\n/**\n * Duplicated daemon convention (client-mcp `daemon/config.ts`): the password\n * used to encrypt a keystore rig auto-provisions (`rig identity create`\n * with no `TOON_CLIENT_KEYSTORE_PASSWORD` set) so the identity reloads across\n * runs with no env var. At-rest obfuscation only.\n */\nexport const DEFAULT_KEYSTORE_PASSWORD = 'toon-client-default';\n\n/** Shared client state dir: `TOON_CLIENT_HOME`, else `~/.toon-client`. */\nexport function clientStateDir(env: NodeJS.ProcessEnv): string {\n return env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\n/** Shared client config file path (`<state-dir>/config.json`). */\nexport function clientConfigPath(env: NodeJS.ProcessEnv): string {\n return join(clientStateDir(env), 'config.json');\n}\n\n/** Default encrypted-keystore path (`<state-dir>/keystore.json`) — the same\n * convention the daemon's first-run provisioning uses. */\nexport function clientKeystorePath(env: NodeJS.ProcessEnv): string {\n return join(clientStateDir(env), 'keystore.json');\n}\n\n/** The subset of the shared client config file identity resolution reads. */\nexport interface ClientConfigIdentityFields {\n mnemonic?: unknown;\n mnemonicAccountIndex?: unknown;\n keystorePath?: unknown;\n keystoreAutoPassword?: unknown;\n}\n\nexport function readClientConfigFile(path: string): ClientConfigIdentityFields {\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as ClientConfigIdentityFields;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new Error(\n `failed to read client config at ${path}: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n}\n\n/**\n * Link a freshly written keystore into the shared client config so the\n * keystore tier of {@link resolveIdentity} (and the daemon) picks it up on the\n * next run — merging into any existing config, preserving every other field,\n * and writing owner-only (mode 0o600). `keystoreAutoPassword` records whether\n * rig chose the default password (so a user-set password still requires the\n * env var to decrypt, exactly like the daemon's first-run convention).\n */\nexport function linkKeystoreInClientConfig(\n env: NodeJS.ProcessEnv,\n keystorePath: string,\n autoPassword: boolean\n): string {\n const configPath = clientConfigPath(env);\n mkdirSync(dirname(configPath), { recursive: true });\n const existing = readClientConfigFile(configPath) as Record<string, unknown>;\n const merged: Record<string, unknown> = {\n ...existing,\n keystorePath,\n ...(autoPassword\n ? { keystoreAutoPassword: true }\n : // A user-set password: drop any stale auto flag so decryption\n // requires TOON_CLIENT_KEYSTORE_PASSWORD.\n { keystoreAutoPassword: false }),\n };\n writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\\n', {\n encoding: 'utf8',\n mode: 0o600,\n });\n return configPath;\n}\n\n// ---------------------------------------------------------------------------\n// .env parsing (RIG_MNEMONIC only — never arbitrary env loading)\n// ---------------------------------------------------------------------------\n\nconst DOTENV_LINE_RE = /^\\s*(?:export\\s+)?RIG_MNEMONIC\\s*=\\s*(.*)\\s*$/;\n\n/**\n * Extract ONLY the `RIG_MNEMONIC` value from `.env` file content. Supports\n * `export ` prefixes, single/double quotes, full-line `#` comments, and\n * inline ` #` comments on unquoted values. Multiple assignments: the last\n * one wins (shell-sourcing semantics). An empty value counts as unset.\n * Nothing is ever loaded into `process.env`.\n */\nexport function parseDotenvMnemonic(content: string): string | undefined {\n let found: string | undefined;\n for (const line of content.split(/\\r?\\n/)) {\n const match = DOTENV_LINE_RE.exec(line);\n if (!match) continue;\n let value = (match[1] ?? '').trim();\n const quote = value[0];\n if ((quote === '\"' || quote === \"'\") && value.length >= 2 && value.endsWith(quote)) {\n value = value.slice(1, -1);\n } else {\n // Unquoted: an inline comment starts at the first whitespace-preceded #.\n const hash = value.search(/\\s#/);\n if (hash !== -1) value = value.slice(0, hash);\n value = value.trim();\n }\n if (value !== '') found = value;\n }\n return found;\n}\n\n/**\n * Walk up from `startDir` (the working directory — the walk passes through\n * the repo root) to the filesystem root, returning the first `.env` file\n * that defines `RIG_MNEMONIC`. `stopDir` bounds the walk (tests).\n */\nexport function findDotenvMnemonic(\n startDir: string,\n stopDir?: string\n): { path: string; mnemonic: string } | undefined {\n let dir = startDir;\n for (;;) {\n const candidate = join(dir, '.env');\n if (existsSync(candidate)) {\n try {\n const mnemonic = parseDotenvMnemonic(readFileSync(candidate, 'utf8'));\n if (mnemonic !== undefined) return { path: candidate, mnemonic };\n } catch {\n // Unreadable .env → keep walking; the file is never required.\n }\n }\n if (stopDir !== undefined && dir === stopDir) return undefined;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Account index (env > per-home config for home-sourced phrases > 0)\n// ---------------------------------------------------------------------------\n\n/** Sources whose phrase lives OUTSIDE the shared state dir. For these the\n * per-home `mnemonicAccountIndex` must not apply — an explicit\n * `RIG_MNEMONIC` derives the same identity across every\n * `TOON_CLIENT_HOME` (#384). */\nconst HOME_INDEPENDENT_SOURCES: ReadonlySet<IdentitySourceKind> = new Set([\n 'env',\n 'env-alias',\n 'dotenv',\n]);\n\n/**\n * Resolve the BIP-44 account index for a phrase from `source`:\n * `RIG_ACCOUNT_INDEX` env always wins; the shared config's\n * `mnemonicAccountIndex` applies only when the phrase itself came from the\n * shared state dir (keystore/config tiers); default 0. Throws on a\n * malformed `RIG_ACCOUNT_INDEX` — silently deriving a different identity\n * is exactly the surprise this precedence exists to prevent.\n */\nexport function resolveAccountIndex(\n env: NodeJS.ProcessEnv,\n source: IdentitySourceKind,\n file: ClientConfigIdentityFields\n): number {\n const raw = env['RIG_ACCOUNT_INDEX']?.trim();\n if (raw) {\n const parsed = Number(raw);\n if (!Number.isInteger(parsed) || parsed < 0) {\n throw new Error(\n `RIG_ACCOUNT_INDEX must be a non-negative integer, got ${JSON.stringify(raw)}`\n );\n }\n return parsed;\n }\n if (HOME_INDEPENDENT_SOURCES.has(source)) return 0;\n return typeof file.mnemonicAccountIndex === 'number' ? file.mnemonicAccountIndex : 0;\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport interface ResolveIdentityOptions {\n env: NodeJS.ProcessEnv;\n /** Working directory the `.env` walk starts from. */\n cwd: string;\n /** Stderr line sink for the deprecated-alias warning. */\n warn(line: string): void;\n /** Bound the `.env` walk-up (tests only). */\n dotenvStopDir?: string;\n}\n\n/** The resolved source before any derivation (dependency-free). */\ninterface MnemonicSource {\n mnemonic: string;\n source: IdentitySourceKind;\n sourceLabel: string;\n}\n\nfunction resolveMnemonicSource(\n options: ResolveIdentityOptions,\n file: ClientConfigIdentityFields,\n configPath: string\n): MnemonicSource | { keystorePath: string } {\n const { env } = options;\n\n const rigEnv = env['RIG_MNEMONIC']?.trim();\n if (rigEnv) {\n return { mnemonic: rigEnv, source: 'env', sourceLabel: 'RIG_MNEMONIC env' };\n }\n\n const aliasEnv = env['TOON_CLIENT_MNEMONIC']?.trim();\n if (aliasEnv) {\n options.warn(\n 'rig: TOON_CLIENT_MNEMONIC is deprecated — rename the variable to RIG_MNEMONIC'\n );\n return {\n mnemonic: aliasEnv,\n source: 'env-alias',\n sourceLabel: 'TOON_CLIENT_MNEMONIC env (deprecated alias)',\n };\n }\n\n const dotenv = findDotenvMnemonic(options.cwd, options.dotenvStopDir);\n if (dotenv) {\n return {\n mnemonic: dotenv.mnemonic,\n source: 'dotenv',\n sourceLabel: dotenv.path,\n };\n }\n\n if (typeof file.keystorePath === 'string' && file.keystorePath !== '') {\n return { keystorePath: file.keystorePath };\n }\n if (typeof file.mnemonic === 'string' && file.mnemonic.trim() !== '') {\n return {\n mnemonic: file.mnemonic.trim(),\n source: 'config',\n sourceLabel: configPath,\n };\n }\n throw new MissingIdentityError(configPath);\n}\n\n/**\n * Load the client key helpers. `@toon-protocol/client` is a regular runtime\n * dependency (#259); the import stays dynamic purely so commands that fail\n * earlier never pay its startup cost.\n */\nasync function loadClientKeys(): Promise<{\n loadKeystore(path: string, password: string): string;\n deriveNostrKeyFromMnemonic(\n mnemonic: string,\n accountIndex?: number\n ): { secretKey: Uint8Array; pubkey: string };\n}> {\n return await import('@toon-protocol/client');\n}\n\n/**\n * Resolve the CLI signing identity along the precedence chain and derive its\n * Nostr pubkey. Throws {@link MissingIdentityError} (with the full\n * three-option remediation) when nothing on the chain yields a phrase.\n */\nexport async function resolveIdentity(\n options: ResolveIdentityOptions\n): Promise<ResolvedIdentity> {\n const configPath = clientConfigPath(options.env);\n const file = readClientConfigFile(configPath);\n const picked = resolveMnemonicSource(options, file, configPath);\n\n const keys = await loadClientKeys();\n let source: MnemonicSource;\n if ('keystorePath' in picked) {\n const password =\n options.env['TOON_CLIENT_KEYSTORE_PASSWORD'] ??\n (file.keystoreAutoPassword === true ? DEFAULT_KEYSTORE_PASSWORD : undefined);\n if (!password) {\n throw new Error(\n `keystorePath is set in ${configPath} but TOON_CLIENT_KEYSTORE_PASSWORD is not provided`\n );\n }\n source = {\n mnemonic: keys.loadKeystore(picked.keystorePath, password),\n source: 'keystore',\n sourceLabel: picked.keystorePath,\n };\n } else {\n source = picked;\n }\n\n const accountIndex = resolveAccountIndex(options.env, source.source, file);\n const { pubkey } = keys.deriveNostrKeyFromMnemonic(source.mnemonic, accountIndex);\n return { ...source, accountIndex, pubkey };\n}\n","/**\n * Standalone network-topology cache (#279): persist the #264 bootstrap\n * result — the announce-derived uplink, ILP routes, settlement chain and its\n * TokenNetwork/token/RPC parameters (or the genesis-seed fallback) — so\n * back-to-back CLI invocations skip the kind:10032 relay query, the\n * payment-peer pick, and the funded-chain `eth_call` probes.\n *\n * SAFETY MODEL — the cache holds only DISCOVERY results, never money state:\n *\n * - The claim watermark (nonce/cumulative) lives in the client's\n * `channels.json` and the #262 channel map, untouched by this cache;\n * every paid write still resumes from the persisted cumulative.\n * - Entries are keyed by relay-origin + identity pubkey + a fingerprint of\n * every explicit env/config field that feeds topology resolution, so a\n * changed relay, identity, or override can never hit a stale entry.\n * - Entries expire after a TTL (default 15 min;\n * `RIG_TOPOLOGY_TTL_MS` overrides, `0` disables the cache entirely).\n * - A cached topology that fails to bootstrap is EXPLICITLY invalidated\n * and re-resolved live (see `cli/standalone-mode.ts`), so a rotated peer\n * endpoint costs one failed attempt, not a broken 15 minutes.\n * - A corrupt/unreadable cache file is a MISS (discovery re-runs), never\n * an error: unlike the #262 channel map, nothing here guards money.\n *\n * Dependency-light on purpose (node:crypto + node:fs): the cached value is a\n * generic JSON document; `cli/standalone-mode.ts` instantiates the class\n * with its `NetworkTopology` shape and a structural validator.\n */\n\nimport { createHash } from 'node:crypto';\nimport { mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\n/** Cache filename under the shared client state dir (`TOON_CLIENT_HOME`). */\nexport const TOPOLOGY_CACHE_FILENAME = 'rig-topology-cache.json';\n\n/** Default entry TTL: 15 minutes. */\nexport const DEFAULT_TOPOLOGY_TTL_MS = 15 * 60 * 1000;\n\n/** TTL env override (milliseconds; `0` disables caching entirely). */\nexport const TOPOLOGY_TTL_ENV = 'RIG_TOPOLOGY_TTL_MS';\n\n/** Resolve the entry TTL: `RIG_TOPOLOGY_TTL_MS` env (>= 0), else 15 min. */\nexport function topologyCacheTtlMs(env: NodeJS.ProcessEnv): number {\n const raw = env[TOPOLOGY_TTL_ENV];\n if (raw === undefined || raw === '') return DEFAULT_TOPOLOGY_TTL_MS;\n const parsed = Number(raw);\n return Number.isFinite(parsed) && parsed >= 0\n ? parsed\n : DEFAULT_TOPOLOGY_TTL_MS;\n}\n\n/**\n * Schema tag mixed into every cache key. A rig release that changes WHAT the\n * resolution produces for the same inputs (not just its values) must bump\n * this: entries written by the previous release then miss instead of\n * shadowing the new behaviour for a whole TTL. Bumped to 2 when the default\n * resolution started placing a BTP uplink alongside the proxy — a cached\n * v1 topology has no `btpUrl`, so paid writes would have kept 401ing for 15\n * minutes after the upgrade.\n */\nexport const TOPOLOGY_SCHEMA_VERSION = 3;\n\n/**\n * Stable cache key: schema tag + relay-origin + identity pubkey + the\n * explicit-config fingerprint. Hashed so the key is filename/JSON-safe and\n * the mnemonic-side inputs never appear in the cache file verbatim.\n */\nexport function topologyCacheKey(args: {\n relayUrl: string;\n identity: string;\n fingerprint: string;\n}): string {\n return createHash('sha256')\n .update(\n `v${TOPOLOGY_SCHEMA_VERSION}\\n${args.relayUrl}\\n${args.identity}\\n` +\n args.fingerprint\n )\n .digest('hex');\n}\n\n/**\n * Canonical fingerprint of the explicit inputs that steer topology\n * resolution (see `resolveNetworkTopology`'s precedence order): the\n * TOON_CLIENT_* env overrides plus the shared-config fields. Any change\n * produces a different cache key — explicit config can never be shadowed by\n * a cached resolution made under different settings.\n */\nexport function explicitConfigFingerprint(\n env: NodeJS.ProcessEnv,\n file: Record<string, unknown>\n): string {\n const envKeys = [\n 'TOON_CLIENT_PROXY_URL',\n 'TOON_CLIENT_BTP_URL',\n 'TOON_CLIENT_DESTINATION',\n 'TOON_CLIENT_PUBLISH_DESTINATION',\n 'TOON_CLIENT_STORE_DESTINATION',\n 'TOON_CLIENT_CHAIN',\n 'TOON_CLIENT_NETWORK',\n ] as const;\n const fileKeys = [\n 'network',\n 'btpUrl',\n 'proxyUrl',\n 'destination',\n 'publishDestination',\n 'storeDestination',\n 'chain',\n 'supportedChains',\n 'settlementAddresses',\n 'preferredTokens',\n 'tokenNetworks',\n 'chainRpcUrls',\n ] as const;\n const picked: Record<string, unknown> = {};\n for (const key of envKeys) {\n if (env[key] !== undefined) picked[`env:${key}`] = env[key];\n }\n for (const key of fileKeys) {\n if (file[key] !== undefined) picked[`file:${key}`] = file[key];\n }\n // Keys are inserted in fixed order above → deterministic serialization.\n return JSON.stringify(picked);\n}\n\ninterface CacheEntry {\n /** ISO timestamp the entry was written. */\n cachedAt: string;\n /** The cached (JSON-safe) topology document. */\n topology: unknown;\n}\n\ninterface CacheFile {\n version: 1;\n entries: Record<string, CacheEntry>;\n}\n\nexport interface TopologyCacheOptions<T> {\n /** Cache file path (under `TOON_CLIENT_HOME`). */\n path: string;\n /** Entry TTL in ms; `0` (or negative) disables reads AND writes. */\n ttlMs: number;\n /** Structural validator for cached values (a failed check is a miss). */\n validate: (value: unknown) => value is T;\n /** Clock override (tests). */\n now?: () => number;\n}\n\n/**\n * File-backed, TTL'd topology cache. All operations are best-effort and\n * synchronous; failures degrade to cache misses / no-ops (discovery is the\n * always-correct fallback).\n */\nexport class TopologyCache<T> {\n readonly path: string;\n readonly ttlMs: number;\n private readonly validate: (value: unknown) => value is T;\n private readonly now: () => number;\n\n constructor(options: TopologyCacheOptions<T>) {\n this.path = options.path;\n this.ttlMs = options.ttlMs;\n this.validate = options.validate;\n this.now = options.now ?? Date.now;\n }\n\n /** True when the cache is disabled (`RIG_TOPOLOGY_TTL_MS=0`). */\n get disabled(): boolean {\n return this.ttlMs <= 0;\n }\n\n /**\n * A fresh, structurally-valid entry for `key` — or undefined (expired,\n * missing, invalid, corrupt file, or disabled). Also reports the entry age\n * for the \"topology from cache\" stderr line.\n */\n read(key: string): { topology: T; ageMs: number } | undefined {\n if (this.disabled) return undefined;\n const entry = this.readFile().entries[key];\n if (!entry) return undefined;\n const cachedAt = Date.parse(entry.cachedAt);\n if (!Number.isFinite(cachedAt)) return undefined;\n const ageMs = this.now() - cachedAt;\n if (ageMs < 0 || ageMs > this.ttlMs) return undefined;\n if (!this.validate(entry.topology)) return undefined;\n return { topology: entry.topology, ageMs };\n }\n\n /** Persist `topology` under `key` (prunes expired entries; best-effort). */\n write(key: string, topology: T): void {\n if (this.disabled) return;\n try {\n const file = this.readFile();\n const nowMs = this.now();\n const fresh = Object.fromEntries(\n Object.entries(file.entries).filter(([, entry]) => {\n const at = Date.parse(entry.cachedAt);\n return Number.isFinite(at) && nowMs - at <= this.ttlMs;\n })\n );\n fresh[key] = {\n cachedAt: new Date(nowMs).toISOString(),\n topology,\n };\n mkdirSync(dirname(this.path), { recursive: true });\n writeFileSync(\n this.path,\n JSON.stringify(\n { version: 1, entries: fresh } satisfies CacheFile,\n null,\n 2\n ),\n { mode: 0o600 }\n );\n } catch {\n // Best-effort — a failed write just means the next run discovers live.\n }\n }\n\n /** Drop `key` (a cached topology failed to bootstrap). Best-effort. */\n invalidate(key: string): void {\n try {\n const file = this.readFile();\n if (!(key in file.entries)) return;\n const remaining = Object.fromEntries(\n Object.entries(file.entries).filter(([k]) => k !== key)\n );\n if (Object.keys(remaining).length === 0) {\n unlinkSync(this.path);\n return;\n }\n writeFileSync(\n this.path,\n JSON.stringify(\n { version: 1, entries: remaining } satisfies CacheFile,\n null,\n 2\n ),\n { mode: 0o600 }\n );\n } catch {\n // Best-effort — worst case the entry expires by TTL.\n }\n }\n\n private readFile(): CacheFile {\n const empty: CacheFile = { version: 1, entries: {} };\n let raw: string;\n try {\n raw = readFileSync(this.path, 'utf8');\n } catch {\n return empty;\n }\n try {\n const parsed = JSON.parse(raw) as CacheFile;\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n parsed.version !== 1 ||\n typeof parsed.entries !== 'object' ||\n parsed.entries === null\n ) {\n return empty;\n }\n return parsed;\n } catch {\n return empty; // corrupt cache = miss, never an error (see module doc)\n }\n }\n}\n","/**\n * Persistence for rig's auto-deployed per-pair Mina `PaymentChannel` zkApps.\n *\n * The Mina zkApp is single-pair (one deployment per client↔connector pair)\n * and costs ~1.1 MINA to deploy, so the zkApp's PRIVATE KEY must survive the\n * process: losing it strands the deployment (and any future co-signed zkApp\n * tx). Records live in `<TOON_CLIENT_HOME>/keys/rig-mina-zkapps.json`\n * (mode 0600 — it holds private keys), keyed `identity|chain`, next to the\n * hand-managed `mina-zkapp-*-deploy.json` records operators already keep.\n *\n * A stale record self-heals: the open path's ownership check (on-chain\n * channelHash vs the current pair) simply stops matching and a fresh zkApp is\n * deployed over a new record.\n */\n\nimport {\n chmodSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\n\n/** One auto-deployed zkApp record (everything needed to reuse or audit it). */\nexport interface RigMinaZkAppRecord {\n /** The rig identity (nostr pubkey hex) that deployed it. */\n identity: string;\n /** The settlement chain the deployment serves (e.g. `mina:devnet`). */\n chain: string;\n /** Deployed zkApp B62 address (== the channel id). */\n zkAppAddress: string;\n /** The zkApp's `EK…` base58 private key. */\n zkAppPrivateKey: string;\n /** The fee payer (client Mina B62) that funded the deployment. */\n feePayer?: string;\n /** Deploy tx hash, when the send surfaced one. */\n deployTxHash?: string;\n /** Verification-key hash of the compiled contract that was deployed. */\n vkHash?: string;\n /** ISO timestamp of the deployment. */\n deployedAt: string;\n /** Provenance note (package/build the contract came from). */\n source?: string;\n}\n\nconst FILENAME = 'rig-mina-zkapps.json';\n\n/** `identity|chain` — one live deployment per identity per chain. */\nfunction keyFor(identity: string, chain: string): string {\n return `${identity}|${chain}`;\n}\n\nexport class MinaZkAppStore {\n constructor(readonly filePath: string) {}\n\n /** The store under a client home dir: `<dir>/keys/rig-mina-zkapps.json`. */\n static forHome(dir: string): MinaZkAppStore {\n return new MinaZkAppStore(join(dir, 'keys', FILENAME));\n }\n\n private readAll(): Record<string, RigMinaZkAppRecord> {\n try {\n return JSON.parse(readFileSync(this.filePath, 'utf8')) as Record<\n string,\n RigMinaZkAppRecord\n >;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new Error(\n `failed to read the Mina zkApp store at ${this.filePath}: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n\n /** The recorded deployment for this identity/chain, if any. */\n lookup(identity: string, chain: string): RigMinaZkAppRecord | undefined {\n return this.readAll()[keyFor(identity, chain)];\n }\n\n /** Read-merge-write; the file holds private keys → 0600. */\n save(record: RigMinaZkAppRecord): void {\n const all = this.readAll();\n all[keyFor(record.identity, record.chain)] = record;\n mkdirSync(dirname(this.filePath), { recursive: true });\n writeFileSync(this.filePath, `${JSON.stringify(all, null, 2)}\\n`, {\n encoding: 'utf8',\n mode: 0o600,\n });\n // writeFileSync's `mode` only applies on CREATE — re-tighten every save.\n chmodSync(this.filePath, 0o600);\n }\n}\n"],"mappings":"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","names":["mkdirSync","readFileSync","writeFileSync","dirname","mkdirSync","readFileSync","writeFileSync","dirname","join"]}
@@ -27,6 +27,26 @@ function parseRoutes(content) {
27
27
  return void 0;
28
28
  }
29
29
  }
30
+ function parseRoutePrices(content) {
31
+ try {
32
+ const parsed = JSON.parse(content);
33
+ const raw = parsed.routePrices;
34
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
35
+ return void 0;
36
+ }
37
+ const out = {};
38
+ for (const [address, price] of Object.entries(
39
+ raw
40
+ )) {
41
+ if (address.length > 0 && typeof price === "string" && /^\d+$/.test(price)) {
42
+ out[address] = price;
43
+ }
44
+ }
45
+ return Object.keys(out).length > 0 ? out : void 0;
46
+ } catch {
47
+ return void 0;
48
+ }
49
+ }
30
50
  function parseCapabilities(content) {
31
51
  try {
32
52
  const parsed = JSON.parse(content);
@@ -109,6 +129,7 @@ async function discoverAnnouncedPeers(relayUrl, options = {}) {
109
129
  }
110
130
  const routes = parseRoutes(event.content);
111
131
  const capabilities = parseCapabilities(event.content);
132
+ const routePrices = parseRoutePrices(event.content);
112
133
  const minaTokenIds = parseMinaTokenIds(event.content);
113
134
  const chainRpcUrls = parseChainRpcUrls(event.content);
114
135
  peers.push({
@@ -116,6 +137,7 @@ async function discoverAnnouncedPeers(relayUrl, options = {}) {
116
137
  info,
117
138
  ...routes ? { routes } : {},
118
139
  ...capabilities ? { capabilities } : {},
140
+ ...routePrices ? { routePrices } : {},
119
141
  ...minaTokenIds ? { minaTokenIds } : {},
120
142
  ...chainRpcUrls ? { chainRpcUrls } : {},
121
143
  createdAt: event.created_at
@@ -444,4 +466,4 @@ export {
444
466
  loadGenesisSeed,
445
467
  genesisSeedPubkeys
446
468
  };
447
- //# sourceMappingURL=chunk-TTJCBGKW.js.map
469
+ //# sourceMappingURL=chunk-UZZ2RN7A.js.map