@toon-protocol/rig 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/{chunk-RZLDFCKS.js → chunk-ACTH6ISJ.js} +82 -3
- package/dist/chunk-ACTH6ISJ.js.map +1 -0
- package/dist/{chunk-GZUDRNOK.js → chunk-FFQXFV4W.js} +213 -2
- package/dist/chunk-FFQXFV4W.js.map +1 -0
- package/dist/cli/rig.js +602 -54
- package/dist/cli/rig.js.map +1 -1
- package/dist/{network-bootstrap-IHQ4PWUW.js → network-bootstrap-3X2GXNJB.js} +6 -2
- package/dist/{standalone-mode-YMIY4GL3.js → standalone-mode-D2YSANBJ.js} +116 -166
- package/dist/standalone-mode-D2YSANBJ.js.map +1 -0
- package/package.json +3 -3
- package/dist/chunk-GZUDRNOK.js.map +0 -1
- package/dist/chunk-RZLDFCKS.js.map +0 -1
- package/dist/standalone-mode-YMIY4GL3.js.map +0 -1
- /package/dist/{network-bootstrap-IHQ4PWUW.js.map → network-bootstrap-3X2GXNJB.js.map} +0 -0
package/README.md
CHANGED
|
@@ -209,8 +209,10 @@ rig pr status <event-id> applied
|
|
|
209
209
|
| `rig site publish [ref]` | **paid** | deploy a pushed repo as a permaweb site: build the ar.io path manifest (repo paths → Arweave txids) and upload it as one paid store write; prints the gateway URL |
|
|
210
210
|
| `rig site url [ref]` | free | print the last-published site URL for a ref |
|
|
211
211
|
| `rig name status <name>` | free | an ArNS name's registry record, ANT process, and current target txId |
|
|
212
|
-
| `rig name buy <name>` / `rig name set <name> <txId>` | **paid¹** | buy an ArNS name / point it at an Arweave txId. ¹Paid in mARIO on Solana via the ar.io registry — **not** ILP; needs the optional `@ar.io/sdk` dep |
|
|
213
|
-
| `rig channel list/open/close/settle` | free / **paid** | inspect or manage the payment channels paid commands hold |
|
|
212
|
+
| `rig name buy <name>` / `rig name set <name> <txId>` | **paid¹** | buy an ArNS name / point it at an Arweave txId. On the devnet, buy/set default to brokering through the deployed store DVM (`--direct` opts out). ¹Paid in mARIO on Solana via the ar.io registry — **not** ILP; needs the optional `@ar.io/sdk` dep |
|
|
213
|
+
| `rig channel list/open/close/settle` | free / **paid** | inspect or manage the payment channels paid commands hold (`rig channels` = `rig channel list`) |
|
|
214
|
+
| `rig chain [set <c>\|unset]` | free | pin which chain/USDC settles paid writes: evm \| sol \| mina |
|
|
215
|
+
| `rig entry [apex\|sandbox\|url]` | free | choose the network entry node (payment ingress + relay); `sandbox` is the Mina-only multihop demo entry |
|
|
214
216
|
| `rig help` / `rig --version` | free | usage / version |
|
|
215
217
|
| everything else | — | executed as `git <args...>` with rig's stdio and git's exit code |
|
|
216
218
|
|
|
@@ -44,6 +44,42 @@ function parseCapabilities(content) {
|
|
|
44
44
|
return void 0;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
function parseMinaTokenIds(content) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(content);
|
|
50
|
+
const map = parsed.minaTokenIds;
|
|
51
|
+
if (typeof map !== "object" || map === null || Array.isArray(map)) {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const [chain, tokenId] of Object.entries(map)) {
|
|
56
|
+
if (typeof tokenId === "string" && /^\d+$/.test(tokenId)) {
|
|
57
|
+
out[chain] = tokenId;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
61
|
+
} catch {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function parseChainRpcUrls(content) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(content);
|
|
68
|
+
const map = parsed.chainRpcUrls;
|
|
69
|
+
if (typeof map !== "object" || map === null || Array.isArray(map)) {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const [chain, url] of Object.entries(map)) {
|
|
74
|
+
if (typeof url === "string" && /^(https?|wss?):\/\//.test(url)) {
|
|
75
|
+
out[chain] = url;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
79
|
+
} catch {
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
47
83
|
async function discoverAnnouncedPeers(relayUrl, options = {}) {
|
|
48
84
|
const factory = options.webSocketFactory ?? defaultDiscoveryWebSocketFactory();
|
|
49
85
|
const events = await queryRelay(
|
|
@@ -73,11 +109,15 @@ async function discoverAnnouncedPeers(relayUrl, options = {}) {
|
|
|
73
109
|
}
|
|
74
110
|
const routes = parseRoutes(event.content);
|
|
75
111
|
const capabilities = parseCapabilities(event.content);
|
|
112
|
+
const minaTokenIds = parseMinaTokenIds(event.content);
|
|
113
|
+
const chainRpcUrls = parseChainRpcUrls(event.content);
|
|
76
114
|
peers.push({
|
|
77
115
|
pubkey: event.pubkey,
|
|
78
116
|
info,
|
|
79
117
|
...routes ? { routes } : {},
|
|
80
118
|
...capabilities ? { capabilities } : {},
|
|
119
|
+
...minaTokenIds ? { minaTokenIds } : {},
|
|
120
|
+
...chainRpcUrls ? { chainRpcUrls } : {},
|
|
81
121
|
createdAt: event.created_at
|
|
82
122
|
});
|
|
83
123
|
}
|
|
@@ -146,21 +186,49 @@ function solanaPresetForChain(chain) {
|
|
|
146
186
|
...programId ? { programId } : {}
|
|
147
187
|
};
|
|
148
188
|
}
|
|
189
|
+
function minaPresetForChain(chain) {
|
|
190
|
+
const [family, network] = chain.split(":");
|
|
191
|
+
if (family !== "mina" || !network) return void 0;
|
|
192
|
+
const tier = network === "mainnet" ? "mainnet" : network === "devnet" ? "devnet" : void 0;
|
|
193
|
+
if (!tier) return void 0;
|
|
194
|
+
const presets = resolveClientNetwork(tier);
|
|
195
|
+
const mina = presets.minaChannel;
|
|
196
|
+
if (!mina) return void 0;
|
|
197
|
+
const tokenId = mina.tokenId;
|
|
198
|
+
return {
|
|
199
|
+
...mina.graphqlUrl ? { graphqlUrl: mina.graphqlUrl } : {},
|
|
200
|
+
...mina.zkAppAddress ? { zkAppAddress: mina.zkAppAddress } : {},
|
|
201
|
+
...tokenId ? { tokenId } : {},
|
|
202
|
+
...mina.networkId ? { networkId: mina.networkId } : {}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
149
205
|
function resolveChainSettlement(chain, explicit, announce) {
|
|
150
206
|
const family = chain.split(":")[0] ?? chain;
|
|
151
207
|
const evmPreset = family === "evm" ? evmPresetForChain(chain) : void 0;
|
|
152
208
|
const solPreset = family === "solana" ? solanaPresetForChain(chain) : void 0;
|
|
153
|
-
const
|
|
209
|
+
const minaPreset = family === "mina" ? minaPresetForChain(chain) : void 0;
|
|
210
|
+
const rpcUrl = explicit.chainRpcUrls?.[chain] ?? // The live deployment's advertised RPC beats the baked preset default: a
|
|
211
|
+
// preset RPC can be a stale/broken endpoint (e.g. Base Sepolia's
|
|
212
|
+
// sepolia.base.org fails setTotalDeposit) while the announce carries the
|
|
213
|
+
// deployment's known-good URL. Same drift lesson as #103.
|
|
214
|
+
announce?.chainRpcUrls?.[chain] ?? evmPreset?.rpcUrl ?? solPreset?.rpcUrl ?? // For Mina the "RPC" is the GraphQL endpoint (non-deployment-specific).
|
|
215
|
+
minaPreset?.graphqlUrl;
|
|
154
216
|
const tokenAddress = explicit.preferredTokens?.[chain] ?? announce?.info.preferredTokens?.[chain] ?? (evmPreset?.usdcAddress || void 0) ?? solPreset?.tokenMint;
|
|
155
217
|
const tokenNetwork = family === "evm" ? explicit.tokenNetworks?.[chain] ?? announce?.info.tokenNetworks?.[chain] ?? (evmPreset?.tokenNetworkAddress || void 0) : void 0;
|
|
156
218
|
const programId = family === "solana" ? explicit.tokenNetworks?.[chain] ?? announce?.info.tokenNetworks?.[chain] ?? solPreset?.programId : void 0;
|
|
219
|
+
const zkAppAddress = family === "mina" ? explicit.tokenNetworks?.[chain] ?? announce?.info.tokenNetworks?.[chain] ?? minaPreset?.zkAppAddress : void 0;
|
|
220
|
+
const minaTokenId = family === "mina" ? announce?.minaTokenIds?.[chain] ?? minaPreset?.tokenId : void 0;
|
|
221
|
+
const networkId = family === "mina" ? minaPreset?.networkId ?? (chain.split(":")[1] === "mainnet" ? "mainnet" : "devnet") : void 0;
|
|
157
222
|
return {
|
|
158
223
|
chain,
|
|
159
224
|
family,
|
|
160
225
|
...rpcUrl ? { rpcUrl } : {},
|
|
161
226
|
...tokenAddress ? { tokenAddress } : {},
|
|
162
227
|
...tokenNetwork ? { tokenNetwork } : {},
|
|
163
|
-
...programId ? { programId } : {}
|
|
228
|
+
...programId ? { programId } : {},
|
|
229
|
+
...zkAppAddress ? { zkAppAddress } : {},
|
|
230
|
+
...minaTokenId ? { minaTokenId } : {},
|
|
231
|
+
...networkId ? { networkId } : {}
|
|
164
232
|
};
|
|
165
233
|
}
|
|
166
234
|
var TokenNetworkUnderivableError = class extends Error {
|
|
@@ -181,6 +249,15 @@ var SolanaChannelUnderivableError = class extends Error {
|
|
|
181
249
|
this.name = "SolanaChannelUnderivableError";
|
|
182
250
|
}
|
|
183
251
|
};
|
|
252
|
+
var MinaChannelUnderivableError = class extends Error {
|
|
253
|
+
constructor(chain, missing, announce, relayUrl) {
|
|
254
|
+
const announceRef = announce ? `the kind:10032 announce from ${announce.pubkey.slice(0, 16)}\u2026 on ${relayUrl} does not carry them` : `no kind:10032 announce was found on ${relayUrl}`;
|
|
255
|
+
super(
|
|
256
|
+
`cannot derive the Mina channel parameters for settlement chain "${chain}" (missing: ${missing.join(", ")}): ${announceRef}, and no built-in preset covers this network \u2014 add a minaChannel { graphqlUrl, zkAppAddress, tokenId, networkId } object to the client config (or set tokenNetworks["${chain}"] = <zkApp address> and chainRpcUrls["${chain}"] = <graphql url>), or pick another chain via TOON_CLIENT_CHAIN / the chain config field`
|
|
257
|
+
);
|
|
258
|
+
this.name = "MinaChannelUnderivableError";
|
|
259
|
+
}
|
|
260
|
+
};
|
|
184
261
|
async function selectSettlementChain(options) {
|
|
185
262
|
const { explicitChain, announcedChains } = options;
|
|
186
263
|
if (explicitChain) {
|
|
@@ -356,13 +433,15 @@ export {
|
|
|
356
433
|
evmChainIdOf,
|
|
357
434
|
evmPresetForChain,
|
|
358
435
|
solanaPresetForChain,
|
|
436
|
+
minaPresetForChain,
|
|
359
437
|
resolveChainSettlement,
|
|
360
438
|
TokenNetworkUnderivableError,
|
|
361
439
|
SolanaChannelUnderivableError,
|
|
440
|
+
MinaChannelUnderivableError,
|
|
362
441
|
selectSettlementChain,
|
|
363
442
|
evmTokenBalance,
|
|
364
443
|
solanaTokenBalance,
|
|
365
444
|
loadGenesisSeed,
|
|
366
445
|
genesisSeedPubkeys
|
|
367
446
|
};
|
|
368
|
-
//# sourceMappingURL=chunk-
|
|
447
|
+
//# sourceMappingURL=chunk-ACTH6ISJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/standalone/network-bootstrap.ts"],"sourcesContent":["/**\n * Network bootstrap for the standalone (embedded-client) rig path (#264):\n * resolve the payment topology — uplink, ILP destinations/routes, settlement\n * chain and its on-chain parameters — from the network itself instead of\n * hand-fed constants.\n *\n * Sources, in strict precedence order (first hit wins, per field):\n *\n * 1. EXPLICIT USER CONFIG — env vars / shared client-config file fields.\n * 2. LIVE kind:10032 ANNOUNCE — the payment peer's `IlpPeerInfo` event\n * discovered on the relay-origin (the relay the paid command resolved\n * via `rig remote`, i.e. what the user pointed rig at). The announce\n * carries `btpEndpoint`/`httpEndpoint` (uplink), `ilpAddress` (channel\n * anchor), `supportedChains` + `settlementAddresses` (settlement), and\n * the out-of-band `routes` map (`{publish, store}` ILP destinations).\n * 3. GENESIS SEED — `@toon-protocol/core`'s committed genesis peer seed\n * (core >= 3.1.1 ships the live devnet apex), the offline fallback when\n * the relay is unreachable or serves no valid announce.\n *\n * Chain-level parameters the announce does NOT carry (EVM TokenNetwork\n * contract, token address, RPC URL) are derived per selected chain:\n * explicit config > announce (`tokenNetworks`/`preferredTokens`, when a peer\n * announces them) > core's deterministic chain presets (`CHAIN_PRESETS`,\n * matched by chain id — e.g. `evm:31337` is the Foundry/anvil deploy whose\n * TOON contract addresses are deterministic; `evm:base:84532` and\n * `solana:devnet` are the public networks the TOON devnet settles on since\n * the zone's self-hosted chain nodes were retired, 2026-07).\n *\n * Settlement-chain selection (#260 root cause 4) — simple and predictable:\n *\n * 1. EXPLICIT — `TOON_CLIENT_CHAIN` env / `chain` config field (family or\n * full chain id), or the first entry of an explicit `supportedChains`.\n * 2. PERSISTED CHANNEL — the chain of the most recently used live channel\n * recorded for this identity in the #262 channel map (a channel there\n * means collateral is already locked on that chain).\n * 3. FUNDED — the first announced chain (EVM or Solana, in announce\n * order) where the identity's token balance is > 0 (one `eth_call` /\n * `getTokenAccountsByOwner` per candidate).\n * 4. DEFAULT — the first EVM chain the peer announces (else the first\n * announced chain), with a printed rationale.\n *\n * This module is pure Node + `@toon-protocol/core` (rig's own core 3.x —\n * distinct from the embedded client's internal core; see\n * `../cli/standalone-mode.ts` for the coexistence note).\n */\n\nimport {\n CHAIN_PRESETS,\n GenesisPeerLoader,\n isEventExpired,\n parseIlpPeerInfo,\n resolveClientNetwork,\n type GenesisPeer,\n type IlpPeerInfo,\n} from '@toon-protocol/core';\nimport {\n queryRelay,\n type NostrEvent,\n type WebSocketFactory,\n} from '../remote-state.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** kind:10032 — ILP peer info announcement (mirrors core's constant). */\nexport const ILP_PEER_INFO_KIND = 10032;\n\n/** Publish/store ILP route hints riding out-of-band in announce content. */\nexport interface AnnouncedRoutes {\n publish?: string;\n store?: string;\n}\n\n/**\n * One announced capability with its FLAT route price (out-of-band\n * `capabilities` content field, e.g. `{capability:'os.publish',\n * address:'g.proxy.relay', price:'1000'}`). The connector (≥3.34.6) gates\n * every paid packet at the destination route's price: a balance-proof claim\n * advancing the channel by less is rejected (F06), so per-packet fees to\n * `address` must be floored at `price`.\n */\nexport interface AnnouncedCapability {\n /** Capability name, e.g. `os.publish` / `os.store`. */\n capability: string;\n /** ILP destination address the capability (and its price) applies to. */\n address: string;\n /** Flat per-packet price, base-unit non-negative integer string. */\n price: string;\n}\n\n/** One schema-valid, unexpired kind:10032 announce seen on the relay. */\nexport interface AnnouncedPeer {\n /** Announcing identity (event author, hex). */\n pubkey: string;\n /** Parsed + validated `IlpPeerInfo` (rig's core 3.x parser). */\n info: IlpPeerInfo;\n /** Out-of-band `routes` content field, when present and well-formed. */\n routes?: AnnouncedRoutes;\n /** Out-of-band `capabilities` content field (route prices), when valid. */\n capabilities?: AnnouncedCapability[];\n /**\n * Out-of-band `minaTokenIds` content field: chain id → Mina token id\n * (decimal string). The zkApp address for a Mina channel already rides the\n * standard `tokenNetworks` map (mirroring Solana's program id), but the Mina\n * token id — the deployment-specific fungible-token discriminator a custom\n * USDC channel opens on — has no home in core's kind:10032 wire schema, so\n * the connector advertises it here (a content ride-along, like `routes`).\n * Absent for native-MINA settlement (token id defaults to '1' downstream).\n */\n minaTokenIds?: Record<string, string>;\n /**\n * Out-of-band `chainRpcUrls` content field: chain id → JSON-RPC / GraphQL\n * endpoint the deployment KNOWS works for that chain. Core's baked chain\n * presets carry a default RPC per chain, but a baked default can be a stale\n * or broken endpoint (e.g. Base Sepolia's `sepolia.base.org` load balancer\n * fails openChannel→setTotalDeposit with InvalidChannelState) — the same\n * stale-constant failure mode as the base-sepolia preset bug (#103). So the\n * live deployment advertises its OWN working RPC here and the client prefers\n * it over the preset (see {@link resolveChainSettlement}). A content\n * ride-along, like `routes` — core's kind:10032 wire schema is unchanged.\n */\n chainRpcUrls?: Record<string, string>;\n /** Announce timestamp (freshness tiebreaker). */\n createdAt: number;\n}\n\n/** Per-chain settlement parameters resolved for the embedded client. */\nexport interface ChainSettlement {\n /** Chain id as announced/configured, e.g. `evm:31337`. */\n chain: string;\n /** `evm` | `solana` | `mina` | … (first chain-id segment). */\n family: string;\n /** JSON-RPC / GraphQL endpoint, when derivable. */\n rpcUrl?: string;\n /** Preferred token (USDC / SPL mint) address, when derivable. */\n tokenAddress?: string;\n /** EVM TokenNetwork contract, when derivable (EVM only). */\n tokenNetwork?: string;\n /** Solana payment-channel program id, when derivable (Solana only). */\n programId?: string;\n /** Mina payment-channel zkApp address, when derivable (Mina only). */\n zkAppAddress?: string;\n /** Mina token id (decimal string), when derivable (Mina only). */\n minaTokenId?: string;\n /** Mina network id for the account/Schnorr prefix (Mina only). */\n networkId?: 'devnet' | 'mainnet';\n}\n\n/** Why a settlement chain was selected (documented rule, in order). */\nexport type ChainSelectionReason =\n | 'explicit'\n | 'persisted-channel'\n | 'funded'\n | 'default';\n\n/** The selected settlement chain plus its rationale. */\nexport interface ChainSelection {\n chain: string;\n reason: ChainSelectionReason;\n /** One human-readable line explaining the pick. */\n detail: string;\n}\n\n// ---------------------------------------------------------------------------\n// Announce discovery\n// ---------------------------------------------------------------------------\n\n/** Default bounded wait for the kind:10032 relay query. */\nexport const DISCOVERY_TIMEOUT_MS = 5000;\n\nfunction parseRoutes(content: string): AnnouncedRoutes | undefined {\n try {\n const parsed = JSON.parse(content) as { routes?: unknown };\n const routes = parsed.routes;\n if (typeof routes !== 'object' || routes === null) return undefined;\n const { publish, store } = routes as Record<string, unknown>;\n const out: AnnouncedRoutes = {\n ...(typeof publish === 'string' && publish.length > 0 ? { publish } : {}),\n ...(typeof store === 'string' && store.length > 0 ? { store } : {}),\n };\n return out.publish || out.store ? out : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Parse the announce's out-of-band `capabilities` array (route prices).\n * Malformed entries are skipped; a `price` must be a non-negative integer\n * string (the same base-unit serialization every other announced amount\n * uses). Returns undefined when nothing valid is present.\n */\nfunction parseCapabilities(content: string): AnnouncedCapability[] | undefined {\n try {\n const parsed = JSON.parse(content) as { capabilities?: unknown };\n if (!Array.isArray(parsed.capabilities)) return undefined;\n const out: AnnouncedCapability[] = [];\n for (const entry of parsed.capabilities) {\n if (typeof entry !== 'object' || entry === null) continue;\n const { capability, address, price } = entry as Record<string, unknown>;\n if (\n typeof capability === 'string' &&\n typeof address === 'string' &&\n address.length > 0 &&\n typeof price === 'string' &&\n /^\\d+$/.test(price)\n ) {\n out.push({ capability, address, price });\n }\n }\n return out.length > 0 ? out : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Parse the announce's out-of-band `minaTokenIds` map (chain id → Mina token\n * id decimal string). Non-string / non-decimal entries are skipped; keys are\n * accepted verbatim (they are matched against the selected chain id later).\n * Returns undefined when nothing valid is present.\n */\nfunction parseMinaTokenIds(content: string): Record<string, string> | undefined {\n try {\n const parsed = JSON.parse(content) as { minaTokenIds?: unknown };\n const map = parsed.minaTokenIds;\n if (typeof map !== 'object' || map === null || Array.isArray(map)) {\n return undefined;\n }\n const out: Record<string, string> = {};\n for (const [chain, tokenId] of Object.entries(map as Record<string, unknown>)) {\n if (typeof tokenId === 'string' && /^\\d+$/.test(tokenId)) {\n out[chain] = tokenId;\n }\n }\n return Object.keys(out).length > 0 ? out : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Parse the announce's out-of-band `chainRpcUrls` map (chain id → RPC/GraphQL\n * endpoint). Non-string / non-http(s) entries are skipped; keys are accepted\n * verbatim (matched against the selected chain id later). Returns undefined\n * when nothing valid is present.\n */\nfunction parseChainRpcUrls(content: string): Record<string, string> | undefined {\n try {\n const parsed = JSON.parse(content) as { chainRpcUrls?: unknown };\n const map = parsed.chainRpcUrls;\n if (typeof map !== 'object' || map === null || Array.isArray(map)) {\n return undefined;\n }\n const out: Record<string, string> = {};\n for (const [chain, url] of Object.entries(map as Record<string, unknown>)) {\n if (typeof url === 'string' && /^(https?|wss?):\\/\\//.test(url)) {\n out[chain] = url;\n }\n }\n return Object.keys(out).length > 0 ? out : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Query `relayUrl` for kind:10032 announces and return the latest\n * schema-valid, unexpired announce per author. Invalid/expired events are\n * skipped silently (the relay serves plenty of non-peer 10032 experiments).\n */\nexport async function discoverAnnouncedPeers(\n relayUrl: string,\n options: {\n timeoutMs?: number;\n webSocketFactory?: WebSocketFactory;\n } = {}\n): Promise<AnnouncedPeer[]> {\n const factory =\n options.webSocketFactory ?? defaultDiscoveryWebSocketFactory();\n const events = await queryRelay(\n relayUrl,\n { kinds: [ILP_PEER_INFO_KIND], limit: 100 },\n options.timeoutMs ?? DISCOVERY_TIMEOUT_MS,\n factory\n );\n\n const latestByAuthor = new Map<string, NostrEvent>();\n for (const event of events) {\n if (event.kind !== ILP_PEER_INFO_KIND) continue;\n const prev = latestByAuthor.get(event.pubkey);\n if (!prev || event.created_at > prev.created_at) {\n latestByAuthor.set(event.pubkey, event);\n }\n }\n\n const peers: AnnouncedPeer[] = [];\n for (const event of latestByAuthor.values()) {\n if (isEventExpired(event as Parameters<typeof isEventExpired>[0])) {\n continue;\n }\n let info: IlpPeerInfo;\n try {\n info = parseIlpPeerInfo(event as Parameters<typeof parseIlpPeerInfo>[0]);\n } catch {\n continue; // not a schema-valid IlpPeerInfo announce\n }\n const routes = parseRoutes(event.content);\n const capabilities = parseCapabilities(event.content);\n const minaTokenIds = parseMinaTokenIds(event.content);\n const chainRpcUrls = parseChainRpcUrls(event.content);\n peers.push({\n pubkey: event.pubkey,\n info,\n ...(routes ? { routes } : {}),\n ...(capabilities ? { capabilities } : {}),\n ...(minaTokenIds ? { minaTokenIds } : {}),\n ...(chainRpcUrls ? { chainRpcUrls } : {}),\n createdAt: event.created_at,\n });\n }\n return peers;\n}\n\nfunction defaultDiscoveryWebSocketFactory(): WebSocketFactory {\n return (url) => {\n const ctor = (\n globalThis as {\n WebSocket?: new (url: string) => ReturnType<WebSocketFactory>;\n }\n ).WebSocket;\n if (!ctor) {\n throw new Error(\n 'No global WebSocket constructor (Node >= 22 required) for announce discovery'\n );\n }\n return new ctor(url);\n };\n}\n\n/**\n * Pick THE payment peer among discovered announces:\n *\n * 1. the announce authored by a genesis-seed pubkey (the committed apex\n * identity — the strongest signal),\n * 2. else an announce that can actually take paid writes: has an uplink\n * endpoint AND `settlementAddresses`, preferring one whose own\n * `ilpAddress` is its `routes.publish` (the publish edge — that is\n * where rig pays first),\n * 3. freshest `created_at` breaks remaining ties.\n */\nexport function pickPaymentPeer(\n peers: AnnouncedPeer[],\n seedPubkeys: readonly string[]\n): AnnouncedPeer | undefined {\n const seeded = peers.filter((p) => seedPubkeys.includes(p.pubkey));\n if (seeded.length > 0) {\n return seeded.sort((a, b) => b.createdAt - a.createdAt)[0];\n }\n\n const payable = peers.filter(\n (p) =>\n (p.info.httpEndpoint || p.info.btpEndpoint) &&\n p.info.settlementAddresses &&\n Object.keys(p.info.settlementAddresses).length > 0\n );\n if (payable.length === 0) return undefined;\n const publishEdges = payable.filter(\n (p) => p.routes?.publish !== undefined && p.routes.publish === p.info.ilpAddress\n );\n const pool = publishEdges.length > 0 ? publishEdges : payable;\n return pool.sort((a, b) => b.createdAt - a.createdAt)[0];\n}\n\n// ---------------------------------------------------------------------------\n// Per-chain settlement derivation\n// ---------------------------------------------------------------------------\n\n/** Numeric chain id of an `evm:<id>` / `evm:<name>:<id>` chain key. */\nexport function evmChainIdOf(chain: string): number | undefined {\n const parts = chain.split(':');\n if (parts[0] !== 'evm') return undefined;\n const raw = parts.length >= 3 ? parts[2] : parts[1];\n const id = Number.parseInt(raw ?? '', 10);\n return Number.isNaN(id) ? undefined : id;\n}\n\n/**\n * Core chain preset matching an EVM chain key by numeric chain id. Presets\n * carry the DETERMINISTIC TOON contract addresses per chain (e.g. the\n * `anvil` 31337 Foundry deploy), which is what makes `tokenNetwork`\n * derivable without the announce carrying it. Base Sepolia (84532) resolves\n * from core's `base-sepolia` preset directly (>=3.1.1 carries the current\n * public 6-decimal USDC + TokenNetwork; the earlier stale addresses that this\n * function used to correct locally are gone).\n */\nexport function evmPresetForChain(chain: string):\n | { rpcUrl: string; usdcAddress: string; tokenNetworkAddress: string }\n | undefined {\n const id = evmChainIdOf(chain);\n if (id === undefined) return undefined;\n for (const preset of Object.values(CHAIN_PRESETS)) {\n if (preset.chainId === id) {\n return {\n rpcUrl: preset.rpcUrl,\n usdcAddress: preset.usdcAddress,\n tokenNetworkAddress: preset.tokenNetworkAddress,\n };\n }\n }\n return undefined;\n}\n\n/**\n * Public-cluster Solana preset matching a `solana:<cluster>` chain key. Draws\n * from core's client network presets (`resolveClientNetwork`) — the same\n * tables the daemon and townhouse resolve — so the deployed public-devnet\n * program/mint and the mainnet-beta RPC + Circle USDC mint come from ONE\n * source. `programId` is present only where the TOON payment-channel program\n * is actually deployed (public devnet today; mainnet-beta once deployed).\n * Announce-provided values and explicit config always take precedence over\n * these presets (see {@link resolveChainSettlement}) — that precedence is\n * what protected the devnet's former self-hosted validator, whose regenerated\n * program id rode the announce (retired 2026-07; the devnet now settles on\n * the public clusters).\n */\nexport function solanaPresetForChain(chain: string):\n | { rpcUrl?: string; tokenMint?: string; programId?: string }\n | undefined {\n const [family, cluster] = chain.split(':');\n if (family !== 'solana' || !cluster) return undefined;\n const tier =\n cluster === 'mainnet-beta'\n ? 'mainnet'\n : cluster === 'devnet'\n ? 'devnet'\n : cluster === 'testnet'\n ? 'testnet'\n : undefined;\n if (!tier) return undefined;\n const presets = resolveClientNetwork(tier);\n // Look up by the exact chain key: the `testnet` tier resolves to the\n // deployed `solana:devnet` cluster, so `solana:testnet` correctly finds\n // nothing (TOON has no deployment on Solana's testnet cluster).\n const rpcUrl = presets.chainRpcUrls[chain];\n const tokenMint = presets.preferredTokens[chain];\n const programId =\n presets.solanaChannel && presets.supportedChains.includes(chain)\n ? presets.solanaChannel.programId\n : undefined;\n if (!rpcUrl && !tokenMint && !programId) return undefined;\n return {\n ...(rpcUrl ? { rpcUrl } : {}),\n ...(tokenMint ? { tokenMint } : {}),\n ...(programId ? { programId } : {}),\n };\n}\n\n/**\n * Public-network Mina preset matching a `mina:<network>` chain key. Draws from\n * core's client network presets (`resolveClientNetwork`) — the SAME tables the\n * daemon and townhouse resolve — so the deployed devnet graphqlUrl and network\n * id come from ONE source.\n *\n * IMPORTANT — the preset supplies only the NON-deployment-specific defaults\n * (`graphqlUrl`, `networkId`): the public minascan endpoint and the network id\n * derivable from the chain key. It also carries the preset's `zkAppAddress`\n * (and, in a future core release, a `tokenId`) as an OFFLINE-ONLY fallback, but\n * the payment-channel zkApp and the custom-token id are DEPLOYMENT-specific and\n * go stale on redeploy — exactly the drift that produced the stale\n * base-sepolia/solana bug (#103). So the live announce (which advertises the\n * deployment's own zkApp via `tokenNetworks` and its token id via\n * `minaTokenIds`) ALWAYS takes precedence over these preset values in\n * {@link resolveChainSettlement}. `undefined` for non-`mina:*` chains and for\n * networks with no deployed TOON zkApp (e.g. `mina:mainnet`).\n */\nexport function minaPresetForChain(chain: string):\n | {\n graphqlUrl?: string;\n zkAppAddress?: string;\n tokenId?: string;\n networkId?: 'devnet' | 'mainnet';\n }\n | undefined {\n const [family, network] = chain.split(':');\n if (family !== 'mina' || !network) return undefined;\n // Mina has no separate `testnet` network — the deployment is on `mina:devnet`\n // (the core `testnet` tier resolves to the same devnet zkApp, so `mina:testnet`\n // is not a real chain and must resolve to nothing, mirroring `solana:testnet`).\n const tier =\n network === 'mainnet' ? 'mainnet' : network === 'devnet' ? 'devnet' : undefined;\n if (!tier) return undefined;\n const presets = resolveClientNetwork(tier);\n const mina = presets.minaChannel;\n if (!mina) return undefined;\n // Core >= 3.1.1's `minaChannel` carries { graphqlUrl, zkAppAddress,\n // networkId }; a `tokenId` field is read opportunistically so a future core\n // release can add it without a client change (announce still wins).\n const tokenId = (mina as { tokenId?: string }).tokenId;\n return {\n ...(mina.graphqlUrl ? { graphqlUrl: mina.graphqlUrl } : {}),\n ...(mina.zkAppAddress ? { zkAppAddress: mina.zkAppAddress } : {}),\n ...(tokenId ? { tokenId } : {}),\n ...(mina.networkId ? { networkId: mina.networkId } : {}),\n };\n}\n\n/** Explicit per-chain maps from the shared client-config file. */\nexport interface ExplicitChainConfig {\n chainRpcUrls?: Record<string, string>;\n preferredTokens?: Record<string, string>;\n tokenNetworks?: Record<string, string>;\n}\n\n/**\n * Resolve one chain's settlement parameters: explicit config > announce >\n * core chain preset. Fields stay undefined when no source covers them —\n * callers decide whether that is fatal (see\n * {@link TokenNetworkUnderivableError} /\n * {@link SolanaChannelUnderivableError}).\n *\n * For `solana:*` chains the chain-keyed maps carry the SPL analogues: the\n * `preferredTokens` entry is the token mint, and the `tokenNetworks` entry is\n * the payment-channel PROGRAM id (the map is chain-keyed settlement-contract\n * addressing — TokenNetwork contract on EVM, channel program on Solana),\n * surfaced as {@link ChainSettlement.programId}.\n */\nexport function resolveChainSettlement(\n chain: string,\n explicit: ExplicitChainConfig,\n announce?: AnnouncedPeer\n): ChainSettlement {\n const family = chain.split(':')[0] ?? chain;\n const evmPreset = family === 'evm' ? evmPresetForChain(chain) : undefined;\n const solPreset = family === 'solana' ? solanaPresetForChain(chain) : undefined;\n const minaPreset = family === 'mina' ? minaPresetForChain(chain) : undefined;\n\n const rpcUrl =\n explicit.chainRpcUrls?.[chain] ??\n // The live deployment's advertised RPC beats the baked preset default: a\n // preset RPC can be a stale/broken endpoint (e.g. Base Sepolia's\n // sepolia.base.org fails setTotalDeposit) while the announce carries the\n // deployment's known-good URL. Same drift lesson as #103.\n announce?.chainRpcUrls?.[chain] ??\n evmPreset?.rpcUrl ??\n solPreset?.rpcUrl ??\n // For Mina the \"RPC\" is the GraphQL endpoint (non-deployment-specific).\n minaPreset?.graphqlUrl;\n const tokenAddress =\n explicit.preferredTokens?.[chain] ??\n announce?.info.preferredTokens?.[chain] ??\n (evmPreset?.usdcAddress || undefined) ??\n solPreset?.tokenMint;\n const tokenNetwork =\n family === 'evm'\n ? (explicit.tokenNetworks?.[chain] ??\n announce?.info.tokenNetworks?.[chain] ??\n (evmPreset?.tokenNetworkAddress || undefined))\n : undefined;\n const programId =\n family === 'solana'\n ? (explicit.tokenNetworks?.[chain] ??\n announce?.info.tokenNetworks?.[chain] ??\n solPreset?.programId)\n : undefined;\n // Mina: the settlement-contract slot (`tokenNetworks`, chain-keyed) carries\n // the payment-channel zkApp address — the announce advertises it exactly as\n // Solana advertises its program id. The token id rides the out-of-band\n // `minaTokenIds` content map. Both prefer explicit config > announce > the\n // (drift-prone) preset fallback; the graphqlUrl/networkId come from the\n // preset (or the derived chain key).\n const zkAppAddress =\n family === 'mina'\n ? (explicit.tokenNetworks?.[chain] ??\n announce?.info.tokenNetworks?.[chain] ??\n minaPreset?.zkAppAddress)\n : undefined;\n const minaTokenId =\n family === 'mina'\n ? (announce?.minaTokenIds?.[chain] ?? minaPreset?.tokenId)\n : undefined;\n const networkId =\n family === 'mina'\n ? (minaPreset?.networkId ??\n (chain.split(':')[1] === 'mainnet' ? 'mainnet' : 'devnet'))\n : undefined;\n\n return {\n chain,\n family,\n ...(rpcUrl ? { rpcUrl } : {}),\n ...(tokenAddress ? { tokenAddress } : {}),\n ...(tokenNetwork ? { tokenNetwork } : {}),\n ...(programId ? { programId } : {}),\n ...(zkAppAddress ? { zkAppAddress } : {}),\n ...(minaTokenId ? { minaTokenId } : {}),\n ...(networkId ? { networkId } : {}),\n };\n}\n\n/** An EVM chain was selected but its TokenNetwork cannot be derived. */\nexport class TokenNetworkUnderivableError extends Error {\n constructor(chain: string, announce: AnnouncedPeer | undefined, relayUrl: string) {\n const announceRef = announce\n ? `the kind:10032 announce from ${announce.pubkey.slice(0, 16)}… on ${relayUrl}`\n : `no kind:10032 announce was found on ${relayUrl}`;\n super(\n `cannot derive the TokenNetwork contract for settlement chain ` +\n `\"${chain}\": ${announceRef} carries no tokenNetworks[\"${chain}\"], ` +\n `and no built-in chain preset matches its chain id — add ` +\n `tokenNetworks[\"${chain}\"] to the client config (or pick another ` +\n `chain via TOON_CLIENT_CHAIN / the chain config field)`\n );\n this.name = 'TokenNetworkUnderivableError';\n }\n}\n\n/**\n * A Solana chain was selected but its channel parameters cannot be derived.\n * The missing pieces are named so the error is directly actionable: a\n * `solanaChannel` config object always works; the chain-keyed maps\n * (`tokenNetworks` = program id, `preferredTokens` = mint,\n * `chainRpcUrls` = RPC) work per-chain.\n */\nexport class SolanaChannelUnderivableError extends Error {\n constructor(\n chain: string,\n missing: string[],\n announce: AnnouncedPeer | undefined,\n relayUrl: string\n ) {\n const announceRef = announce\n ? `the kind:10032 announce from ${announce.pubkey.slice(0, 16)}… on ${relayUrl} does not carry them`\n : `no kind:10032 announce was found on ${relayUrl}`;\n super(\n `cannot derive the Solana channel parameters for settlement chain ` +\n `\"${chain}\" (missing: ${missing.join(', ')}): ${announceRef}, and ` +\n `no built-in preset covers this cluster — add a solanaChannel ` +\n `{ rpcUrl, programId, tokenMint } object to the client config (or ` +\n `set tokenNetworks[\"${chain}\"] = <program id>, ` +\n `preferredTokens[\"${chain}\"] = <mint>, chainRpcUrls[\"${chain}\"]), ` +\n `or pick another chain via TOON_CLIENT_CHAIN / the chain config field`\n );\n this.name = 'SolanaChannelUnderivableError';\n }\n}\n\n/**\n * A Mina chain was selected but its channel parameters cannot be derived. The\n * missing pieces are named so the error is directly actionable: an explicit\n * `minaChannel` config object always works; the live announce normally\n * supplies the zkApp (`tokenNetworks[\"mina:…\"]`) and token id\n * (`minaTokenIds[\"mina:…\"]`), while the graphqlUrl comes from a core preset.\n */\nexport class MinaChannelUnderivableError extends Error {\n constructor(\n chain: string,\n missing: string[],\n announce: AnnouncedPeer | undefined,\n relayUrl: string\n ) {\n const announceRef = announce\n ? `the kind:10032 announce from ${announce.pubkey.slice(0, 16)}… on ${relayUrl} does not carry them`\n : `no kind:10032 announce was found on ${relayUrl}`;\n super(\n `cannot derive the Mina channel parameters for settlement chain ` +\n `\"${chain}\" (missing: ${missing.join(', ')}): ${announceRef}, and ` +\n `no built-in preset covers this network — add a minaChannel ` +\n `{ graphqlUrl, zkAppAddress, tokenId, networkId } object to the client ` +\n `config (or set tokenNetworks[\"${chain}\"] = <zkApp address> and ` +\n `chainRpcUrls[\"${chain}\"] = <graphql url>), or pick another chain via ` +\n `TOON_CLIENT_CHAIN / the chain config field`\n );\n this.name = 'MinaChannelUnderivableError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Settlement-chain selection (#260 root cause 4)\n// ---------------------------------------------------------------------------\n\n/** The slice of a #262 channel-map record chain selection consumes. */\nexport interface ChannelRecordLike {\n chain: string;\n lastUsedAt: string;\n /** True when the withdraw flow closed/settled the channel. */\n closed: boolean;\n}\n\n/** Async token-balance probe (injectable; default is a raw `eth_call`). */\nexport type EvmBalanceProbe = (args: {\n rpcUrl: string;\n tokenAddress: string;\n owner: string;\n}) => Promise<bigint>;\n\n/**\n * Async SPL token-balance probe (injectable; default is a raw\n * `getTokenAccountsByOwner`). `tokenAddress` is the SPL mint and `owner`\n * the identity's base58 Solana address — the same argument shape as\n * {@link EvmBalanceProbe} so both feed the one funded-chain loop.\n */\nexport type SolanaBalanceProbe = (args: {\n rpcUrl: string;\n tokenAddress: string;\n owner: string;\n}) => Promise<bigint>;\n\nexport interface SelectChainOptions {\n /**\n * Explicit chain choice: full chain id (`evm:31337`), a family\n * (`evm` | `solana` | `mina`) matched against announced chains, or the\n * first entry of an explicit `supportedChains` config.\n */\n explicitChain?: string;\n /** Chains the payment peer announces (announce order preserved). */\n announcedChains: readonly string[];\n /** #262 channel-map records for this identity (any anchor). */\n records?: readonly ChannelRecordLike[];\n /** Identity's EVM address (funded-chain probe, `evm:*` candidates). */\n evmAddress?: string;\n /**\n * Identity's base58 Solana address (funded-chain probe, `solana:*`\n * candidates). Absent (e.g. the identity's Ed25519 key could not be\n * derived) means Solana chains are simply not probed.\n */\n solanaAddress?: string;\n /** Per-chain settlement resolver (for RPC/token of probe candidates). */\n resolveSettlement: (chain: string) => ChainSettlement;\n /** EVM balance probe; probe errors just skip the candidate. */\n probeBalance?: EvmBalanceProbe;\n /** Solana balance probe; probe errors just skip the candidate. */\n probeSolanaBalance?: SolanaBalanceProbe;\n}\n\n/**\n * Select the settlement chain per the documented rule: explicit >\n * persisted channel > funded chain (EVM + Solana, in announce order) >\n * first announced EVM chain.\n */\nexport async function selectSettlementChain(\n options: SelectChainOptions\n): Promise<ChainSelection> {\n const { explicitChain, announcedChains } = options;\n\n // 1 — explicit config always wins (even when the peer does not announce\n // it: the user may know better than a stale announce).\n if (explicitChain) {\n if (explicitChain.includes(':')) {\n // Chain negotiation is an EXACT string intersection: a full chain id\n // spelled differently from the announce for the SAME EVM chain\n // (`evm:base:84532` vs announced `evm:84532`) would strand the pin and\n // let negotiation fall through to another chain entirely. Align an\n // explicit EVM chain id to the announced spelling (same rule the\n // configured-supportedChains path applies).\n if (!announcedChains.includes(explicitChain)) {\n const chainId = evmChainIdOf(explicitChain);\n const equivalent =\n chainId !== undefined\n ? announcedChains.find((c) => evmChainIdOf(c) === chainId)\n : undefined;\n if (equivalent) {\n return {\n chain: equivalent,\n reason: 'explicit',\n detail:\n `chain ${explicitChain} set by config → announced spelling ` +\n `${equivalent} (same EVM chain id ${chainId})`,\n };\n }\n }\n return {\n chain: explicitChain,\n reason: 'explicit',\n detail: `chain ${explicitChain} set by config`,\n };\n }\n const familyMatch = announcedChains.find(\n (c) => (c.split(':')[0] ?? c) === explicitChain\n );\n if (!familyMatch) {\n throw new Error(\n `configured chain family \"${explicitChain}\" is not announced by the ` +\n `payment peer (announced: ${announcedChains.join(', ') || 'none'}) — ` +\n 'set a full chain id (e.g. \"evm:31337\") to force it'\n );\n }\n return {\n chain: familyMatch,\n reason: 'explicit',\n detail: `chain family \"${explicitChain}\" set by config → ${familyMatch}`,\n };\n }\n\n // 2 — a live persisted channel means collateral is already locked there.\n const live = (options.records ?? [])\n .filter((r) => !r.closed && announcedChains.includes(r.chain))\n .sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));\n const persisted = live[0];\n if (persisted) {\n return {\n chain: persisted.chain,\n reason: 'persisted-channel',\n detail: `existing payment channel on ${persisted.chain} (rig channel map)`,\n };\n }\n\n // 3 — first announced chain where the identity holds tokens, probing\n // EVM and Solana candidates in ANNOUNCE ORDER (the peer's stated\n // preference breaks a both-funded tie). Families without an address or\n // probe are skipped, as are candidates whose RPC/token are underivable.\n const evmChains = announcedChains.filter((c) => c.startsWith('evm:'));\n for (const chain of announcedChains) {\n const family = chain.split(':')[0];\n let probe: EvmBalanceProbe | SolanaBalanceProbe | undefined;\n let owner: string | undefined;\n if (family === 'evm') {\n probe = options.probeBalance;\n owner = options.evmAddress;\n } else if (family === 'solana') {\n probe = options.probeSolanaBalance;\n owner = options.solanaAddress;\n }\n if (!probe || !owner) continue;\n const settlement = options.resolveSettlement(chain);\n if (!settlement.rpcUrl || !settlement.tokenAddress) continue;\n try {\n const balance = await probe({\n rpcUrl: settlement.rpcUrl,\n tokenAddress: settlement.tokenAddress,\n owner,\n });\n if (balance > 0n) {\n return {\n chain,\n reason: 'funded',\n detail: `wallet holds ${balance} token base units on ${chain}`,\n };\n }\n } catch {\n // Unreachable RPC / bad token — not a candidate; fall through.\n }\n }\n\n // 4 — predictable default: the first EVM chain the peer announces.\n const fallback = evmChains[0] ?? announcedChains[0];\n if (!fallback) {\n throw new Error(\n 'the payment peer announces no settlement chains — cannot select a ' +\n 'chain for paid writes (set supportedChains/chain in the client config)'\n );\n }\n return {\n chain: fallback,\n reason: 'default',\n detail: evmChains[0]\n ? `first EVM chain announced by the payment peer`\n : `first chain announced by the payment peer (no EVM chain announced)`,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Default balance probes (raw JSON-RPC, no extra deps)\n// ---------------------------------------------------------------------------\n\n/** ERC-20 `balanceOf(address)` selector. */\nconst BALANCE_OF_SELECTOR = '0x70a08231';\n\n/**\n * Read an ERC-20 balance with one raw `eth_call` (keeps the probe free of\n * viem — the embedded client is not started yet when selection runs).\n */\nexport async function evmTokenBalance(args: {\n rpcUrl: string;\n tokenAddress: string;\n owner: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n}): Promise<bigint> {\n const fetchImpl = args.fetchImpl ?? fetch;\n const owner = args.owner.replace(/^0x/, '').toLowerCase().padStart(64, '0');\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), args.timeoutMs ?? 5000);\n try {\n const res = await fetchImpl(args.rpcUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'eth_call',\n params: [\n { to: args.tokenAddress, data: `${BALANCE_OF_SELECTOR}${owner}` },\n 'latest',\n ],\n }),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`eth_call failed: HTTP ${res.status}`);\n }\n const body = (await res.json()) as { result?: unknown; error?: { message?: string } };\n if (typeof body.result !== 'string') {\n throw new Error(`eth_call failed: ${body.error?.message ?? 'no result'}`);\n }\n return BigInt(body.result === '0x' ? '0x0' : body.result);\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read an SPL token balance with one raw `getTokenAccountsByOwner` call\n * (keeps the probe free of @solana/web3.js — the embedded client is not\n * started yet when selection runs). `tokenAddress` is the mint; balances\n * are summed across all of the owner's token accounts for that mint (an\n * owner can hold the mint in more than one account).\n */\nexport async function solanaTokenBalance(args: {\n rpcUrl: string;\n tokenAddress: string;\n owner: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n}): Promise<bigint> {\n const fetchImpl = args.fetchImpl ?? fetch;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), args.timeoutMs ?? 5000);\n try {\n const res = await fetchImpl(args.rpcUrl, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'getTokenAccountsByOwner',\n params: [\n args.owner,\n { mint: args.tokenAddress },\n { encoding: 'jsonParsed' },\n ],\n }),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`getTokenAccountsByOwner failed: HTTP ${res.status}`);\n }\n const body = (await res.json()) as {\n result?: { value?: unknown };\n error?: { message?: string };\n };\n const accounts = body.result?.value;\n if (!Array.isArray(accounts)) {\n throw new Error(\n `getTokenAccountsByOwner failed: ${body.error?.message ?? 'no result'}`\n );\n }\n let total = 0n;\n for (const account of accounts) {\n const amount = (\n account as {\n account?: {\n data?: {\n parsed?: { info?: { tokenAmount?: { amount?: unknown } } };\n };\n };\n }\n )?.account?.data?.parsed?.info?.tokenAmount?.amount;\n if (typeof amount === 'string') total += BigInt(amount);\n }\n return total;\n } finally {\n clearTimeout(timer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Genesis seed access (rig's own core 3.x — live devnet apex since core 3.x)\n// ---------------------------------------------------------------------------\n\n/** The committed genesis peer seed (first entry), if any. */\nexport function loadGenesisSeed(): GenesisPeer | undefined {\n return GenesisPeerLoader.loadGenesisPeers()[0];\n}\n\n/** All committed genesis-seed pubkeys (announce-selection preference). */\nexport function genesisSeedPubkeys(): string[] {\n return GenesisPeerLoader.loadGenesisPeers().map((p) => p.pubkey);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -176,6 +176,210 @@ async function resolveIdentity(options) {
|
|
|
176
176
|
return { ...source, accountIndex, pubkey };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// src/standalone/topology-cache.ts
|
|
180
|
+
import { createHash } from "crypto";
|
|
181
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync } from "fs";
|
|
182
|
+
import { dirname as dirname2 } from "path";
|
|
183
|
+
var TOPOLOGY_CACHE_FILENAME = "rig-topology-cache.json";
|
|
184
|
+
var DEFAULT_TOPOLOGY_TTL_MS = 15 * 60 * 1e3;
|
|
185
|
+
var TOPOLOGY_TTL_ENV = "RIG_TOPOLOGY_TTL_MS";
|
|
186
|
+
function topologyCacheTtlMs(env) {
|
|
187
|
+
const raw = env[TOPOLOGY_TTL_ENV];
|
|
188
|
+
if (raw === void 0 || raw === "") return DEFAULT_TOPOLOGY_TTL_MS;
|
|
189
|
+
const parsed = Number(raw);
|
|
190
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_TOPOLOGY_TTL_MS;
|
|
191
|
+
}
|
|
192
|
+
function topologyCacheKey(args) {
|
|
193
|
+
return createHash("sha256").update(`${args.relayUrl}
|
|
194
|
+
${args.identity}
|
|
195
|
+
${args.fingerprint}`).digest("hex");
|
|
196
|
+
}
|
|
197
|
+
function explicitConfigFingerprint(env, file) {
|
|
198
|
+
const envKeys = [
|
|
199
|
+
"TOON_CLIENT_PROXY_URL",
|
|
200
|
+
"TOON_CLIENT_BTP_URL",
|
|
201
|
+
"TOON_CLIENT_DESTINATION",
|
|
202
|
+
"TOON_CLIENT_PUBLISH_DESTINATION",
|
|
203
|
+
"TOON_CLIENT_STORE_DESTINATION",
|
|
204
|
+
"TOON_CLIENT_CHAIN",
|
|
205
|
+
"TOON_CLIENT_NETWORK"
|
|
206
|
+
];
|
|
207
|
+
const fileKeys = [
|
|
208
|
+
"network",
|
|
209
|
+
"btpUrl",
|
|
210
|
+
"proxyUrl",
|
|
211
|
+
"destination",
|
|
212
|
+
"publishDestination",
|
|
213
|
+
"storeDestination",
|
|
214
|
+
"chain",
|
|
215
|
+
"supportedChains",
|
|
216
|
+
"settlementAddresses",
|
|
217
|
+
"preferredTokens",
|
|
218
|
+
"tokenNetworks",
|
|
219
|
+
"chainRpcUrls"
|
|
220
|
+
];
|
|
221
|
+
const picked = {};
|
|
222
|
+
for (const key of envKeys) {
|
|
223
|
+
if (env[key] !== void 0) picked[`env:${key}`] = env[key];
|
|
224
|
+
}
|
|
225
|
+
for (const key of fileKeys) {
|
|
226
|
+
if (file[key] !== void 0) picked[`file:${key}`] = file[key];
|
|
227
|
+
}
|
|
228
|
+
return JSON.stringify(picked);
|
|
229
|
+
}
|
|
230
|
+
var TopologyCache = class {
|
|
231
|
+
path;
|
|
232
|
+
ttlMs;
|
|
233
|
+
validate;
|
|
234
|
+
now;
|
|
235
|
+
constructor(options) {
|
|
236
|
+
this.path = options.path;
|
|
237
|
+
this.ttlMs = options.ttlMs;
|
|
238
|
+
this.validate = options.validate;
|
|
239
|
+
this.now = options.now ?? Date.now;
|
|
240
|
+
}
|
|
241
|
+
/** True when the cache is disabled (`RIG_TOPOLOGY_TTL_MS=0`). */
|
|
242
|
+
get disabled() {
|
|
243
|
+
return this.ttlMs <= 0;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A fresh, structurally-valid entry for `key` — or undefined (expired,
|
|
247
|
+
* missing, invalid, corrupt file, or disabled). Also reports the entry age
|
|
248
|
+
* for the "topology from cache" stderr line.
|
|
249
|
+
*/
|
|
250
|
+
read(key) {
|
|
251
|
+
if (this.disabled) return void 0;
|
|
252
|
+
const entry = this.readFile().entries[key];
|
|
253
|
+
if (!entry) return void 0;
|
|
254
|
+
const cachedAt = Date.parse(entry.cachedAt);
|
|
255
|
+
if (!Number.isFinite(cachedAt)) return void 0;
|
|
256
|
+
const ageMs = this.now() - cachedAt;
|
|
257
|
+
if (ageMs < 0 || ageMs > this.ttlMs) return void 0;
|
|
258
|
+
if (!this.validate(entry.topology)) return void 0;
|
|
259
|
+
return { topology: entry.topology, ageMs };
|
|
260
|
+
}
|
|
261
|
+
/** Persist `topology` under `key` (prunes expired entries; best-effort). */
|
|
262
|
+
write(key, topology) {
|
|
263
|
+
if (this.disabled) return;
|
|
264
|
+
try {
|
|
265
|
+
const file = this.readFile();
|
|
266
|
+
const nowMs = this.now();
|
|
267
|
+
const fresh = Object.fromEntries(
|
|
268
|
+
Object.entries(file.entries).filter(([, entry]) => {
|
|
269
|
+
const at = Date.parse(entry.cachedAt);
|
|
270
|
+
return Number.isFinite(at) && nowMs - at <= this.ttlMs;
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
fresh[key] = {
|
|
274
|
+
cachedAt: new Date(nowMs).toISOString(),
|
|
275
|
+
topology
|
|
276
|
+
};
|
|
277
|
+
mkdirSync2(dirname2(this.path), { recursive: true });
|
|
278
|
+
writeFileSync2(
|
|
279
|
+
this.path,
|
|
280
|
+
JSON.stringify(
|
|
281
|
+
{ version: 1, entries: fresh },
|
|
282
|
+
null,
|
|
283
|
+
2
|
|
284
|
+
),
|
|
285
|
+
{ mode: 384 }
|
|
286
|
+
);
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Drop `key` (a cached topology failed to bootstrap). Best-effort. */
|
|
291
|
+
invalidate(key) {
|
|
292
|
+
try {
|
|
293
|
+
const file = this.readFile();
|
|
294
|
+
if (!(key in file.entries)) return;
|
|
295
|
+
const remaining = Object.fromEntries(
|
|
296
|
+
Object.entries(file.entries).filter(([k]) => k !== key)
|
|
297
|
+
);
|
|
298
|
+
if (Object.keys(remaining).length === 0) {
|
|
299
|
+
unlinkSync(this.path);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
writeFileSync2(
|
|
303
|
+
this.path,
|
|
304
|
+
JSON.stringify(
|
|
305
|
+
{ version: 1, entries: remaining },
|
|
306
|
+
null,
|
|
307
|
+
2
|
|
308
|
+
),
|
|
309
|
+
{ mode: 384 }
|
|
310
|
+
);
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
readFile() {
|
|
315
|
+
const empty = { version: 1, entries: {} };
|
|
316
|
+
let raw;
|
|
317
|
+
try {
|
|
318
|
+
raw = readFileSync2(this.path, "utf8");
|
|
319
|
+
} catch {
|
|
320
|
+
return empty;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const parsed = JSON.parse(raw);
|
|
324
|
+
if (typeof parsed !== "object" || parsed === null || parsed.version !== 1 || typeof parsed.entries !== "object" || parsed.entries === null) {
|
|
325
|
+
return empty;
|
|
326
|
+
}
|
|
327
|
+
return parsed;
|
|
328
|
+
} catch {
|
|
329
|
+
return empty;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// src/standalone/mina-zkapp-store.ts
|
|
335
|
+
import {
|
|
336
|
+
chmodSync,
|
|
337
|
+
mkdirSync as mkdirSync3,
|
|
338
|
+
readFileSync as readFileSync3,
|
|
339
|
+
writeFileSync as writeFileSync3
|
|
340
|
+
} from "fs";
|
|
341
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
342
|
+
var FILENAME = "rig-mina-zkapps.json";
|
|
343
|
+
function keyFor(identity, chain) {
|
|
344
|
+
return `${identity}|${chain}`;
|
|
345
|
+
}
|
|
346
|
+
var MinaZkAppStore = class _MinaZkAppStore {
|
|
347
|
+
constructor(filePath) {
|
|
348
|
+
this.filePath = filePath;
|
|
349
|
+
}
|
|
350
|
+
filePath;
|
|
351
|
+
/** The store under a client home dir: `<dir>/keys/rig-mina-zkapps.json`. */
|
|
352
|
+
static forHome(dir) {
|
|
353
|
+
return new _MinaZkAppStore(join2(dir, "keys", FILENAME));
|
|
354
|
+
}
|
|
355
|
+
readAll() {
|
|
356
|
+
try {
|
|
357
|
+
return JSON.parse(readFileSync3(this.filePath, "utf8"));
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (err.code === "ENOENT") return {};
|
|
360
|
+
throw new Error(
|
|
361
|
+
`failed to read the Mina zkApp store at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** The recorded deployment for this identity/chain, if any. */
|
|
366
|
+
lookup(identity, chain) {
|
|
367
|
+
return this.readAll()[keyFor(identity, chain)];
|
|
368
|
+
}
|
|
369
|
+
/** Read-merge-write; the file holds private keys → 0600. */
|
|
370
|
+
save(record) {
|
|
371
|
+
const all = this.readAll();
|
|
372
|
+
all[keyFor(record.identity, record.chain)] = record;
|
|
373
|
+
mkdirSync3(dirname3(this.filePath), { recursive: true });
|
|
374
|
+
writeFileSync3(this.filePath, `${JSON.stringify(all, null, 2)}
|
|
375
|
+
`, {
|
|
376
|
+
encoding: "utf8",
|
|
377
|
+
mode: 384
|
|
378
|
+
});
|
|
379
|
+
chmodSync(this.filePath, 384);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
179
383
|
export {
|
|
180
384
|
MissingIdentityError,
|
|
181
385
|
DEFAULT_KEYSTORE_PASSWORD,
|
|
@@ -184,6 +388,13 @@ export {
|
|
|
184
388
|
readClientConfigFile,
|
|
185
389
|
linkKeystoreInClientConfig,
|
|
186
390
|
resolveAccountIndex,
|
|
187
|
-
resolveIdentity
|
|
391
|
+
resolveIdentity,
|
|
392
|
+
TOPOLOGY_CACHE_FILENAME,
|
|
393
|
+
TOPOLOGY_TTL_ENV,
|
|
394
|
+
topologyCacheTtlMs,
|
|
395
|
+
topologyCacheKey,
|
|
396
|
+
explicitConfigFingerprint,
|
|
397
|
+
TopologyCache,
|
|
398
|
+
MinaZkAppStore
|
|
188
399
|
};
|
|
189
|
-
//# sourceMappingURL=chunk-
|
|
400
|
+
//# sourceMappingURL=chunk-FFQXFV4W.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 * Stable cache key: relay-origin + identity pubkey + the explicit-config\n * fingerprint. Hashed so the key is filename/JSON-safe and the mnemonic-side\n * 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(`${args.relayUrl}\\n${args.identity}\\n${args.fingerprint}`)\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"]}
|