@toon-protocol/rig 3.5.3 → 4.0.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 +112 -152
- package/dist/chunk-CDSFVZDV.js +290 -0
- package/dist/chunk-CDSFVZDV.js.map +1 -0
- package/dist/{chunk-B5ISARMU.js → chunk-GVXBUL3C.js} +15 -2
- package/dist/chunk-GVXBUL3C.js.map +1 -0
- package/dist/chunk-LTFMPOHU.js +194 -0
- package/dist/chunk-LTFMPOHU.js.map +1 -0
- package/dist/{chunk-LCYCKWAT.js → chunk-YZRR2UO6.js} +86 -5
- package/dist/chunk-YZRR2UO6.js.map +1 -0
- package/dist/{chunk-HJDPM37T.js → chunk-ZZBA5VVL.js} +11 -7
- package/dist/{chunk-HJDPM37T.js.map → chunk-ZZBA5VVL.js.map} +1 -1
- package/dist/cli/rig.js +298 -529
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +3 -109
- package/dist/index.js +9 -108
- package/dist/index.js.map +1 -1
- package/dist/{publisher-BFcnZeJI.d.ts → publisher-CcL96ugq.d.ts} +58 -1
- package/dist/standalone/index.d.ts +241 -378
- package/dist/standalone/index.js +19 -11
- package/dist/standalone-mode-LEAC4A2L.js +390 -0
- package/dist/standalone-mode-LEAC4A2L.js.map +1 -0
- package/package.json +5 -4
- package/dist/chunk-4CAOEXGQ.js +0 -631
- package/dist/chunk-4CAOEXGQ.js.map +0 -1
- package/dist/chunk-B5ISARMU.js.map +0 -1
- package/dist/chunk-FFQXFV4W.js +0 -400
- package/dist/chunk-FFQXFV4W.js.map +0 -1
- package/dist/chunk-LCYCKWAT.js.map +0 -1
- package/dist/chunk-TTJCBGKW.js +0 -447
- package/dist/chunk-TTJCBGKW.js.map +0 -1
- package/dist/network-bootstrap-CTYIZMP6.js +0 -40
- package/dist/network-bootstrap-CTYIZMP6.js.map +0 -1
- package/dist/standalone-mode-LBWGU7NV.js +0 -724
- package/dist/standalone-mode-LBWGU7NV.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/standalone/channel-map.ts","../src/standalone/nonce-guard.ts","../src/mime.ts"],"sourcesContent":["/**\n * Peer→channel map for the STANDALONE embedded publisher (#262).\n *\n * Why this exists: `@toon-protocol/client`'s `ChannelManager` persists the\n * off-chain nonce/cumulative-claim watermark (its `ChannelStore`,\n * `channels.json`, keyed by channelId) but keeps the peer→channelId mapping\n * ONLY in memory. So every standalone CLI invocation used to open (and fund)\n * a FRESH on-chain channel — the #260 fresh-outsider e2e stranded five\n * deposits across five commands. This store remembers WHICH channel the\n * identity holds with each peer, keyed by\n * `identity pubkey | ILP anchor (peer/apex destination) | chain | tokenNetwork`,\n * so the next invocation resumes it via `ChannelManager.trackChannel` (which\n * rehydrates the nonce watermark from `channels.json`) with zero on-chain\n * writes.\n *\n * It is the standalone twin of the daemon's\n * `packages/client-mcp/src/daemon/apex-channel-store.ts` — same record shape\n * (channelId + the chain context `trackChannel` needs), extended with the\n * identity pubkey (rig identities come from an env/`.env` precedence chain,\n * so one state dir can serve several identities) and the tokenNetwork.\n * `@toon-protocol/rig` must not import `@toon-protocol/client-mcp` (that\n * package depends on this one — circular), hence the twin; keep the\n * semantics in sync.\n *\n * CONCURRENCY: writes happen only from paid commands, which already hold the\n * per-identity advisory lockfile (./nonce-guard.ts `NonceLock`) for their\n * whole lifetime — the same guard that serializes the claim watermark also\n * serializes this file for one identity. `rig channel list` reads only.\n *\n * CORRUPTION: an unreadable/invalid map file is a hard\n * {@link ChannelMapCorruptError} — surfaced BEFORE any on-chain open — never\n * an empty fallback. Falling back to \"no channels\" would silently open (and\n * fund) a duplicate channel, which is exactly the #262 bug.\n *\n * This module is dependency-light on purpose (node:fs only): the free\n * `rig channel list` command reads it without the optional\n * `@toon-protocol/client` peer dependency installed.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Paths (TOON_CLIENT_HOME conventions — see nonce-guard.ts module doc)\n// ---------------------------------------------------------------------------\n\n/** Map filename under the shared client state dir. */\nexport const RIG_CHANNEL_MAP_FILENAME = 'rig-channels.json';\n\n/**\n * Resolve the two channel-state files under `TOON_CLIENT_HOME` (default\n * `~/.toon-client`): the rig peer→channel map, and the client's nonce\n * watermark store (`config.json`'s `channelStorePath`, default\n * `<dir>/channels.json` — the same resolution `cli/standalone-mode.ts` feeds\n * the embedded ToonClient).\n */\nexport function resolveChannelPaths(env: NodeJS.ProcessEnv): {\n mapPath: string;\n watermarkPath: string;\n} {\n const dir = env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n let configured: string | undefined;\n try {\n const raw = readFileSync(join(dir, 'config.json'), 'utf8');\n const parsed = JSON.parse(raw) as { channelStorePath?: unknown };\n if (typeof parsed.channelStorePath === 'string') {\n configured = parsed.channelStorePath;\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw new Error(\n `failed to read client config at ${join(dir, 'config.json')}: ` +\n `${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n return {\n mapPath: join(dir, RIG_CHANNEL_MAP_FILENAME),\n watermarkPath: configured ?? join(dir, 'channels.json'),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/**\n * Chain context `ChannelManager.trackChannel` needs to resume a channel\n * (same shape the daemon's apex-channel-store persists).\n */\nexport interface PersistedChannelContext {\n chainType: string;\n chainId: number;\n tokenNetworkAddress: string;\n tokenAddress?: string;\n /** Counterparty settlement address (required for Solana/Mina proofs). */\n recipient?: string;\n}\n\n/** One persisted peer→channel binding. */\nexport interface ChannelMapRecord {\n /** On-chain payment channel id. */\n channelId: string;\n /** Registered peer id the negotiation was keyed by (`peerNegotiations`). */\n peerId: string;\n /** Hex Nostr pubkey of the identity that opened the channel. */\n identity: string;\n /** ILP anchor destination the channel was opened against (peer/apex). */\n destination: string;\n /** Negotiated settlement chain, e.g. `evm:31337`. */\n chain: string;\n /** TokenNetwork contract address ('' when the peer announced none). */\n tokenNetwork: string;\n /** Context for `trackChannel` on resume. */\n context: PersistedChannelContext;\n /** On-chain deposit total (base units, string), when known. */\n depositTotal?: string;\n /** ISO timestamps. */\n openedAt: string;\n lastUsedAt: string;\n}\n\n/** The composite key a record is stored under. */\nexport interface ChannelMapKey {\n identity: string;\n destination: string;\n chain: string;\n tokenNetwork: string;\n}\n\n/**\n * One entry of the client's nonce-watermark store (`channels.json`) —\n * format DUPLICATED from `@toon-protocol/client`'s `JsonFileChannelStore`\n * (`packages/client/src/channel/ChannelStore.ts`); keep in sync.\n */\nexport interface WatermarkEntry {\n nonce: number;\n /** Cumulative claimed amount, base units (string-encoded bigint). */\n cumulativeAmount: string;\n /** Withdraw-flow timers, string-encoded unix SECONDS. */\n closedAt?: string;\n settleableAt?: string;\n settledAt?: string;\n}\n\n/** The peer→channel map file is unreadable or malformed. */\nexport class ChannelMapCorruptError extends Error {\n constructor(\n public readonly path: string,\n detail: string\n ) {\n super(\n `channel state file ${path} is corrupt (${detail}) — refusing to ` +\n 'continue: proceeding would silently open (and fund) a duplicate ' +\n 'on-chain channel. Fix or remove the file; removing it makes rig ' +\n 'forget which channels it holds (existing deposits stay locked ' +\n 'on-chain until settled).'\n );\n this.name = 'ChannelMapCorruptError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Store\n// ---------------------------------------------------------------------------\n\ninterface MapFile {\n version: 1;\n channels: Record<string, ChannelMapRecord>;\n}\n\nfunction keyOf(key: ChannelMapKey): string {\n return `${key.identity}|${key.destination}|${key.chain}|${key.tokenNetwork}`;\n}\n\n/** The key fields of a full record. */\nexport function recordKey(record: ChannelMapRecord): ChannelMapKey {\n return {\n identity: record.identity,\n destination: record.destination,\n chain: record.chain,\n tokenNetwork: record.tokenNetwork,\n };\n}\n\nfunction isContext(v: unknown): v is PersistedChannelContext {\n if (typeof v !== 'object' || v === null) return false;\n const c = v as Record<string, unknown>;\n return (\n typeof c['chainType'] === 'string' &&\n typeof c['chainId'] === 'number' &&\n typeof c['tokenNetworkAddress'] === 'string'\n );\n}\n\nfunction isRecord(v: unknown): v is ChannelMapRecord {\n if (typeof v !== 'object' || v === null) return false;\n const r = v as Record<string, unknown>;\n return (\n typeof r['channelId'] === 'string' &&\n typeof r['peerId'] === 'string' &&\n typeof r['identity'] === 'string' &&\n typeof r['destination'] === 'string' &&\n typeof r['chain'] === 'string' &&\n typeof r['tokenNetwork'] === 'string' &&\n isContext(r['context'])\n );\n}\n\nexport interface ChannelMapStoreOptions {\n /** The rig peer→channel map file (`rig-channels.json`). */\n mapPath: string;\n /** The client's nonce-watermark store (`channels.json`). */\n watermarkPath: string;\n}\n\n/**\n * File-backed peer→channel map + read/seed access to the client's nonce\n * watermark store. Synchronous I/O (matches the client's `ChannelStore`\n * surface); see the module doc for the locking and corruption contracts.\n */\nexport class ChannelMapStore {\n readonly mapPath: string;\n readonly watermarkPath: string;\n\n constructor(options: ChannelMapStoreOptions) {\n this.mapPath = options.mapPath;\n this.watermarkPath = options.watermarkPath;\n }\n\n /** All recorded channels. @throws {ChannelMapCorruptError} */\n list(): ChannelMapRecord[] {\n return Object.values(this.readMap().channels);\n }\n\n /**\n * Recorded channels for one (identity, destination) pair — the resume\n * candidates for a paid command. @throws {ChannelMapCorruptError}\n */\n listFor(identity: string, destination: string): ChannelMapRecord[] {\n return this.list().filter(\n (r) => r.identity === identity && r.destination === destination\n );\n }\n\n /**\n * Record a (freshly opened) channel. Overwrites any previous record under\n * the same (identity, destination, chain, tokenNetwork) key — the old\n * channel is closed/stale by then; its claim watermark stays in the\n * watermark store.\n */\n record(\n record: Omit<ChannelMapRecord, 'openedAt' | 'lastUsedAt'> &\n Partial<Pick<ChannelMapRecord, 'openedAt' | 'lastUsedAt'>>\n ): void {\n const now = new Date().toISOString();\n const full: ChannelMapRecord = {\n ...record,\n openedAt: record.openedAt ?? now,\n lastUsedAt: record.lastUsedAt ?? now,\n };\n const data = this.readMap();\n data.channels[keyOf(recordKey(full))] = full;\n this.writeMap(data);\n }\n\n /**\n * Bump a record's `lastUsedAt` (and optionally its known on-chain deposit)\n * after resuming it. Unknown keys are a no-op.\n */\n touch(key: ChannelMapKey, update?: { depositTotal?: string }): void {\n const data = this.readMap();\n const existing = data.channels[keyOf(key)];\n if (!existing) return;\n existing.lastUsedAt = new Date().toISOString();\n if (update?.depositTotal !== undefined) {\n existing.depositTotal = update.depositTotal;\n }\n this.writeMap(data);\n }\n\n /**\n * Read one channel's nonce-watermark entry from the client's\n * `channels.json` (undefined when the file or entry is missing).\n * @throws {ChannelMapCorruptError} when the watermark file is unreadable.\n */\n readWatermark(channelId: string): WatermarkEntry | undefined {\n return this.readWatermarkFile()[channelId];\n }\n\n /**\n * Seed a fresh channel's watermark entry (`nonce 0, cumulative 0`) so a\n * later resume can tell \"never claimed against\" apart from \"watermark\n * lost\". Never overwrites an existing entry.\n */\n seedWatermark(channelId: string): void {\n const data = this.readWatermarkFile();\n if (data[channelId]) return;\n data[channelId] = { nonce: 0, cumulativeAmount: '0' };\n mkdirSync(dirname(this.watermarkPath), { recursive: true });\n writeFileSync(\n this.watermarkPath,\n JSON.stringify(data, null, 2),\n 'utf-8'\n );\n }\n\n // ── file I/O ───────────────────────────────────────────────────────────────\n\n private readMap(): MapFile {\n let raw: string;\n try {\n raw = readFileSync(this.mapPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n return { version: 1, channels: {} };\n }\n throw new ChannelMapCorruptError(\n this.mapPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n if (\n typeof parsed !== 'object' ||\n parsed === null ||\n (parsed as { version?: unknown }).version !== 1 ||\n typeof (parsed as { channels?: unknown }).channels !== 'object' ||\n (parsed as { channels?: unknown }).channels === null\n ) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n 'expected { \"version\": 1, \"channels\": { … } }'\n );\n }\n const channels = (parsed as { channels: Record<string, unknown> })\n .channels;\n for (const [key, value] of Object.entries(channels)) {\n if (!isRecord(value)) {\n throw new ChannelMapCorruptError(\n this.mapPath,\n `entry ${JSON.stringify(key)} is missing required fields`\n );\n }\n }\n return parsed as MapFile;\n }\n\n private writeMap(data: MapFile): void {\n mkdirSync(dirname(this.mapPath), { recursive: true });\n writeFileSync(this.mapPath, JSON.stringify(data, null, 2), {\n mode: 0o600,\n });\n }\n\n private readWatermarkFile(): Record<string, WatermarkEntry> {\n let raw: string;\n try {\n raw = readFileSync(this.watermarkPath, 'utf8');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return {};\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n err instanceof Error ? err.message : String(err)\n );\n }\n try {\n return JSON.parse(raw) as Record<string, WatermarkEntry>;\n } catch (err) {\n throw new ChannelMapCorruptError(\n this.watermarkPath,\n `invalid JSON: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Status derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Where a channel sits in the withdraw journey, from its watermark timers —\n * mirrors `ChannelManager.getChannelCloseState`. A missing entry reads as\n * `open` (recorded channels are seeded at open time; a lost watermark file\n * surfaces separately as unknown claim state).\n */\nexport function channelStatus(\n entry: WatermarkEntry | undefined,\n nowSec: number = Math.floor(Date.now() / 1000)\n): 'open' | 'closing' | 'settleable' | 'settled' {\n if (!entry || entry.closedAt === undefined) return 'open';\n if (entry.settledAt !== undefined) return 'settled';\n if (\n entry.settleableAt !== undefined &&\n BigInt(nowSec) >= BigInt(entry.settleableAt)\n ) {\n return 'settleable';\n }\n return 'closing';\n}\n","/**\n * Nonce-ownership guard for the STANDALONE embedded Publisher (#228).\n *\n * Why this exists: a payment channel's balance proof is a CUMULATIVE\n * watermark — the ChannelManager auto-increments the nonce and cumulative\n * amount on every `signBalanceProof`. Two writers signing claims on the same\n * channel from separate processes (a running `toon-clientd` daemon plus a\n * standalone embedded client, or two standalone processes) each keep their\n * own cumulative counter, so their claims race: the connector sees\n * non-monotonic watermarks and a re-signed claim can double-charge (the\n * hazard documented in packages/rig-web/tests/e2e/seed/lib/publish.ts).\n *\n * Two independent defenses, both keyed by the Nostr pubkey (one identity =\n * one channel set):\n *\n * 1. Daemon detection — probe the toon-clientd loopback control API\n * (`GET /status`) and REFUSE when it reports the SAME identity. A daemon\n * on a different identity holds different channels and is harmless.\n * Since #279 the CLI's paid WRITE commands never get here with a\n * same-identity daemon up — they delegate to its `/git/*` routes first\n * (`cli/daemon-session.ts`), which achieves the same one-writer goal by\n * handing the watermark to the daemon. This guard remains the backstop\n * for the probe→publish race window and for operations with no daemon\n * route (channel close/settle, explicit open).\n * 2. Advisory lockfile — an exclusive per-pubkey lockfile under the shared\n * `~/.toon-client` state dir so two STANDALONE processes can't race each\n * other (the daemon check only covers the daemon). Stale locks (dead pid)\n * are reclaimed.\n *\n * The daemon port and state-dir conventions are DUPLICATED from\n * `packages/client-mcp/src/daemon/config.ts` (default port 8787 /\n * `TOON_CLIENT_HTTP_PORT`; `~/.toon-client` / `TOON_CLIENT_HOME`).\n * `@toon-protocol/rig` must not depend on `@toon-protocol/client-mcp`\n * (the daemon package depends on this one for the #227 Publisher — the\n * import would be circular), so the constants live here with this note.\n * Keep them in sync.\n */\n\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Shared conventions (duplicated from client-mcp — see module doc)\n// ---------------------------------------------------------------------------\n\n/** Default toon-clientd loopback control API port (client-mcp `httpPort`). */\nexport const DEFAULT_DAEMON_PORT = 8787;\n\n/** Daemon control API port: `TOON_CLIENT_HTTP_PORT` env, else 8787. */\nexport function defaultDaemonPort(): number {\n const env = process.env['TOON_CLIENT_HTTP_PORT'];\n const parsed = env ? Number(env) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;\n}\n\n/**\n * Shared client state dir: `TOON_CLIENT_HOME` env, else `~/.toon-client` —\n * the same dir the daemon keeps its config/channel stores in, so daemon and\n * standalone processes agree on where the advisory locks live.\n */\nexport function defaultLockDir(): string {\n return process.env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\n// ---------------------------------------------------------------------------\n// Force-standalone override\n// ---------------------------------------------------------------------------\n\n/**\n * Env var (and `--standalone` / `--no-daemon` flag) that makes rig ALWAYS run\n * embedded, bypassing a running `toon-clientd` entirely — both the #279\n * daemon-delegation fast path ({@link ../cli/daemon-session.ts}) and the\n * same-identity refusal ({@link checkDaemonIdentity}, Guard 1 below).\n *\n * SAFETY: this only disables the DAEMON detection. The exclusive per-pubkey\n * advisory lockfile (Guard 2, {@link NonceLock}) still runs, so two forced\n * STANDALONE rig processes cannot race the channel's cumulative-claim\n * watermark. What it CANNOT protect against is a running daemon on the SAME\n * identity that is ALSO signing claims concurrently — the daemon does not\n * take the lockfile. So the durable, race-free way to \"always standalone\" is\n * to STOP the daemon (its pid is in `~/.toon-client/daemon.pid`); this\n * override is for the common case where the daemon is idle/in-the-way and you\n * just want rig to sign for itself.\n */\nexport const RIG_STANDALONE_ENV = 'RIG_STANDALONE';\n\n/**\n * True when {@link RIG_STANDALONE_ENV} is set to a truthy value\n * (`1`/`true`/`yes`/`on`, case-insensitive). Any other value — including an\n * explicit `0`/`false`/empty — leaves the default daemon-aware behaviour.\n */\nexport function standaloneForced(env: NodeJS.ProcessEnv): boolean {\n const raw = env[RIG_STANDALONE_ENV];\n if (raw === undefined) return false;\n const v = raw.trim().toLowerCase();\n return v === '1' || v === 'true' || v === 'yes' || v === 'on';\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** A running toon-clientd holds the same identity (channel watermark owner). */\nexport class DaemonIdentityConflictError extends Error {\n constructor(\n /** The shared Nostr pubkey (hex). */\n public readonly pubkey: string,\n /** The daemon control API URL that answered. */\n public readonly daemonUrl: string\n ) {\n super(\n `toon-clientd is running with this identity (${pubkey.slice(0, 8)}…) at ` +\n `${daemonUrl} — paid rig writes delegate to it automatically, ` +\n `but this operation has no daemon route (or the daemon appeared ` +\n `mid-run): stop the daemon and re-run. Two writers on one identity ` +\n `would race the payment channel's cumulative-claim watermark ` +\n `(double-charge hazard).`\n );\n this.name = 'DaemonIdentityConflictError';\n }\n}\n\n/** Another standalone process already holds the per-identity lock. */\nexport class StandaloneLockError extends Error {\n constructor(\n public readonly pubkey: string,\n public readonly lockPath: string,\n public readonly holderPid: number\n ) {\n super(\n `another standalone process (pid ${holderPid}) already holds the ` +\n `payment-channel lock for identity ${pubkey.slice(0, 8)}… ` +\n `(${lockPath}) — wait for it to finish or stop it. Two writers on one ` +\n `identity would race the cumulative-claim watermark.`\n );\n this.name = 'StandaloneLockError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Daemon detection\n// ---------------------------------------------------------------------------\n\nexport interface CheckDaemonOptions {\n /** Control API port (default: `TOON_CLIENT_HTTP_PORT` env, else 8787). */\n port?: number;\n /** Probe timeout, ms (default 1500 — loopback, so fast). */\n timeoutMs?: number;\n /** Inject a fetch implementation (tests). Defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Probe the toon-clientd loopback control API and throw\n * {@link DaemonIdentityConflictError} when a daemon responds on `/status`\n * with `identity.nostrPubkey === pubkey`.\n *\n * Anything short of a positive identity match lets the caller proceed: no\n * listener, a timeout, a non-JSON response (some other local service on the\n * port), or a daemon on a DIFFERENT identity (its channels are keyed to its\n * own pubkey — no shared watermark).\n */\nexport async function checkDaemonIdentity(\n pubkey: string,\n options: CheckDaemonOptions = {}\n): Promise<void> {\n const port = options.port ?? defaultDaemonPort();\n const fetchImpl = options.fetchImpl ?? fetch;\n const url = `http://127.0.0.1:${port}/status`;\n\n let daemonPubkey: string | undefined;\n try {\n const res = await fetchImpl(url, {\n signal: AbortSignal.timeout(options.timeoutMs ?? 1500),\n });\n if (!res.ok) return; // listening, but not a healthy daemon status\n const body = (await res.json()) as {\n identity?: { nostrPubkey?: unknown };\n };\n const candidate = body?.identity?.nostrPubkey;\n if (typeof candidate === 'string') daemonPubkey = candidate;\n } catch {\n // Unreachable / timed out / not JSON → no same-identity daemon detected.\n return;\n }\n\n if (daemonPubkey !== undefined && daemonPubkey === pubkey) {\n throw new DaemonIdentityConflictError(pubkey, url);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Advisory per-identity lockfile\n// ---------------------------------------------------------------------------\n\ninterface LockFileContents {\n pid: number;\n pubkey: string;\n createdAt: string;\n}\n\nexport interface AcquireLockOptions {\n /** Directory the lockfile lives in (default: {@link defaultLockDir}). */\n dir?: string;\n /** Override the recorded pid (tests). Defaults to `process.pid`. */\n pid?: number;\n}\n\n/** True when `pid` refers to a live process we can see. */\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = alive but not ours; ESRCH (and anything else) = not running.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Exclusive advisory lock for one identity's payment-channel watermark.\n *\n * Acquired with an atomic `wx` create of `standalone-<pubkey>.lock` (JSON:\n * pid + pubkey + timestamp) under the shared state dir. A pre-existing lock\n * whose pid is dead (or whose contents are unreadable) is STALE and gets\n * reclaimed; a live holder throws {@link StandaloneLockError}. Released\n * explicitly via {@link release} and best-effort on process exit.\n */\nexport class NonceLock {\n private released = false;\n private readonly exitHandler: () => void;\n\n private constructor(\n public readonly pubkey: string,\n public readonly lockPath: string\n ) {\n this.exitHandler = () => {\n try {\n unlinkSync(this.lockPath);\n } catch {\n // best-effort — the pid check makes a leftover lock reclaimable anyway\n }\n };\n process.once('exit', this.exitHandler);\n }\n\n static async acquire(\n pubkey: string,\n options: AcquireLockOptions = {}\n ): Promise<NonceLock> {\n const dir = options.dir ?? defaultLockDir();\n const pid = options.pid ?? process.pid;\n const lockPath = join(dir, `standalone-${pubkey}.lock`);\n mkdirSync(dir, { recursive: true });\n\n const payload = JSON.stringify(\n {\n pid,\n pubkey,\n createdAt: new Date().toISOString(),\n } satisfies LockFileContents,\n null,\n 2\n );\n\n // Two attempts: initial exclusive create, then one retry after reclaiming\n // a stale lock. A live holder on either attempt is a hard refusal.\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n writeFileSync(lockPath, payload, { flag: 'wx' });\n return new NonceLock(pubkey, lockPath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;\n\n let holderPid: number | undefined;\n try {\n const parsed = JSON.parse(\n readFileSync(lockPath, 'utf8')\n ) as Partial<LockFileContents>;\n if (typeof parsed.pid === 'number') holderPid = parsed.pid;\n } catch {\n // Unreadable/corrupt lock → treat as stale.\n }\n\n // Same process re-acquiring (e.g. a retried push in one CLI run) is\n // not a race — the ChannelManager watermark is shared in-process.\n if (\n holderPid !== undefined &&\n holderPid !== pid &&\n pidAlive(holderPid)\n ) {\n throw new StandaloneLockError(pubkey, lockPath, holderPid);\n }\n\n // Stale (dead pid / corrupt / our own pid): reclaim and retry.\n try {\n unlinkSync(lockPath);\n } catch {\n // Lost a reclaim race with another process — the retry's exclusive\n // create settles the winner.\n }\n }\n }\n // Both attempts hit EEXIST → another process is actively (re)creating it.\n throw new StandaloneLockError(pubkey, lockPath, -1);\n }\n\n /** Remove the lockfile and detach the exit hook. Idempotent. */\n release(): void {\n if (this.released) return;\n this.released = true;\n process.removeListener('exit', this.exitHandler);\n try {\n unlinkSync(this.lockPath);\n } catch {\n // already gone — fine\n }\n }\n}\n","/**\n * File-extension → MIME type derivation for permaweb site deploys (#368).\n *\n * Every `rig push` already stores a repo's raw blob bytes on Arweave; tagging\n * each blob upload with a `Content-Type` derived from its path turns a pushed\n * repo into a servable static site (a gateway returns `text/html` for\n * `index.html` instead of `application/octet-stream`). The store forwards the\n * kind:5094 `output` tag to the Turbo upload's `Content-Type` — verified\n * against `@toon-protocol/core`'s `buildBlobStorageRequest` (sets\n * `[\"output\", contentType]`) and its inverse parser (reads the `output` tag\n * back into `contentType`, defaulting to `application/octet-stream`).\n *\n * The map is deliberately small — the common static-site asset extensions —\n * and any unknown or extensionless path falls back to the octet-stream\n * default (the same default the store applies for an absent `output`), so\n * this never guesses wildly.\n */\n\n/** The universal fallback: the store's own default for an absent `output`. */\nexport const DEFAULT_CONTENT_TYPE = 'application/octet-stream';\n\n/**\n * Lowercased extension (no dot) → MIME type. Curated for static sites; extend\n * as real deploys need more. Anything absent falls back to octet-stream.\n */\nconst EXTENSION_MIME: ReadonlyMap<string, string> = new Map([\n // Markup / documents\n ['html', 'text/html'],\n ['htm', 'text/html'],\n ['xml', 'application/xml'],\n ['txt', 'text/plain'],\n ['md', 'text/markdown'],\n ['csv', 'text/csv'],\n ['pdf', 'application/pdf'],\n // Styles / scripts\n ['css', 'text/css'],\n ['js', 'text/javascript'],\n ['mjs', 'text/javascript'],\n ['cjs', 'text/javascript'],\n ['map', 'application/json'],\n ['json', 'application/json'],\n ['wasm', 'application/wasm'],\n // Fonts\n ['woff', 'font/woff'],\n ['woff2', 'font/woff2'],\n ['ttf', 'font/ttf'],\n ['otf', 'font/otf'],\n ['eot', 'application/vnd.ms-fontobject'],\n // Images\n ['png', 'image/png'],\n ['jpg', 'image/jpeg'],\n ['jpeg', 'image/jpeg'],\n ['gif', 'image/gif'],\n ['svg', 'image/svg+xml'],\n ['webp', 'image/webp'],\n ['avif', 'image/avif'],\n ['ico', 'image/x-icon'],\n ['bmp', 'image/bmp'],\n // Media\n ['mp3', 'audio/mpeg'],\n ['mp4', 'video/mp4'],\n ['webm', 'video/webm'],\n ['ogg', 'audio/ogg'],\n ['wav', 'audio/wav'],\n]);\n\n/** The lowercased extension of a path (no leading dot), or `undefined`. */\nfunction extensionOf(path: string): string | undefined {\n const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\\\'));\n const base = slash >= 0 ? path.slice(slash + 1) : path;\n const dot = base.lastIndexOf('.');\n // A leading dot (dotfile like `.gitignore`) or no dot → no usable extension.\n if (dot <= 0) return undefined;\n return base.slice(dot + 1).toLowerCase();\n}\n\n/**\n * Derive a `Content-Type` from a file path's extension. Unknown/extensionless\n * paths (and `undefined`) fall back to {@link DEFAULT_CONTENT_TYPE}.\n */\nexport function contentTypeForPath(path: string | undefined): string {\n if (!path) return DEFAULT_CONTENT_TYPE;\n const ext = extensionOf(path);\n if (ext === undefined) return DEFAULT_CONTENT_TYPE;\n return EXTENSION_MIME.get(ext) ?? DEFAULT_CONTENT_TYPE;\n}\n\n/**\n * Pick a single deterministic upload path for a blob reachable by MULTIPLE\n * paths (the same content committed under different names). The store keys a\n * blob by its content, so it carries ONE `Content-Type`:\n *\n * - all paths agree on a content type → the lexicographically-first path\n * (deterministic across runs);\n * - the paths DISAGREE (e.g. `a.js` and `a.txt` share bytes) → `undefined`,\n * so the caller uploads it as {@link DEFAULT_CONTENT_TYPE} rather than\n * silently favoring one extension over another.\n *\n * An empty list yields `undefined` (octet-stream).\n */\nexport function resolveConflictingPath(paths: string[]): string | undefined {\n if (paths.length === 0) return undefined;\n const sorted = [...paths].sort();\n const types = new Set(sorted.map((p) => contentTypeForPath(p)));\n if (types.size === 1) return sorted[0];\n return undefined;\n}\n"],"mappings":"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","names":["mkdirSync","readFileSync","writeFileSync","homedir","join"]}
|
package/dist/chunk-TTJCBGKW.js
DELETED
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
queryRelay
|
|
3
|
-
} from "./chunk-2KJJ7LSO.js";
|
|
4
|
-
|
|
5
|
-
// src/standalone/network-bootstrap.ts
|
|
6
|
-
import {
|
|
7
|
-
CHAIN_PRESETS,
|
|
8
|
-
GenesisPeerLoader,
|
|
9
|
-
isEventExpired,
|
|
10
|
-
parseIlpPeerInfo,
|
|
11
|
-
resolveClientNetwork
|
|
12
|
-
} from "@toon-protocol/core";
|
|
13
|
-
var ILP_PEER_INFO_KIND = 10032;
|
|
14
|
-
var DISCOVERY_TIMEOUT_MS = 5e3;
|
|
15
|
-
function parseRoutes(content) {
|
|
16
|
-
try {
|
|
17
|
-
const parsed = JSON.parse(content);
|
|
18
|
-
const routes = parsed.routes;
|
|
19
|
-
if (typeof routes !== "object" || routes === null) return void 0;
|
|
20
|
-
const { publish, store } = routes;
|
|
21
|
-
const out = {
|
|
22
|
-
...typeof publish === "string" && publish.length > 0 ? { publish } : {},
|
|
23
|
-
...typeof store === "string" && store.length > 0 ? { store } : {}
|
|
24
|
-
};
|
|
25
|
-
return out.publish || out.store ? out : void 0;
|
|
26
|
-
} catch {
|
|
27
|
-
return void 0;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function parseCapabilities(content) {
|
|
31
|
-
try {
|
|
32
|
-
const parsed = JSON.parse(content);
|
|
33
|
-
if (!Array.isArray(parsed.capabilities)) return void 0;
|
|
34
|
-
const out = [];
|
|
35
|
-
for (const entry of parsed.capabilities) {
|
|
36
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
37
|
-
const { capability, address, price } = entry;
|
|
38
|
-
if (typeof capability === "string" && typeof address === "string" && address.length > 0 && typeof price === "string" && /^\d+$/.test(price)) {
|
|
39
|
-
out.push({ capability, address, price });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return out.length > 0 ? out : void 0;
|
|
43
|
-
} catch {
|
|
44
|
-
return void 0;
|
|
45
|
-
}
|
|
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
|
-
}
|
|
83
|
-
async function discoverAnnouncedPeers(relayUrl, options = {}) {
|
|
84
|
-
const factory = options.webSocketFactory ?? defaultDiscoveryWebSocketFactory();
|
|
85
|
-
const events = await queryRelay(
|
|
86
|
-
relayUrl,
|
|
87
|
-
{ kinds: [ILP_PEER_INFO_KIND], limit: 100 },
|
|
88
|
-
options.timeoutMs ?? DISCOVERY_TIMEOUT_MS,
|
|
89
|
-
factory
|
|
90
|
-
);
|
|
91
|
-
const latestByAuthor = /* @__PURE__ */ new Map();
|
|
92
|
-
for (const event of events) {
|
|
93
|
-
if (event.kind !== ILP_PEER_INFO_KIND) continue;
|
|
94
|
-
const prev = latestByAuthor.get(event.pubkey);
|
|
95
|
-
if (!prev || event.created_at > prev.created_at) {
|
|
96
|
-
latestByAuthor.set(event.pubkey, event);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const peers = [];
|
|
100
|
-
for (const event of latestByAuthor.values()) {
|
|
101
|
-
if (isEventExpired(event)) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
let info;
|
|
105
|
-
try {
|
|
106
|
-
info = parseIlpPeerInfo(event);
|
|
107
|
-
} catch {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const routes = parseRoutes(event.content);
|
|
111
|
-
const capabilities = parseCapabilities(event.content);
|
|
112
|
-
const minaTokenIds = parseMinaTokenIds(event.content);
|
|
113
|
-
const chainRpcUrls = parseChainRpcUrls(event.content);
|
|
114
|
-
peers.push({
|
|
115
|
-
pubkey: event.pubkey,
|
|
116
|
-
info,
|
|
117
|
-
...routes ? { routes } : {},
|
|
118
|
-
...capabilities ? { capabilities } : {},
|
|
119
|
-
...minaTokenIds ? { minaTokenIds } : {},
|
|
120
|
-
...chainRpcUrls ? { chainRpcUrls } : {},
|
|
121
|
-
createdAt: event.created_at
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
return peers;
|
|
125
|
-
}
|
|
126
|
-
function defaultDiscoveryWebSocketFactory() {
|
|
127
|
-
return (url) => {
|
|
128
|
-
const ctor = globalThis.WebSocket;
|
|
129
|
-
if (!ctor) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
"No global WebSocket constructor (Node >= 22 required) for announce discovery"
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
return new ctor(url);
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
function pickPaymentPeer(peers, seedPubkeys) {
|
|
138
|
-
const seeded = peers.filter((p) => seedPubkeys.includes(p.pubkey));
|
|
139
|
-
if (seeded.length > 0) {
|
|
140
|
-
return seeded.sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
141
|
-
}
|
|
142
|
-
const payable = peers.filter(
|
|
143
|
-
(p) => (p.info.httpEndpoint || p.info.btpEndpoint) && p.info.settlementAddresses && Object.keys(p.info.settlementAddresses).length > 0
|
|
144
|
-
);
|
|
145
|
-
if (payable.length === 0) return void 0;
|
|
146
|
-
const publishEdges = payable.filter(
|
|
147
|
-
(p) => p.routes?.publish !== void 0 && p.routes.publish === p.info.ilpAddress
|
|
148
|
-
);
|
|
149
|
-
const pool = publishEdges.length > 0 ? publishEdges : payable;
|
|
150
|
-
return pool.sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
151
|
-
}
|
|
152
|
-
function evmChainIdOf(chain) {
|
|
153
|
-
const parts = chain.split(":");
|
|
154
|
-
if (parts[0] !== "evm") return void 0;
|
|
155
|
-
const raw = parts.length >= 3 ? parts[2] : parts[1];
|
|
156
|
-
const id = Number.parseInt(raw ?? "", 10);
|
|
157
|
-
return Number.isNaN(id) ? void 0 : id;
|
|
158
|
-
}
|
|
159
|
-
function evmPresetForChain(chain) {
|
|
160
|
-
const id = evmChainIdOf(chain);
|
|
161
|
-
if (id === void 0) return void 0;
|
|
162
|
-
for (const preset of Object.values(CHAIN_PRESETS)) {
|
|
163
|
-
if (preset.chainId === id) {
|
|
164
|
-
return {
|
|
165
|
-
rpcUrl: preset.rpcUrl,
|
|
166
|
-
usdcAddress: preset.usdcAddress,
|
|
167
|
-
tokenNetworkAddress: preset.tokenNetworkAddress
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return void 0;
|
|
172
|
-
}
|
|
173
|
-
function solanaPresetForChain(chain) {
|
|
174
|
-
const [family, cluster] = chain.split(":");
|
|
175
|
-
if (family !== "solana" || !cluster) return void 0;
|
|
176
|
-
const tier = cluster === "mainnet-beta" ? "mainnet" : cluster === "devnet" ? "devnet" : cluster === "testnet" ? "testnet" : void 0;
|
|
177
|
-
if (!tier) return void 0;
|
|
178
|
-
const presets = resolveClientNetwork(tier);
|
|
179
|
-
const rpcUrl = presets.chainRpcUrls[chain];
|
|
180
|
-
const tokenMint = presets.preferredTokens[chain];
|
|
181
|
-
const programId = presets.solanaChannel && presets.supportedChains.includes(chain) ? presets.solanaChannel.programId : void 0;
|
|
182
|
-
if (!rpcUrl && !tokenMint && !programId) return void 0;
|
|
183
|
-
return {
|
|
184
|
-
...rpcUrl ? { rpcUrl } : {},
|
|
185
|
-
...tokenMint ? { tokenMint } : {},
|
|
186
|
-
...programId ? { programId } : {}
|
|
187
|
-
};
|
|
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
|
-
}
|
|
205
|
-
function resolveChainSettlement(chain, explicit, announce) {
|
|
206
|
-
const family = chain.split(":")[0] ?? chain;
|
|
207
|
-
const evmPreset = family === "evm" ? evmPresetForChain(chain) : void 0;
|
|
208
|
-
const solPreset = family === "solana" ? solanaPresetForChain(chain) : void 0;
|
|
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;
|
|
216
|
-
const tokenAddress = explicit.preferredTokens?.[chain] ?? announce?.info.preferredTokens?.[chain] ?? (evmPreset?.usdcAddress || void 0) ?? solPreset?.tokenMint;
|
|
217
|
-
const tokenNetwork = family === "evm" ? explicit.tokenNetworks?.[chain] ?? announce?.info.tokenNetworks?.[chain] ?? (evmPreset?.tokenNetworkAddress || void 0) : void 0;
|
|
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;
|
|
222
|
-
return {
|
|
223
|
-
chain,
|
|
224
|
-
family,
|
|
225
|
-
...rpcUrl ? { rpcUrl } : {},
|
|
226
|
-
...tokenAddress ? { tokenAddress } : {},
|
|
227
|
-
...tokenNetwork ? { tokenNetwork } : {},
|
|
228
|
-
...programId ? { programId } : {},
|
|
229
|
-
...zkAppAddress ? { zkAppAddress } : {},
|
|
230
|
-
...minaTokenId ? { minaTokenId } : {},
|
|
231
|
-
...networkId ? { networkId } : {}
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
var TokenNetworkUnderivableError = class extends Error {
|
|
235
|
-
constructor(chain, announce, relayUrl) {
|
|
236
|
-
const announceRef = announce ? `the kind:10032 announce from ${announce.pubkey.slice(0, 16)}\u2026 on ${relayUrl}` : `no kind:10032 announce was found on ${relayUrl}`;
|
|
237
|
-
super(
|
|
238
|
-
`cannot derive the TokenNetwork contract for settlement chain "${chain}": ${announceRef} carries no tokenNetworks["${chain}"], and no built-in chain preset matches its chain id \u2014 add tokenNetworks["${chain}"] to the client config (or pick another chain via TOON_CLIENT_CHAIN / the chain config field)`
|
|
239
|
-
);
|
|
240
|
-
this.name = "TokenNetworkUnderivableError";
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
var SolanaChannelUnderivableError = class extends Error {
|
|
244
|
-
constructor(chain, missing, announce, relayUrl) {
|
|
245
|
-
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}`;
|
|
246
|
-
super(
|
|
247
|
-
`cannot derive the Solana channel parameters for settlement chain "${chain}" (missing: ${missing.join(", ")}): ${announceRef}, and no built-in preset covers this cluster \u2014 add a solanaChannel { rpcUrl, programId, tokenMint } object to the client config (or set tokenNetworks["${chain}"] = <program id>, preferredTokens["${chain}"] = <mint>, chainRpcUrls["${chain}"]), or pick another chain via TOON_CLIENT_CHAIN / the chain config field`
|
|
248
|
-
);
|
|
249
|
-
this.name = "SolanaChannelUnderivableError";
|
|
250
|
-
}
|
|
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
|
-
};
|
|
261
|
-
async function selectSettlementChain(options) {
|
|
262
|
-
const { explicitChain, announcedChains } = options;
|
|
263
|
-
if (explicitChain) {
|
|
264
|
-
if (explicitChain.includes(":")) {
|
|
265
|
-
if (!announcedChains.includes(explicitChain)) {
|
|
266
|
-
const chainId = evmChainIdOf(explicitChain);
|
|
267
|
-
const equivalent = chainId !== void 0 ? announcedChains.find((c) => evmChainIdOf(c) === chainId) : void 0;
|
|
268
|
-
if (equivalent) {
|
|
269
|
-
return {
|
|
270
|
-
chain: equivalent,
|
|
271
|
-
reason: "explicit",
|
|
272
|
-
detail: `chain ${explicitChain} set by config \u2192 announced spelling ${equivalent} (same EVM chain id ${chainId})`
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return {
|
|
277
|
-
chain: explicitChain,
|
|
278
|
-
reason: "explicit",
|
|
279
|
-
detail: `chain ${explicitChain} set by config`
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
const familyMatch = announcedChains.find(
|
|
283
|
-
(c) => (c.split(":")[0] ?? c) === explicitChain
|
|
284
|
-
);
|
|
285
|
-
if (!familyMatch) {
|
|
286
|
-
throw new Error(
|
|
287
|
-
`configured chain family "${explicitChain}" is not announced by the payment peer (announced: ${announcedChains.join(", ") || "none"}) \u2014 set a full chain id (e.g. "evm:31337") to force it`
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
return {
|
|
291
|
-
chain: familyMatch,
|
|
292
|
-
reason: "explicit",
|
|
293
|
-
detail: `chain family "${explicitChain}" set by config \u2192 ${familyMatch}`
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
const live = (options.records ?? []).filter((r) => !r.closed && announcedChains.includes(r.chain)).sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));
|
|
297
|
-
const persisted = live[0];
|
|
298
|
-
if (persisted) {
|
|
299
|
-
return {
|
|
300
|
-
chain: persisted.chain,
|
|
301
|
-
reason: "persisted-channel",
|
|
302
|
-
detail: `existing payment channel on ${persisted.chain} (rig channel map)`
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
const evmChains = announcedChains.filter((c) => c.startsWith("evm:"));
|
|
306
|
-
for (const chain of announcedChains) {
|
|
307
|
-
const family = chain.split(":")[0];
|
|
308
|
-
let probe;
|
|
309
|
-
let owner;
|
|
310
|
-
if (family === "evm") {
|
|
311
|
-
probe = options.probeBalance;
|
|
312
|
-
owner = options.evmAddress;
|
|
313
|
-
} else if (family === "solana") {
|
|
314
|
-
probe = options.probeSolanaBalance;
|
|
315
|
-
owner = options.solanaAddress;
|
|
316
|
-
}
|
|
317
|
-
if (!probe || !owner) continue;
|
|
318
|
-
const settlement = options.resolveSettlement(chain);
|
|
319
|
-
if (!settlement.rpcUrl || !settlement.tokenAddress) continue;
|
|
320
|
-
try {
|
|
321
|
-
const balance = await probe({
|
|
322
|
-
rpcUrl: settlement.rpcUrl,
|
|
323
|
-
tokenAddress: settlement.tokenAddress,
|
|
324
|
-
owner
|
|
325
|
-
});
|
|
326
|
-
if (balance > 0n) {
|
|
327
|
-
return {
|
|
328
|
-
chain,
|
|
329
|
-
reason: "funded",
|
|
330
|
-
detail: `wallet holds ${balance} token base units on ${chain}`
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
} catch {
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
const fallback = evmChains[0] ?? announcedChains[0];
|
|
337
|
-
if (!fallback) {
|
|
338
|
-
throw new Error(
|
|
339
|
-
"the payment peer announces no settlement chains \u2014 cannot select a chain for paid writes (set supportedChains/chain in the client config)"
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
return {
|
|
343
|
-
chain: fallback,
|
|
344
|
-
reason: "default",
|
|
345
|
-
detail: evmChains[0] ? `first EVM chain announced by the payment peer` : `first chain announced by the payment peer (no EVM chain announced)`
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
var BALANCE_OF_SELECTOR = "0x70a08231";
|
|
349
|
-
async function evmTokenBalance(args) {
|
|
350
|
-
const fetchImpl = args.fetchImpl ?? fetch;
|
|
351
|
-
const owner = args.owner.replace(/^0x/, "").toLowerCase().padStart(64, "0");
|
|
352
|
-
const controller = new AbortController();
|
|
353
|
-
const timer = setTimeout(() => controller.abort(), args.timeoutMs ?? 5e3);
|
|
354
|
-
try {
|
|
355
|
-
const res = await fetchImpl(args.rpcUrl, {
|
|
356
|
-
method: "POST",
|
|
357
|
-
headers: { "content-type": "application/json" },
|
|
358
|
-
body: JSON.stringify({
|
|
359
|
-
jsonrpc: "2.0",
|
|
360
|
-
id: 1,
|
|
361
|
-
method: "eth_call",
|
|
362
|
-
params: [
|
|
363
|
-
{ to: args.tokenAddress, data: `${BALANCE_OF_SELECTOR}${owner}` },
|
|
364
|
-
"latest"
|
|
365
|
-
]
|
|
366
|
-
}),
|
|
367
|
-
signal: controller.signal
|
|
368
|
-
});
|
|
369
|
-
if (!res.ok) {
|
|
370
|
-
throw new Error(`eth_call failed: HTTP ${res.status}`);
|
|
371
|
-
}
|
|
372
|
-
const body = await res.json();
|
|
373
|
-
if (typeof body.result !== "string") {
|
|
374
|
-
throw new Error(`eth_call failed: ${body.error?.message ?? "no result"}`);
|
|
375
|
-
}
|
|
376
|
-
return BigInt(body.result === "0x" ? "0x0" : body.result);
|
|
377
|
-
} finally {
|
|
378
|
-
clearTimeout(timer);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
async function solanaTokenBalance(args) {
|
|
382
|
-
const fetchImpl = args.fetchImpl ?? fetch;
|
|
383
|
-
const controller = new AbortController();
|
|
384
|
-
const timer = setTimeout(() => controller.abort(), args.timeoutMs ?? 5e3);
|
|
385
|
-
try {
|
|
386
|
-
const res = await fetchImpl(args.rpcUrl, {
|
|
387
|
-
method: "POST",
|
|
388
|
-
headers: { "content-type": "application/json" },
|
|
389
|
-
body: JSON.stringify({
|
|
390
|
-
jsonrpc: "2.0",
|
|
391
|
-
id: 1,
|
|
392
|
-
method: "getTokenAccountsByOwner",
|
|
393
|
-
params: [
|
|
394
|
-
args.owner,
|
|
395
|
-
{ mint: args.tokenAddress },
|
|
396
|
-
{ encoding: "jsonParsed" }
|
|
397
|
-
]
|
|
398
|
-
}),
|
|
399
|
-
signal: controller.signal
|
|
400
|
-
});
|
|
401
|
-
if (!res.ok) {
|
|
402
|
-
throw new Error(`getTokenAccountsByOwner failed: HTTP ${res.status}`);
|
|
403
|
-
}
|
|
404
|
-
const body = await res.json();
|
|
405
|
-
const accounts = body.result?.value;
|
|
406
|
-
if (!Array.isArray(accounts)) {
|
|
407
|
-
throw new Error(
|
|
408
|
-
`getTokenAccountsByOwner failed: ${body.error?.message ?? "no result"}`
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
let total = 0n;
|
|
412
|
-
for (const account of accounts) {
|
|
413
|
-
const amount = account?.account?.data?.parsed?.info?.tokenAmount?.amount;
|
|
414
|
-
if (typeof amount === "string") total += BigInt(amount);
|
|
415
|
-
}
|
|
416
|
-
return total;
|
|
417
|
-
} finally {
|
|
418
|
-
clearTimeout(timer);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
function loadGenesisSeed() {
|
|
422
|
-
return GenesisPeerLoader.loadGenesisPeers()[0];
|
|
423
|
-
}
|
|
424
|
-
function genesisSeedPubkeys() {
|
|
425
|
-
return GenesisPeerLoader.loadGenesisPeers().map((p) => p.pubkey);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export {
|
|
429
|
-
ILP_PEER_INFO_KIND,
|
|
430
|
-
DISCOVERY_TIMEOUT_MS,
|
|
431
|
-
discoverAnnouncedPeers,
|
|
432
|
-
pickPaymentPeer,
|
|
433
|
-
evmChainIdOf,
|
|
434
|
-
evmPresetForChain,
|
|
435
|
-
solanaPresetForChain,
|
|
436
|
-
minaPresetForChain,
|
|
437
|
-
resolveChainSettlement,
|
|
438
|
-
TokenNetworkUnderivableError,
|
|
439
|
-
SolanaChannelUnderivableError,
|
|
440
|
-
MinaChannelUnderivableError,
|
|
441
|
-
selectSettlementChain,
|
|
442
|
-
evmTokenBalance,
|
|
443
|
-
solanaTokenBalance,
|
|
444
|
-
loadGenesisSeed,
|
|
445
|
-
genesisSeedPubkeys
|
|
446
|
-
};
|
|
447
|
-
//# sourceMappingURL=chunk-TTJCBGKW.js.map
|