@toon-protocol/client-mcp 0.31.2 → 0.33.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/daemon/first-run.ts","../src/relay-subscription.ts","../src/daemon/client-runner.ts","../../sdk/dist/chunk-UP2VWCW5.js","../../sdk/src/errors.ts","../../sdk/src/identity.ts","../../sdk/src/gift-wrap.ts","../../sdk/src/swap-handler.ts","../../sdk/src/stream-swap.ts","../src/daemon/apex-channel-store.ts","../src/daemon/targets-store.ts","../src/daemon/apex-discovery.ts","../src/daemon/routes.ts"],"sourcesContent":["/**\n * First-run onboarding for `toon-clientd`.\n *\n * A brand-new user (`npx`/plugin install) has no identity and no config file.\n * Before the daemon resolves its config it calls {@link scaffoldFirstRun},\n * which makes a fresh install start with zero manual setup:\n *\n * 1. **Identity** — if no mnemonic source is configured (no\n * `TOON_CLIENT_MNEMONIC`, no `keystorePath`, no `mnemonic`), generate a\n * fresh BIP-39 mnemonic, encrypt it to `~/.toon-client/keystore.json`, and\n * record `keystorePath` (+ `keystoreAutoPassword`) in `config.json`. The\n * keystore is encrypted with `TOON_CLIENT_KEYSTORE_PASSWORD` when set, else\n * a default password so the identity survives restarts with no env var.\n * The mnemonic + derived addresses are printed ONCE for backup.\n * 2. **Transport scaffolding** — ensure `config.json` carries the transport\n * knobs (`btpUrl`/`relayUrl`/`managedAnonProxy`) plus a `_help` block\n * documenting direct-vs-`.anyone` selection, so the user can see what to\n * point at.\n *\n * This is all idempotent: on later runs an identity already exists, so nothing\n * is regenerated and the config is left untouched.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { deriveFullIdentity, generateKeystore } from '@toon-protocol/client';\nimport {\n configDir,\n defaultConfigPath,\n readConfigFile,\n DEFAULT_KEYSTORE_PASSWORD,\n type DaemonConfigFile,\n} from './config.js';\n\n/** Default keystore path: `~/.toon-client/keystore.json`. */\nexport function defaultKeystorePath(): string {\n return join(configDir(), 'keystore.json');\n}\n\n/**\n * True when SOME mnemonic source is already configured: the env var, a keystore\n * path, or an inline mnemonic. When false the daemon would otherwise hard-fail\n * with \"No mnemonic configured\".\n */\nexport function hasConfiguredIdentity(file: DaemonConfigFile): boolean {\n return Boolean(\n process.env['TOON_CLIENT_MNEMONIC']?.trim() ||\n file.keystorePath ||\n file.mnemonic\n );\n}\n\n/** The `_help` block written into a scaffolded config to document transport. */\nconst CONFIG_HELP = {\n transport:\n 'Point btpUrl/relayUrl at your apex. For an .anyone hidden service use ' +\n 'ws://<host>.anyone:3000/btp + ws://<host>.anyone:7100 — the daemon ' +\n 'auto-routes through a managed anon proxy. For a direct apex use ' +\n 'ws://<host>:3000/btp + ws://<host>:7100.',\n btpUrl: 'REQUIRED. BTP WebSocket URL of the apex/connector for paid writes.',\n relayUrl:\n 'Town relay WebSocket URL for FREE reads. Default ws://localhost:7100.',\n managedAnonProxy:\n 'Optional override — leave unset to auto-infer from btpUrl (.anyone host → ' +\n 'managed anon proxy, anything else → direct).',\n keystorePath:\n 'Auto-generated encrypted identity. Do not hand-edit; back up your seed phrase.',\n};\n\ninterface ScaffoldOptions {\n /** Config file path (defaults to `TOON_CLIENT_CONFIG` / `~/.toon-client/config.json`). */\n configPath?: string;\n /** Log sink (defaults to stderr, since stdout may carry MCP/JSON). */\n log?: (msg: string) => void;\n}\n\n/**\n * Generate + persist an identity and/or scaffold transport config on first run.\n * Safe to call on every startup — it only acts when something is missing.\n */\nexport async function scaffoldFirstRun(\n opts: ScaffoldOptions = {}\n): Promise<void> {\n const log = opts.log ?? ((m: string): void => console.error(m));\n const configPath =\n opts.configPath ?? process.env['TOON_CLIENT_CONFIG'] ?? defaultConfigPath();\n\n // Ensure the config directory exists before writing the keystore/config.\n mkdirSync(dirname(configPath), { recursive: true });\n\n const file = readConfigFile(configPath);\n let updated: DaemonConfigFile = { ...file };\n let changed = false;\n\n // --- 1. Identity ----------------------------------------------------------\n if (!hasConfiguredIdentity(file)) {\n const keystorePath = defaultKeystorePath();\n const envPassword = process.env['TOON_CLIENT_KEYSTORE_PASSWORD'];\n const password = envPassword ?? DEFAULT_KEYSTORE_PASSWORD;\n\n // Reuse an existing keystore file if one is already on disk (e.g. config\n // was deleted but the keystore survived); otherwise mint a new mnemonic.\n let mnemonic: string;\n if (existsSync(keystorePath)) {\n // Leave the existing keystore untouched; just relink it in config.\n mnemonic = '';\n log(\n `[toon-clientd] first run: relinking existing keystore at ${keystorePath}`\n );\n } else {\n const generated = generateKeystore(keystorePath, password);\n mnemonic = generated.mnemonic;\n }\n\n updated = {\n ...updated,\n keystorePath,\n // Only flag auto-password when WE chose it, so a user-imported keystore\n // (custom password) still requires the env var.\n ...(envPassword ? {} : { keystoreAutoPassword: true }),\n };\n changed = true;\n\n if (mnemonic) {\n await printNewIdentity(mnemonic, keystorePath, Boolean(envPassword), log);\n }\n }\n\n // --- 2. Transport scaffolding --------------------------------------------\n if (!existsSync(configPath)) {\n // Fresh install: surface the transport knobs with guidance. We deliberately\n // do NOT write a concrete `managedAnonProxy` — leaving it unset lets\n // `resolveConfig` infer the transport from the btpUrl hostname (`.anyone` →\n // managed anon proxy, anything else → direct). Writing `false` here would\n // short-circuit that inference and break a later `.anyone` btpUrl.\n updated = {\n _help: CONFIG_HELP,\n btpUrl: '',\n relayUrl: 'ws://localhost:7100',\n ...updated,\n } as DaemonConfigFile;\n changed = true;\n log(\n `[toon-clientd] wrote starter config at ${configPath} — set \"btpUrl\" to your apex before publishing.`\n );\n }\n\n if (changed) writeConfigFile(configPath, updated);\n}\n\n/** Derive + print the new identity's addresses and a one-time backup notice. */\nasync function printNewIdentity(\n mnemonic: string,\n keystorePath: string,\n hasEnvPassword: boolean,\n log: (msg: string) => void\n): Promise<void> {\n const id = await deriveFullIdentity(mnemonic);\n const lines = [\n '',\n '════════════════════════════════════════════════════════════════',\n ' TOON client: generated a new identity (first run)',\n '════════════════════════════════════════════════════════════════',\n ` Nostr pubkey : ${id.nostr.pubkey}`,\n ` EVM address : ${id.evm.address}`,\n ...(id.solana.publicKey ? [` Solana : ${id.solana.publicKey}`] : []),\n ...(id.mina.publicKey ? [` Mina : ${id.mina.publicKey}`] : []),\n '',\n ' Seed phrase (BACK THIS UP — shown only once):',\n ` ${mnemonic}`,\n '',\n ` Encrypted keystore: ${keystorePath}`,\n hasEnvPassword\n ? ' Encrypted with TOON_CLIENT_KEYSTORE_PASSWORD.'\n : ' Encrypted with the default password (set TOON_CLIENT_KEYSTORE_PASSWORD\\n' +\n ' + re-import to use your own). Identity reloads automatically on restart.',\n '════════════════════════════════════════════════════════════════',\n '',\n ];\n log(lines.join('\\n'));\n}\n\n/** Write the config file as pretty JSON with mode 0o600. */\nfunction writeConfigFile(path: string, file: DaemonConfigFile): void {\n writeFileSync(path, JSON.stringify(file, null, 2) + '\\n', {\n encoding: 'utf8',\n mode: 0o600,\n });\n}\n","/**\n * Persistent town-relay Nostr-WS subscription — the read half of the TOON\n * client, which `@toon-protocol/client` does not provide (its bootstrap only\n * issues one-shot WS queries and `DiscoveryTracker` is passive).\n *\n * Reads are FREE: this opens a long-lived NIP-01 connection to the town relay,\n * keeps a bounded ring buffer of received events (de-duplicated by `event.id`),\n * and lets callers drain new events via a monotonic cursor. It auto-reconnects\n * with exponential backoff and re-issues every active REQ on reconnect.\n *\n * The WebSocket is injectable (`wsFactory`) so unit tests can drive the wire\n * protocol without a real relay; the default factory uses the `ws` package and,\n * when a `socks5h://` proxy is configured, routes through it so `.anyone`\n * hidden-service relays are reachable.\n */\n\nimport { createRequire } from 'node:module';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { NostrFilter } from './control-api.js';\n\n// ESM has no `require`; synchronously load the node-only `ws`/`socks-proxy-agent`\n// deps for the default WebSocket factory without forcing every caller (e.g. the\n// browser-style injected-factory path in tests) to pull them in.\nconst nodeRequire = createRequire(import.meta.url);\n\n/** Minimal WebSocket surface this module depends on (subset of `ws`). */\nexport interface MinimalWebSocket {\n send(data: string): void;\n close(): void;\n on(event: 'open' | 'close', cb: () => void): void;\n on(event: 'message', cb: (data: unknown) => void): void;\n on(event: 'error', cb: (err: unknown) => void): void;\n}\n\nexport type WebSocketFactory = (url: string) => MinimalWebSocket;\n\nexport interface RelaySubscriptionOptions {\n /** Town relay WS URL, e.g. `wss://<host>.anyone/` or `ws://localhost:7100`. */\n relayUrl: string;\n /** Optional `socks5h://host:port` proxy for `.anyone` relays. */\n socksProxy?: string;\n /** Max events retained in the ring buffer (oldest evicted). Default 5000. */\n bufferSize?: number;\n /** Base reconnect delay, ms. Default 1000. */\n reconnectBaseMs?: number;\n /** Max reconnect delay, ms. Default 30000. */\n reconnectMaxMs?: number;\n /** Inject a WebSocket factory (tests / proxy customisation). */\n wsFactory?: WebSocketFactory;\n /**\n * Decode an `EVENT` payload that arrived as a string. The TOON relay sends\n * events TOON-encoded (key/value text) rather than as a JSON object, so the\n * daemon injects a TOON decoder here. When the payload is already a JSON\n * object it is used directly and this is not called.\n */\n decodeEvent?: (raw: string) => NostrEvent;\n /**\n * Invoked once per newly-buffered (de-duplicated) event. The daemon uses this\n * to feed a runner-level MERGED buffer across many relays — so a fan-out read\n * (`toon_read` with no relayUrl) draws from one ordered stream with a single\n * scalar cursor. The relay still keeps its own buffer for `bufferedCount` /\n * single-relay drains.\n */\n onEvent?: (subId: string, event: NostrEvent) => void;\n /** Optional logger. */\n logger?: (msg: string) => void;\n}\n\ninterface BufferedEvent {\n seq: number;\n subId: string;\n event: NostrEvent;\n}\n\n/** Result of {@link RelaySubscription.getEvents}. */\nexport interface DrainResult {\n events: NostrEvent[];\n cursor: number;\n hasMore: boolean;\n}\n\nconst DEFAULT_BUFFER = 5000;\nconst DEFAULT_BASE_MS = 1000;\nconst DEFAULT_MAX_MS = 30_000;\n\n/** Shared no-op used as the default logger. */\nconst noop = (): void => undefined;\n\nexport class RelaySubscription {\n private readonly relayUrl: string;\n private readonly socksProxy?: string;\n private readonly bufferSize: number;\n private readonly reconnectBaseMs: number;\n private readonly reconnectMaxMs: number;\n private readonly log: (msg: string) => void;\n private readonly wsFactory: WebSocketFactory;\n private readonly decodeEvent?: (raw: string) => NostrEvent;\n private readonly onEvent?: (subId: string, event: NostrEvent) => void;\n\n /** Active subscriptions: subId -> filters (re-sent on every (re)connect). */\n private readonly subscriptions = new Map<string, NostrFilter[]>();\n\n /** Ring buffer of received events, ordered by ascending `seq`. */\n private buffer: BufferedEvent[] = [];\n /** De-dup index: event.id -> seq (kept in lockstep with the buffer). */\n private readonly seen = new Set<string>();\n private seqCounter = 0;\n private subIdCounter = 0;\n\n private ws: MinimalWebSocket | null = null;\n private connected = false;\n private closing = false;\n private reconnectAttempts = 0;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(opts: RelaySubscriptionOptions) {\n this.relayUrl = opts.relayUrl;\n this.socksProxy = opts.socksProxy;\n this.bufferSize = opts.bufferSize ?? DEFAULT_BUFFER;\n this.reconnectBaseMs = opts.reconnectBaseMs ?? DEFAULT_BASE_MS;\n this.reconnectMaxMs = opts.reconnectMaxMs ?? DEFAULT_MAX_MS;\n this.log = opts.logger ?? noop;\n this.wsFactory = opts.wsFactory ?? defaultWebSocketFactory(this.socksProxy);\n this.decodeEvent = opts.decodeEvent;\n this.onEvent = opts.onEvent;\n }\n\n /** Whether the underlying socket is currently open. */\n isConnected(): boolean {\n return this.connected;\n }\n\n /** Number of events currently held in the buffer. */\n bufferedCount(): number {\n return this.buffer.length;\n }\n\n /** Active subscription ids. */\n activeSubscriptions(): string[] {\n return [...this.subscriptions.keys()];\n }\n\n /** Open the connection (idempotent). */\n start(): void {\n this.closing = false;\n if (this.ws) return;\n this.open();\n }\n\n /**\n * Register a persistent subscription and (if connected) send the REQ.\n * Returns the subscription id (caller-supplied or generated).\n */\n subscribe(filters: NostrFilter | NostrFilter[], subId?: string): string {\n const id = subId ?? `sub-${++this.subIdCounter}`;\n const list = Array.isArray(filters) ? filters : [filters];\n this.subscriptions.set(id, list);\n if (this.connected) this.sendReq(id, list);\n return id;\n }\n\n /** Cancel a subscription and send CLOSE if connected. */\n unsubscribe(subId: string): void {\n if (!this.subscriptions.delete(subId)) return;\n if (this.connected) this.sendRaw(['CLOSE', subId]);\n }\n\n /**\n * Drain events newer than `cursor`. The cursor is the highest `seq` returned;\n * pass it back to fetch only events received since. Filtering by `subId`\n * restricts to one subscription.\n */\n getEvents(\n opts: { subId?: string; cursor?: number; limit?: number } = {}\n ): DrainResult {\n const after = opts.cursor ?? 0;\n const limit = opts.limit ?? 200;\n const matches = this.buffer.filter(\n (b) =>\n b.seq > after && (opts.subId === undefined || b.subId === opts.subId)\n );\n const page = matches.slice(0, limit);\n const hasMore = matches.length > page.length;\n const last = page.at(-1);\n const cursor = last ? last.seq : after;\n return { events: page.map((b) => b.event), cursor, hasMore };\n }\n\n /** Close the connection permanently and stop reconnecting. */\n close(): void {\n this.closing = true;\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n if (this.ws) {\n try {\n this.ws.close();\n } catch {\n /* ignore */\n }\n this.ws = null;\n }\n this.connected = false;\n }\n\n // ── internals ────────────────────────────────────────────────────────────\n\n private open(): void {\n let ws: MinimalWebSocket;\n try {\n ws = this.wsFactory(this.relayUrl);\n } catch (err) {\n this.log(`[relay] connect failed: ${errMsg(err)}`);\n this.scheduleReconnect();\n return;\n }\n this.ws = ws;\n\n ws.on('open', () => {\n this.connected = true;\n this.reconnectAttempts = 0;\n this.log(`[relay] connected to ${this.relayUrl}`);\n // Re-issue every active subscription.\n for (const [id, filters] of this.subscriptions) this.sendReq(id, filters);\n });\n\n ws.on('message', (data: unknown) => this.handleMessage(data));\n\n ws.on('error', (err: unknown) => {\n this.log(`[relay] socket error: ${errMsg(err)}`);\n });\n\n ws.on('close', () => {\n this.connected = false;\n this.ws = null;\n if (!this.closing) {\n this.log('[relay] disconnected; scheduling reconnect');\n this.scheduleReconnect();\n }\n });\n }\n\n private scheduleReconnect(): void {\n if (this.closing || this.reconnectTimer) return;\n const delay = Math.min(\n this.reconnectMaxMs,\n this.reconnectBaseMs * 2 ** this.reconnectAttempts\n );\n this.reconnectAttempts += 1;\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n if (!this.closing) this.open();\n }, delay);\n // Don't keep the event loop alive solely for a reconnect timer.\n (this.reconnectTimer as { unref?: () => void }).unref?.();\n }\n\n private sendReq(subId: string, filters: NostrFilter[]): void {\n this.sendRaw(['REQ', subId, ...filters]);\n }\n\n private sendRaw(message: unknown[]): void {\n if (!this.ws || !this.connected) return;\n try {\n this.ws.send(JSON.stringify(message));\n } catch (err) {\n this.log(`[relay] send failed: ${errMsg(err)}`);\n }\n }\n\n private handleMessage(data: unknown): void {\n let parsed: unknown;\n try {\n parsed = JSON.parse(toText(data));\n } catch {\n return;\n }\n if (!Array.isArray(parsed) || parsed.length === 0) return;\n const type = parsed[0];\n switch (type) {\n case 'EVENT': {\n const subId = typeof parsed[1] === 'string' ? parsed[1] : '';\n const event = this.parseEventPayload(parsed[2]);\n if (event && typeof event.id === 'string')\n this.bufferEvent(subId, event);\n break;\n }\n case 'EOSE':\n // End of stored events — nothing to do; we keep streaming live events.\n break;\n case 'CLOSED':\n this.log(\n `[relay] subscription closed by relay: ${String(parsed[2] ?? '')}`\n );\n break;\n case 'NOTICE':\n this.log(`[relay] NOTICE: ${String(parsed[1] ?? '')}`);\n break;\n default:\n break;\n }\n }\n\n /**\n * Normalise an `EVENT` payload to a NostrEvent. Standard relays send a JSON\n * object; the TOON relay sends a TOON-encoded string, decoded via the injected\n * `decodeEvent`. Returns undefined when it can't be parsed.\n */\n private parseEventPayload(raw: unknown): NostrEvent | undefined {\n if (\n raw &&\n typeof raw === 'object' &&\n typeof (raw as NostrEvent).id === 'string'\n ) {\n return raw as NostrEvent;\n }\n if (typeof raw === 'string' && this.decodeEvent) {\n try {\n return this.decodeEvent(raw);\n } catch (err) {\n this.log(`[relay] event decode failed: ${errMsg(err)}`);\n return undefined;\n }\n }\n return undefined;\n }\n\n private bufferEvent(subId: string, event: NostrEvent): void {\n if (this.seen.has(event.id)) return; // de-dup by event.id\n this.seen.add(event.id);\n this.buffer.push({ seq: ++this.seqCounter, subId, event });\n if (this.buffer.length > this.bufferSize) {\n const evicted = this.buffer.shift();\n if (evicted) this.seen.delete(evicted.event.id);\n }\n // Mirror into the runner-level merged buffer (cross-relay fan-out reads).\n this.onEvent?.(subId, event);\n }\n}\n\nfunction toText(data: unknown): string {\n if (typeof data === 'string') return data;\n if (data instanceof Uint8Array) return Buffer.from(data).toString('utf8');\n if (Buffer.isBuffer(data)) return data.toString('utf8');\n return String(data);\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * Default factory backed by the `ws` package. When a `socks5h://` proxy is\n * provided, routes the WebSocket through it via `socks-proxy-agent` (optional\n * dependency, loaded lazily so the bundle works without it).\n */\nfunction defaultWebSocketFactory(socksProxy?: string): WebSocketFactory {\n return (url: string): MinimalWebSocket => {\n const WebSocketImpl = nodeRequire('ws') as new (\n address: string,\n opts?: { agent?: unknown }\n ) => MinimalWebSocket;\n let agent: unknown;\n if (socksProxy) {\n const { SocksProxyAgent } = nodeRequire('socks-proxy-agent') as {\n SocksProxyAgent: new (proxy: string) => unknown;\n };\n agent = new SocksProxyAgent(socksProxy);\n }\n return new WebSocketImpl(url, agent ? { agent } : undefined);\n };\n}\n","/**\n * ClientRunner — the daemon's connection owner. The TOON client is 1-to-MANY:\n * it can write through several apexes (each a `ToonClient` + BTP session +\n * payment channel) and read from several relays (each a `RelaySubscription`).\n *\n * • Writes go through BTP, never to a relay directly — `publish`/`swap` select\n * an apex (default: the config-seeded one).\n * • Reads FAN OUT — `subscribe`/`getEvents` apply across every relay and merge\n * into one ordered stream with a single scalar cursor (the runner owns the\n * merged buffer; each `RelaySubscription` mirrors new events into it).\n *\n * Targets are added at runtime (`addRelay`/`addApex`), persisted to\n * `targets.json`, replayed on the next boot, and removable. The config-seeded\n * relay + apex are the permanent DEFAULT targets and cannot be removed.\n *\n * Each apex bootstraps asynchronously and non-blocking: the connection comes up\n * in the background (the managed anon proxy alone can take 30–90s). Until ready,\n * writes against it report `bootstrapping` so tools surface \"retry\".\n */\n\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport { generateSecretKey } from 'nostr-tools/pure';\nimport { decodeEventFromToon } from '@toon-protocol/core';\nimport { startManagedAnonProxy } from '@toon-protocol/client';\nimport type { ToonClientConfig } from '@toon-protocol/client';\nimport { streamSwap } from '@toon-protocol/sdk/swap';\nimport { RelaySubscription } from '../relay-subscription.js';\nimport type {\n AddApexRequest,\n AddApexResponse,\n ApexTargetStatus,\n ChannelsResponse,\n ChainStatus,\n EventsResponse,\n NostrFilter,\n PublishResponse,\n RelayTargetStatus,\n SettlementChain,\n StatusResponse,\n SubscribeRequest,\n SubscribeResponse,\n SwapResponse,\n TargetsResponse,\n} from '../control-api.js';\nimport type {\n EventsQuery,\n PublishRequest,\n SwapRequest,\n} from '../control-api.js';\nimport {\n configDir,\n type ApexNegotiationConfig,\n type ResolvedDaemonConfig,\n} from './config.js';\nimport {\n loadApexChannel,\n saveApexChannel,\n type PersistedChannelContext,\n} from './apex-channel-store.js';\nimport {\n loadTargets,\n removeApexTarget,\n removeRelayTarget,\n saveApexTarget,\n saveRelayTarget,\n type PersistedApexTarget,\n} from './targets-store.js';\nimport { discoverApex } from './apex-discovery.js';\n\n/** The subset of `ToonClient` the runner depends on. */\nexport interface ToonClientLike {\n start(): Promise<{ peersDiscovered: number; mode: string }>;\n stop(): Promise<void>;\n getPublicKey(): string;\n getEvmAddress(): string | undefined;\n getSolanaAddress(): string | undefined;\n getMinaAddress(): string | undefined;\n getNetworkStatus(): { evm: string; solana: string; mina: string } | undefined;\n publishEvent(\n event: NostrEvent,\n options?: { destination?: string; claim?: unknown; ilpAmount?: bigint }\n ): Promise<{\n success: boolean;\n eventId?: string;\n data?: string;\n error?: string;\n }>;\n signBalanceProof(channelId: string, amount: bigint): Promise<unknown>;\n openChannel(destination?: string): Promise<string>;\n getTrackedChannels(): string[];\n getChannelNonce(channelId: string): number;\n getChannelCumulativeAmount(channelId: string): bigint;\n sendSwapPacket(params: {\n destination: string;\n amount: bigint;\n toonData: Uint8Array;\n claim?: unknown;\n }): Promise<{\n accepted: boolean;\n data?: string;\n code?: string;\n message?: string;\n }>;\n}\n\n/** A started managed proxy: just the teardown handle the runner needs. */\nexport interface ManagedProxyHandle {\n stop(): Promise<void>;\n}\n\n/** Starts a managed `anon` read proxy on a loopback SOCKS port. */\nexport type StartReadProxy = (opts: {\n socksPort: number;\n log?: (msg: string) => void;\n}) => Promise<ManagedProxyHandle>;\n\n/** Builds a `ToonClient` (or a fake) for a given resolved client config. */\nexport type CreateClient = (config: ToonClientConfig) => ToonClientLike;\n\n/** Builds a `RelaySubscription` for a given relay URL + optional read proxy. */\nexport type CreateRelay = (opts: {\n relayUrl: string;\n socksProxy?: string;\n onEvent: (subId: string, event: NostrEvent) => void;\n logger?: (msg: string) => void;\n}) => RelaySubscription;\n\nexport interface ClientRunnerDeps {\n config: ResolvedDaemonConfig;\n /** Factory producing the (real or fake) ToonClient for a client config. */\n createClient: CreateClient;\n /** Factory producing a relay subscription (defaults to the real one). */\n createRelay?: CreateRelay;\n /**\n * Starts the daemon-managed read proxy (defaults to the real\n * `startManagedAnonProxy`). Injected so tests avoid the anon download/spawn.\n */\n startReadProxy?: StartReadProxy;\n logger?: (msg: string) => void;\n /** Path to the dynamic-targets store (tests override). */\n targetsPath?: string;\n}\n\n/** One apex write target: a BTP session + its payment channel + settlement. */\ninterface ApexConnection {\n btpUrl: string;\n client: ToonClientLike;\n negotiation?: ApexNegotiationConfig;\n childPeers: string[];\n destination: string;\n chain: SettlementChain;\n /** Per-apex channel-store path (distinct so parallel apexes don't race it). */\n channelStorePath: string;\n feePerEvent: bigint;\n apexChannelId?: string;\n ready: boolean;\n bootstrapping: boolean;\n /** In-flight bootstrap, so concurrent callers await the same work (not re-run). */\n bootstrapPromise?: Promise<void>;\n lastError?: string;\n isDefault: boolean;\n}\n\n/** A runner-level merged read-buffer entry, tagged with its source relay. */\ninterface MergedEvent {\n seq: number;\n relayUrl: string;\n subId: string;\n event: NostrEvent;\n}\n\nconst MERGED_BUFFER = 5000;\n\nexport class ClientRunner {\n private readonly config: ResolvedDaemonConfig;\n private readonly createClient: CreateClient;\n private readonly createRelay: CreateRelay;\n private readonly startReadProxy: StartReadProxy;\n private readonly log: (msg: string) => void;\n private readonly targetsPath?: string;\n\n private readonly startedAt = Date.now();\n\n /** Apex write targets, keyed by btpUrl. */\n private readonly apexes = new Map<string, ApexConnection>();\n /** Relay read targets, keyed by relayUrl. */\n private readonly relays = new Map<string, RelaySubscription>();\n\n /** Runner-level merged read buffer across all relays (de-duped by event.id). */\n private merged: MergedEvent[] = [];\n private readonly mergedSeen = new Set<string>();\n private mergedSeq = 0;\n\n /**\n * Fan-out subscriptions (no relayUrl restriction): replayed onto relays added\n * later so a new relay immediately participates in existing reads.\n */\n private readonly fanoutSubs = new Map<string, NostrFilter[]>();\n private subIdCounter = 0;\n\n private readonly defaultBtpUrl: string;\n private readonly defaultRelayUrl: string;\n\n /** Teardown for the daemon-managed read proxy (btp-direct + relay-`.anyone`). */\n private stopReadProxy: (() => Promise<void>) | undefined;\n private readProxyError: string | undefined;\n private stopped = false;\n private started = false;\n\n constructor(deps: ClientRunnerDeps) {\n this.config = deps.config;\n this.createClient = deps.createClient;\n this.log = deps.logger ?? ((): void => undefined);\n if (deps.targetsPath !== undefined) this.targetsPath = deps.targetsPath;\n this.defaultBtpUrl = deps.config.toonClientConfig.btpUrl ?? '';\n this.defaultRelayUrl = deps.config.relayUrl;\n\n this.createRelay =\n deps.createRelay ??\n ((opts) =>\n new RelaySubscription({\n relayUrl: opts.relayUrl,\n ...(opts.socksProxy ? { socksProxy: opts.socksProxy } : {}),\n ...(opts.logger ? { logger: opts.logger } : {}),\n onEvent: opts.onEvent,\n // The TOON relay sends events TOON-encoded (text) on reads, not JSON.\n decodeEvent: (raw) =>\n decodeEventFromToon(new TextEncoder().encode(raw)),\n }));\n\n this.startReadProxy =\n deps.startReadProxy ??\n ((opts) =>\n startManagedAnonProxy({\n socksPort: opts.socksPort,\n ...(opts.log ? { log: opts.log } : {}),\n }));\n\n // Build the permanent config-seeded default relay + apex up front (not yet\n // started/bootstrapped) so `bootstrap()` works standalone (the daemon and\n // tests both rely on constructing then awaiting bootstrap()).\n this.registerRelay(this.defaultRelayUrl, this.config.socksProxy);\n const defaultApex = this.makeApex({\n btpUrl: this.defaultBtpUrl,\n client: this.createClient(this.config.toonClientConfig),\n ...(this.config.apex ? { negotiation: this.config.apex } : {}),\n childPeers: this.config.apexChildPeers ?? [],\n destination: this.config.destination,\n chain: this.config.chain,\n channelStorePath:\n this.config.toonClientConfig.channelStorePath ??\n this.apexChannelStorePathFor(this.defaultBtpUrl),\n feePerEvent: this.config.feePerEvent,\n isDefault: true,\n });\n this.apexes.set(defaultApex.btpUrl, defaultApex);\n }\n\n /**\n * Start the live connections: the shared read proxy, every relay socket, the\n * default apex bootstrap (non-blocking), then replay persisted dynamic\n * targets. Returns immediately; apexes become ready asynchronously.\n */\n start(): void {\n if (this.started) return;\n this.started = true;\n // The managed read proxy is shared by every relay; start it once up front.\n if (this.config.manageReadProxy) void this.bringUpReadProxy();\n for (const relay of this.relays.values()) relay.start();\n void this.bootstrap();\n this.replayPersistedTargets();\n }\n\n /** Await the default apex's bootstrap (kicking it off if not already running). */\n bootstrap(): Promise<void> {\n const apex = this.apexes.get(this.defaultBtpUrl);\n if (!apex) return Promise.resolve();\n return this.bootstrapApex(apex);\n }\n\n // ── Relays (reads) ─────────────────────────────────────────────────────────\n\n /**\n * Build + register a relay (idempotent by URL), wiring its events into the\n * merged buffer and replaying active fan-out subscriptions. Does NOT start the\n * socket — callers start it (so construction stays side-effect-free for tests).\n */\n private registerRelay(\n relayUrl: string,\n socksProxy?: string\n ): RelaySubscription {\n const existing = this.relays.get(relayUrl);\n if (existing) return existing;\n const relay = this.createRelay({\n relayUrl,\n ...(socksProxy ? { socksProxy } : {}),\n logger: this.log,\n onEvent: (subId, event) => this.pushMerged(relayUrl, subId, event),\n });\n this.relays.set(relayUrl, relay);\n // A new relay joins every active fan-out subscription.\n for (const [subId, filters] of this.fanoutSubs)\n relay.subscribe(filters, subId);\n return relay;\n }\n\n /**\n * Add a relay read target at runtime. `.anyone` relays reuse the managed read\n * proxy (started here if needed). Persisted unless `persist` is false.\n */\n async addRelay(relayUrl: string, persist = true): Promise<void> {\n if (this.relays.has(relayUrl)) return;\n let socksProxy = this.config.socksProxy;\n if (isAnyoneHost(relayUrl) && !socksProxy) {\n await this.ensureReadProxy();\n socksProxy = `socks5h://127.0.0.1:${this.config.readProxySocksPort ?? 9050}`;\n }\n const relay = this.registerRelay(relayUrl, socksProxy);\n relay.start();\n if (persist) saveRelayTarget(relayUrl, this.targetsPath);\n }\n\n /** Remove a relay read target. The config-seeded default cannot be removed. */\n removeRelay(relayUrl: string): void {\n if (relayUrl === this.defaultRelayUrl) {\n throw new TargetError('Cannot remove the default (config-seeded) relay.');\n }\n const relay = this.relays.get(relayUrl);\n if (!relay) throw new TargetError(`No such relay: ${relayUrl}`);\n relay.close();\n this.relays.delete(relayUrl);\n // Drop its events from the merged buffer (and dedup index).\n this.merged = this.merged.filter((m) => {\n if (m.relayUrl === relayUrl) {\n this.mergedSeen.delete(m.event.id);\n return false;\n }\n return true;\n });\n removeRelayTarget(relayUrl, this.targetsPath);\n }\n\n /** Mirror a newly-buffered relay event into the merged cross-relay buffer. */\n private pushMerged(relayUrl: string, subId: string, event: NostrEvent): void {\n if (this.mergedSeen.has(event.id)) return;\n this.mergedSeen.add(event.id);\n this.merged.push({ seq: ++this.mergedSeq, relayUrl, subId, event });\n if (this.merged.length > MERGED_BUFFER) {\n const evicted = this.merged.shift();\n if (evicted) this.mergedSeen.delete(evicted.event.id);\n }\n }\n\n /**\n * Register a free-read subscription. With no `relayUrl` it FANS OUT across\n * every relay (and onto relays added later); with one it targets that relay.\n */\n subscribe(req: SubscribeRequest): SubscribeResponse {\n const subId = req.subId ?? `sub-${++this.subIdCounter}`;\n const filters = Array.isArray(req.filters) ? req.filters : [req.filters];\n const targets = req.relayUrl ? [req.relayUrl] : [...this.relays.keys()];\n if (req.relayUrl && !this.relays.has(req.relayUrl)) {\n throw new TargetError(`No such relay: ${req.relayUrl}`);\n }\n if (!req.relayUrl) this.fanoutSubs.set(subId, filters);\n for (const url of targets) this.relays.get(url)?.subscribe(filters, subId);\n return { subId, relays: targets };\n }\n\n /** Drain merged events newer than the cursor (free read), optionally scoped. */\n getEvents(query: EventsQuery): EventsResponse {\n const after = query.cursor ?? 0;\n const limit = query.limit ?? 200;\n const matches = this.merged.filter(\n (m) =>\n m.seq > after &&\n (query.subId === undefined || m.subId === query.subId) &&\n (query.relayUrl === undefined || m.relayUrl === query.relayUrl)\n );\n const page = matches.slice(0, limit);\n const hasMore = matches.length > page.length;\n const last = page.at(-1);\n return {\n events: page.map((m) => m.event),\n cursor: last ? last.seq : after,\n hasMore,\n };\n }\n\n // ── Apexes (writes) ──────────────────────────────────────────────────────\n\n private makeApex(init: {\n btpUrl: string;\n client: ToonClientLike;\n negotiation?: ApexNegotiationConfig;\n childPeers: string[];\n destination: string;\n chain: SettlementChain;\n channelStorePath: string;\n feePerEvent: bigint;\n isDefault: boolean;\n }): ApexConnection {\n return {\n ...init,\n ready: false,\n bootstrapping: false,\n };\n }\n\n /**\n * Bootstrap one apex (memoized): start, inject negotiation, open/resume the\n * channel, route child peers. Concurrent callers await the same in-flight\n * work rather than re-running it.\n */\n private bootstrapApex(apex: ApexConnection): Promise<void> {\n if (apex.ready) return Promise.resolve();\n if (!apex.bootstrapPromise) {\n apex.bootstrapPromise = this.doBootstrapApex(apex);\n }\n return apex.bootstrapPromise;\n }\n\n private async doBootstrapApex(apex: ApexConnection): Promise<void> {\n apex.bootstrapping = true;\n try {\n await apex.client.start();\n this.injectApexNegotiation(apex);\n apex.apexChannelId = await this.openOrResumeApexChannel(apex);\n this.routeChildPeersThroughApexChannel(apex);\n apex.ready = true;\n apex.lastError = undefined;\n this.log(\n `[runner] apex ${apex.btpUrl} ready; channel ${apex.apexChannelId}`\n );\n } catch (err) {\n apex.lastError = err instanceof Error ? err.message : String(err);\n this.log(\n `[runner] apex ${apex.btpUrl} bootstrap failed: ${apex.lastError}`\n );\n } finally {\n apex.bootstrapping = false;\n }\n }\n\n /**\n * Add an apex write target. Settlement params are discovered by reading the\n * apex's kind:10032 off the given relay (added first if unknown). Persisted.\n */\n async addApex(req: AddApexRequest): Promise<AddApexResponse> {\n await this.addRelay(req.relayUrl); // ensure + persist the discovery relay\n const relay = this.relays.get(req.relayUrl);\n if (!relay) throw new TargetError(`Relay unavailable: ${req.relayUrl}`);\n\n const discovered = await discoverApex({\n relay,\n ilpAddress: req.ilpAddress,\n ...(req.pubkey ? { pubkey: req.pubkey } : {}),\n ...(req.chain ? { chain: req.chain } : {}),\n ...(req.childPeers ? { childPeers: req.childPeers } : {}),\n });\n\n const feePerEvent =\n req.feePerEvent !== undefined\n ? BigInt(req.feePerEvent)\n : this.config.feePerEvent;\n\n await this.instantiateApex(\n {\n btpUrl: discovered.btpUrl,\n negotiation: discovered.negotiation,\n ...(discovered.apexChildPeers\n ? { apexChildPeers: discovered.apexChildPeers }\n : {}),\n feePerEvent: req.feePerEvent ?? feePerEvent.toString(),\n discoveredFrom: req.relayUrl,\n },\n true\n );\n\n const apex = this.apexes.get(discovered.btpUrl);\n if (!apex) {\n throw new TargetError(\n `Apex ${discovered.btpUrl} failed to register after discovery.`\n );\n }\n return {\n btpUrl: apex.btpUrl,\n destination: apex.destination,\n chain: apex.chain,\n ready: apex.ready,\n };\n }\n\n /** Build + register + bootstrap an apex from a (persisted) target record. */\n private async instantiateApex(\n target: PersistedApexTarget,\n persist: boolean\n ): Promise<void> {\n if (this.apexes.has(target.btpUrl)) return;\n const clientConfig = this.deriveApexClientConfig(\n target.btpUrl,\n target.negotiation.destination\n );\n const apex = this.makeApex({\n btpUrl: target.btpUrl,\n client: this.createClient(clientConfig),\n negotiation: target.negotiation,\n childPeers: target.apexChildPeers ?? [],\n destination: target.negotiation.destination,\n chain: target.negotiation.chain,\n channelStorePath: this.apexChannelStorePathFor(target.btpUrl),\n feePerEvent: BigInt(target.feePerEvent ?? this.config.feePerEvent),\n isDefault: false,\n });\n this.apexes.set(apex.btpUrl, apex);\n if (persist) saveApexTarget(target, this.targetsPath);\n await this.bootstrapApex(apex);\n }\n\n /** Remove an apex write target. The config-seeded default cannot be removed. */\n async removeApex(btpUrl: string): Promise<void> {\n if (btpUrl === this.defaultBtpUrl) {\n throw new TargetError('Cannot remove the default (config-seeded) apex.');\n }\n const apex = this.apexes.get(btpUrl);\n if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);\n try {\n await apex.client.stop();\n } catch (err) {\n this.log(\n `[runner] apex ${btpUrl} stop error: ${err instanceof Error ? err.message : String(err)}`\n );\n }\n this.apexes.delete(btpUrl);\n removeApexTarget(btpUrl, this.targetsPath);\n }\n\n /** Derive a per-apex ToonClientConfig from the default (shared identity/transport). */\n private deriveApexClientConfig(\n btpUrl: string,\n destination: string\n ): ToonClientConfig {\n const base = this.config.toonClientConfig;\n return {\n ...base,\n btpUrl,\n destinationAddress: destination,\n // Distinct nonce-watermark store per apex so parallel ChannelManagers in\n // this process never race a shared channels.json.\n channelStorePath: this.apexChannelStorePathFor(btpUrl),\n ilpInfo: { ...base.ilpInfo, btpEndpoint: btpUrl },\n };\n }\n\n private apexChannelStorePathFor(btpUrl: string): string {\n return `${configDir()}/channels-${sanitize(btpUrl)}.json`;\n }\n\n // ── Persisted-target replay ────────────────────────────────────────────────\n\n private replayPersistedTargets(): void {\n let store;\n try {\n store = loadTargets(this.targetsPath);\n } catch (err) {\n this.log(\n `[runner] failed to load targets store: ${err instanceof Error ? err.message : String(err)}`\n );\n return;\n }\n for (const r of store.relays) {\n if (r.relayUrl === this.defaultRelayUrl) continue;\n void this.addRelay(r.relayUrl, false).catch((err) =>\n this.log(`[runner] replay relay ${r.relayUrl} failed: ${errMsg(err)}`)\n );\n }\n for (const a of store.apexes) {\n if (a.btpUrl === this.defaultBtpUrl) continue;\n void this.instantiateApex(a, false).catch((err) =>\n this.log(`[runner] replay apex ${a.btpUrl} failed: ${errMsg(err)}`)\n );\n }\n }\n\n // ── Shared read proxy ──────────────────────────────────────────────────────\n\n private async ensureReadProxy(): Promise<void> {\n if (this.stopReadProxy) return;\n await this.bringUpReadProxy();\n }\n\n private async bringUpReadProxy(): Promise<void> {\n if (this.stopReadProxy) return;\n const socksPort = this.config.readProxySocksPort ?? 9050;\n try {\n this.log(\n `[runner] starting managed read proxy on 127.0.0.1:${socksPort}`\n );\n const proxy = await this.startReadProxy({\n socksPort,\n log: (m) => this.log(`[anon] ${m}`),\n });\n if (this.stopped) {\n await proxy.stop();\n return;\n }\n this.stopReadProxy = () => proxy.stop();\n this.readProxyError = undefined;\n this.log('[runner] managed read proxy ready');\n } catch (err) {\n this.readProxyError = err instanceof Error ? err.message : String(err);\n this.log(`[runner] managed read proxy failed: ${this.readProxyError}`);\n }\n }\n\n // ── Channel / negotiation helpers (per-apex) ───────────────────────────────\n\n /** Open the apex channel — or, on a restart, RESUME the existing one. */\n private async openOrResumeApexChannel(apex: ApexConnection): Promise<string> {\n const { destination, chain } = apex;\n const { apexChannelStorePath } = this.config;\n const saved = loadApexChannel(apexChannelStorePath, destination, chain);\n const cm = (\n apex.client as unknown as {\n channelManager?: {\n trackChannel?: (id: string, ctx: PersistedChannelContext) => void;\n };\n }\n ).channelManager;\n\n if (saved && cm && typeof cm.trackChannel === 'function') {\n cm.trackChannel(saved.channelId, saved.context);\n this.log(\n `[runner] resumed apex channel ${saved.channelId} (tracked, no re-deposit)`\n );\n return saved.channelId;\n }\n\n const channelId = await apex.client.openChannel(destination);\n if (apex.negotiation) {\n const a = apex.negotiation;\n saveApexChannel(apexChannelStorePath, destination, chain, {\n channelId,\n context: {\n chainType: a.chain,\n chainId: a.chainId,\n tokenNetworkAddress: a.tokenNetwork ?? '',\n ...(a.tokenAddress ? { tokenAddress: a.tokenAddress } : {}),\n recipient: a.settlementAddress,\n },\n });\n }\n return channelId;\n }\n\n /** Inject the apex settlement negotiation directly into its ToonClient. */\n private injectApexNegotiation(apex: ApexConnection): void {\n const a = apex.negotiation;\n if (!a) return;\n const negotiations = (\n apex.client as unknown as { peerNegotiations?: Map<string, unknown> }\n ).peerNegotiations;\n if (!(negotiations instanceof Map)) {\n throw new Error(\n 'ToonClient.peerNegotiations layout changed — cannot inject apex negotiation'\n );\n }\n negotiations.set(a.peerId, {\n chain: a.chainKey,\n chainType: a.chain,\n chainId: a.chainId,\n settlementAddress: a.settlementAddress,\n tokenAddress: a.tokenAddress,\n tokenNetwork: a.tokenNetwork,\n });\n this.log(`[runner] injected apex negotiation for peer \"${a.peerId}\"`);\n }\n\n /** Route apex CHILD peers (dvm/mill) through the SAME apex payment channel. */\n private routeChildPeersThroughApexChannel(apex: ApexConnection): void {\n const a = apex.negotiation;\n if (!a || !apex.apexChannelId || apex.childPeers.length === 0) return;\n const client = apex.client as unknown as {\n peerNegotiations?: Map<string, unknown>;\n channelManager?: { peerChannels?: Map<string, string> };\n };\n const negotiations = client.peerNegotiations;\n const peerChannels = client.channelManager?.peerChannels;\n if (!(negotiations instanceof Map) || !(peerChannels instanceof Map)) {\n this.log(\n '[runner] cannot route child peers — ToonClient internals layout changed'\n );\n return;\n }\n for (const peer of apex.childPeers) {\n if (peer === a.peerId) continue;\n negotiations.set(peer, {\n chain: a.chainKey,\n chainType: a.chain,\n chainId: a.chainId,\n settlementAddress: a.settlementAddress,\n tokenAddress: a.tokenAddress,\n tokenNetwork: a.tokenNetwork,\n });\n peerChannels.set(peer, apex.apexChannelId);\n this.log(\n `[runner] routed child peer \"${peer}\" through apex channel ${apex.apexChannelId}`\n );\n }\n }\n\n // ── Status ─────────────────────────────────────────────────────────────────\n\n private defaultApex(): ApexConnection | undefined {\n return this.apexes.get(this.defaultBtpUrl);\n }\n\n /** Whether any apex has finished bootstrapping. */\n isReady(): boolean {\n return [...this.apexes.values()].some((a) => a.ready);\n }\n\n isBootstrapping(): boolean {\n return [...this.apexes.values()].some((a) => a.bootstrapping);\n }\n\n getStatus(): StatusResponse {\n const apex = this.defaultApex();\n const client = apex?.client;\n const net = client?.getNetworkStatus();\n const network: ChainStatus[] | undefined = net\n ? (['evm', 'solana', 'mina'] as const).map((c) => ({\n chain: c,\n ready: net[c] === 'configured',\n detail: net[c],\n }))\n : undefined;\n const relay = this.relays.get(this.defaultRelayUrl);\n return {\n uptimeMs: Date.now() - this.startedAt,\n bootstrapping: apex?.bootstrapping ?? false,\n ready: apex?.ready ?? false,\n settlementChain: this.config.chain,\n identity: {\n nostrPubkey: safe(() => client?.getPublicKey()) ?? '',\n evmAddress: safe(() => client?.getEvmAddress()),\n solanaAddress: safe(() => client?.getSolanaAddress()),\n minaAddress: safe(() => client?.getMinaAddress()),\n },\n transport: {\n type: this.config.socksProxy ? 'socks5' : 'direct',\n ...(this.config.socksProxy\n ? { socksProxy: this.config.socksProxy }\n : {}),\n ...(apex ? { btpUrl: apex.btpUrl } : {}),\n },\n relay: {\n url: this.defaultRelayUrl,\n connected: relay?.isConnected() ?? false,\n buffered: relay?.bufferedCount() ?? 0,\n subscriptions: relay?.activeSubscriptions() ?? [],\n ...(this.readProxyError ? { proxyError: this.readProxyError } : {}),\n },\n ...(network ? { network } : {}),\n ...(apex?.lastError ? { lastError: apex.lastError } : {}),\n };\n }\n\n /** Full registry of relay + apex targets with per-target status. */\n getTargets(): TargetsResponse {\n const relays: RelayTargetStatus[] = [...this.relays.entries()].map(\n ([relayUrl, r]) => ({\n relayUrl,\n connected: r.isConnected(),\n buffered: r.bufferedCount(),\n subscriptions: r.activeSubscriptions(),\n isDefault: relayUrl === this.defaultRelayUrl,\n })\n );\n const apexes: ApexTargetStatus[] = [...this.apexes.values()].map((a) => ({\n btpUrl: a.btpUrl,\n destination: a.destination,\n chain: a.chain,\n ready: a.ready,\n bootstrapping: a.bootstrapping,\n ...(a.apexChannelId ? { channelId: a.apexChannelId } : {}),\n ...(a.lastError ? { lastError: a.lastError } : {}),\n isDefault: a.isDefault,\n }));\n return { relays, apexes };\n }\n\n // ── Paid operations ──────────────────────────────────────────────────────\n\n /** Pay-to-write a single event through the selected (or default) apex. */\n async publish(req: PublishRequest): Promise<PublishResponse> {\n const apex = this.selectApex(req.btpUrl);\n this.assertApexReady(apex);\n const channelId =\n apex.apexChannelId ?? (await apex.client.openChannel(req.destination));\n const fee = req.fee !== undefined ? BigInt(req.fee) : apex.feePerEvent;\n const claim = await apex.client.signBalanceProof(channelId, fee);\n const result = await apex.client.publishEvent(req.event, {\n ...(req.destination ? { destination: req.destination } : {}),\n claim,\n ilpAmount: fee,\n });\n if (!result.success) {\n throw new PublishRejectedError(result.error ?? 'relay rejected event');\n }\n return {\n eventId: result.eventId ?? req.event.id,\n ...(result.data !== undefined ? { data: result.data } : {}),\n channelId,\n nonce: apex.client.getChannelNonce(channelId),\n };\n }\n\n /** Open (or return) a payment channel on the selected (or default) apex. */\n async openChannel(\n destination?: string,\n btpUrl?: string\n ): Promise<{ channelId: string }> {\n const apex = this.selectApex(btpUrl);\n this.assertApexReady(apex);\n const channelId = await apex.client.openChannel(\n destination ?? apex.destination\n );\n if (!destination || destination === apex.destination) {\n apex.apexChannelId = channelId;\n }\n return { channelId };\n }\n\n /** List tracked channels across ALL apexes with nonce + cumulative amount. */\n getChannels(): ChannelsResponse {\n const seen = new Set<string>();\n const channels: ChannelsResponse['channels'] = [];\n for (const apex of this.apexes.values()) {\n for (const channelId of apex.client.getTrackedChannels()) {\n if (seen.has(channelId)) continue;\n seen.add(channelId);\n channels.push({\n channelId,\n nonce: apex.client.getChannelNonce(channelId),\n cumulativeAmount: apex.client\n .getChannelCumulativeAmount(channelId)\n .toString(),\n });\n }\n }\n return { channels };\n }\n\n /** Swap source→target asset against a mill peer via the selected apex. */\n async swap(req: SwapRequest): Promise<SwapResponse> {\n const apex = this.selectApex(req.btpUrl);\n this.assertApexReady(apex);\n const senderSecretKey = generateSecretKey();\n const result = await streamSwap({\n client: apex.client as unknown as Parameters<\n typeof streamSwap\n >[0]['client'],\n millPubkey: req.millPubkey,\n millIlpAddress: req.destination,\n pair: req.pair,\n senderSecretKey,\n chainRecipient: req.chainRecipient,\n totalAmount: BigInt(req.amount),\n packetCount: req.packetCount ?? 1,\n });\n const firstReject = result.rejections[0];\n return {\n accepted: result.claims.length > 0,\n packetsAccepted: result.claims.length,\n claims: result.claims.map((c) => ({\n sourceAmount: c.sourceAmount.toString(),\n targetAmount: c.targetAmount.toString(),\n claim: Buffer.from(c.claimBytes).toString('base64'),\n ...(c.channelId ? { channelId: c.channelId } : {}),\n ...(c.recipient ? { recipient: c.recipient } : {}),\n ...(c.millSignerAddress\n ? { millSignerAddress: c.millSignerAddress }\n : {}),\n ...(c.claimId ? { claimId: c.claimId } : {}),\n ...(c.nonce ? { nonce: c.nonce } : {}),\n ...(c.cumulativeAmount ? { cumulativeAmount: c.cumulativeAmount } : {}),\n })),\n cumulativeSource: result.cumulativeSource.toString(),\n cumulativeTarget: result.cumulativeTarget.toString(),\n state: result.state,\n ...(firstReject\n ? { code: firstReject.code, message: firstReject.message }\n : {}),\n };\n }\n\n /** Graceful teardown: close every relay + stop every apex client + read proxy. */\n async stop(): Promise<void> {\n if (this.stopped) return;\n this.stopped = true;\n for (const relay of this.relays.values()) relay.close();\n if (this.stopReadProxy) {\n try {\n await this.stopReadProxy();\n } catch (err) {\n this.log(`[runner] read proxy stop error: ${errMsg(err)}`);\n }\n this.stopReadProxy = undefined;\n }\n for (const apex of this.apexes.values()) {\n try {\n await apex.client.stop();\n } catch (err) {\n this.log(`[runner] client stop error (${apex.btpUrl}): ${errMsg(err)}`);\n }\n }\n }\n\n // ── internals ────────────────────────────────────────────────────────────\n\n private selectApex(btpUrl?: string): ApexConnection {\n if (btpUrl) {\n const apex = this.apexes.get(btpUrl);\n if (!apex) throw new TargetError(`No such apex: ${btpUrl}`);\n return apex;\n }\n const def = this.defaultApex();\n if (!def) throw new NotReadyError('No apex configured.');\n return def;\n }\n\n private assertApexReady(apex: ApexConnection): void {\n if (!apex.ready) {\n throw new NotReadyError(\n apex.bootstrapping\n ? 'Apex is still bootstrapping (BTP/anon coming up) — retry shortly.'\n : (apex.lastError ?? 'Apex is not ready.')\n );\n }\n }\n}\n\n/** Thrown by paid-write operations while the target apex is not yet ready. */\nexport class NotReadyError extends Error {\n readonly retryable = true;\n constructor(message: string) {\n super(message);\n this.name = 'NotReadyError';\n }\n}\n\n/** Thrown when the relay/connector rejects a paid write. */\nexport class PublishRejectedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PublishRejectedError';\n }\n}\n\n/** Thrown for invalid target add/remove/select operations (maps to HTTP 400/404). */\nexport class TargetError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'TargetError';\n }\n}\n\nfunction safe<T>(fn: () => T): T | undefined {\n try {\n return fn();\n } catch {\n return undefined;\n }\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Filesystem-safe slug for a per-apex channel-store filename. */\nfunction sanitize(s: string): string {\n return s\n .replace(/[^a-zA-Z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80);\n}\n\nfunction isAnyoneHost(url: string): boolean {\n try {\n return new URL(url).hostname.endsWith('.anyone');\n } catch {\n return false;\n }\n}\n","var __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __require = /* @__PURE__ */ ((x) => typeof require !== \"undefined\" ? require : typeof Proxy !== \"undefined\" ? new Proxy(x, {\n get: (a, b) => (typeof require !== \"undefined\" ? require : a)[b]\n}) : x)(function(x) {\n if (typeof require !== \"undefined\") return require.apply(this, arguments);\n throw Error('Dynamic require of \"' + x + '\" is not supported');\n});\nvar __esm = (fn, res) => function __init() {\n return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;\n};\nvar __commonJS = (cb, mod) => function __require2() {\n return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;\n};\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n if (from && typeof from === \"object\" || typeof from === \"function\") {\n for (let key of __getOwnPropNames(from))\n if (!__hasOwnProp.call(to, key) && key !== except)\n __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n }\n return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\nexport {\n __require,\n __esm,\n __commonJS,\n __export,\n __toCommonJS\n};\n//# sourceMappingURL=chunk-UP2VWCW5.js.map","/**\n * SDK-specific error classes for @toon-protocol/sdk.\n * All errors extend ToonError from @toon-protocol/core for a consistent error hierarchy.\n */\n\nimport { ToonError } from '@toon-protocol/core';\n\n/**\n * Error thrown when identity operations fail.\n * Used for invalid mnemonics, invalid secret keys, and key derivation failures.\n */\nexport class IdentityError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'IDENTITY_ERROR', cause);\n this.name = 'IdentityError';\n }\n}\n\n/**\n * Error thrown when node lifecycle operations fail.\n * Used for start/stop failures, configuration errors, etc.\n */\nexport class NodeError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'NODE_ERROR', cause);\n this.name = 'NodeError';\n }\n}\n\n/**\n * Error thrown when handler dispatch operations fail.\n * Used for handler registration conflicts, missing handlers, and dispatch errors.\n */\nexport class HandlerError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'HANDLER_ERROR', cause);\n this.name = 'HandlerError';\n }\n}\n\n/**\n * Error thrown when Schnorr signature verification fails.\n * Used for invalid signatures, malformed events, and verification pipeline errors.\n */\nexport class VerificationError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'VERIFICATION_ERROR', cause);\n this.name = 'VerificationError';\n }\n}\n\n/**\n * Error thrown when payment validation fails.\n * Used for pricing calculation errors, insufficient payment, and pricing policy violations.\n */\nexport class PricingError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'PRICING_ERROR', cause);\n this.name = 'PricingError';\n }\n}\n\n/**\n * Error thrown when NIP-59 gift wrap or NIP-44 FULFILL encryption operations fail.\n * Used for wrap/unwrap failures, decryption errors, and malformed gift wrap events.\n */\nexport class GiftWrapError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'GIFT_WRAP_ERROR', cause);\n this.name = 'GiftWrapError';\n }\n}\n\n/**\n * Error thrown when Mill swap handler orchestration fails.\n * Used for rate-conversion errors (invalid format, zero, overflow guards),\n * unsupported pair lookups, and issuer-boundary failures that are NOT\n * gift-wrap-specific. Gift-wrap failures continue to surface as `GiftWrapError`.\n */\nexport class SwapHandlerError extends ToonError {\n constructor(message: string, cause?: Error) {\n super(message, 'SWAP_HANDLER_ERROR', cause);\n this.name = 'SwapHandlerError';\n }\n}\n\n/**\n * Error thrown by the sender-side `streamSwap()` API (Story 12.5).\n *\n * All failures are categorized by a narrow `code` so callers can branch on\n * cause. `INVALID_*` codes are construction-time validation failures (thrown\n * synchronously before any packet fires). `FULFILL_DECODE_FAILED` surfaces\n * when the Mill returns `accepted: true` but the FULFILL data cannot be\n * decoded — this is a non-fatal per-packet error and is captured in\n * `StreamSwapResult.errors[]`.\n */\nexport class StreamSwapError extends Error {\n readonly code:\n | 'INVALID_AMOUNT'\n | 'INVALID_CHUNKING'\n | 'INVALID_PAIR'\n | 'INVALID_STATE'\n | 'INVALID_CHAIN_RECIPIENT'\n | 'FULFILL_DECODE_FAILED';\n // Not declared on Error in lib.es5; ES2022 adds it, but some tsconfigs\n // still target older libs. Declare explicitly for cross-version safety.\n declare readonly cause?: unknown;\n\n constructor(\n code: StreamSwapError['code'],\n message: string,\n options?: { cause?: unknown }\n ) {\n super(message);\n this.name = 'StreamSwapError';\n this.code = code;\n if (options && 'cause' in options) {\n Object.defineProperty(this, 'cause', {\n value: options.cause,\n enumerable: false,\n writable: true,\n configurable: true,\n });\n }\n }\n}\n\n/**\n * Error thrown by the sender-side `buildSettlementTx()` helper (Story 12.6).\n *\n * Settlement is a post-swap one-shot computation, so this error class is\n * THROWN synchronously (unlike `streamSwap` which routes per-packet failures\n * through `StreamSwapResult`). Callers are expected to wrap the call in\n * `try/catch`.\n *\n * Narrow `code` union lets callers branch on cause — see\n * `_bmad-output/implementation-artifacts/12-6-build-settlement-tx.md` AC-11\n * for the per-code semantics.\n *\n * @since 12.6\n * @stable — Epic 13 Chain Bridge DVM depends on this error shape.\n */\nexport class SettlementTxError extends Error {\n readonly code:\n | 'INVALID_INPUT'\n | 'MISSING_SETTLEMENT_METADATA'\n | 'UNSUPPORTED_CHAIN'\n | 'MISSING_RECIPIENT'\n | 'RECIPIENT_MISMATCH'\n | 'MILL_SIGNER_MISMATCH'\n | 'DUPLICATE_NONCE'\n | 'NON_MONOTONIC_CUMULATIVE'\n | 'INVALID_SIGNATURE_LENGTH'\n | 'INVALID_SIGNATURE_V'\n | 'ENCODING_FAILED';\n declare readonly cause?: unknown;\n\n constructor(\n code: SettlementTxError['code'],\n message: string,\n options?: { cause?: unknown }\n ) {\n super(message);\n this.name = 'SettlementTxError';\n this.code = code;\n if (options && 'cause' in options) {\n Object.defineProperty(this, 'cause', {\n value: options.cause,\n enumerable: false,\n writable: true,\n configurable: true,\n });\n }\n }\n}\n","/**\n * Unified identity module for @toon-protocol/sdk.\n *\n * Derives both a Nostr pubkey (x-only Schnorr, BIP-340) and an EVM address\n * (Keccak-256) from a single secp256k1 private key, following the NIP-06\n * derivation standard.\n *\n * Both Nostr and EVM use the secp256k1 elliptic curve, so a single 12-word\n * seed phrase can recover the complete identity across both layers.\n */\n\nimport {\n generateMnemonic as _generateMnemonic,\n validateMnemonic,\n mnemonicToSeedSync,\n} from '@scure/bip39';\nimport { wordlist } from '@scure/bip39/wordlists/english.js';\nimport { HDKey } from '@scure/bip32';\nimport { getPublicKey } from 'nostr-tools/pure';\nimport { secp256k1 } from '@noble/curves/secp256k1.js';\nimport { ed25519 } from '@noble/curves/ed25519.js';\nimport { keccak_256 } from '@noble/hashes/sha3.js';\nimport { hmac } from '@noble/hashes/hmac.js';\nimport { sha512 } from '@noble/hashes/sha2.js';\nimport { bytesToHex } from '@noble/hashes/utils.js';\nimport { hexToMinaBase58PrivateKey } from '@toon-protocol/core';\nimport { IdentityError } from './errors.js';\n\n/**\n * Represents a node's complete identity: secret key, Nostr pubkey, and EVM address.\n */\nexport interface NodeIdentity {\n /** The 32-byte secp256k1 secret key. */\n secretKey: Uint8Array;\n /** The x-only Schnorr public key (32 bytes, 64 lowercase hex characters). */\n pubkey: string;\n /** The EIP-55 checksummed EVM address (0x-prefixed, 42 characters). */\n evmAddress: string;\n}\n\n/**\n * Solana Ed25519 identity derived via SLIP-0010 from a BIP-39 mnemonic.\n */\nexport interface SolanaIdentity {\n /** 64-byte Ed25519 keypair (32-byte private key + 32-byte public key). */\n secretKey: Uint8Array;\n /** Base58-encoded Ed25519 public key (Solana address). */\n publicKey: string;\n}\n\n/**\n * Mina Pallas identity derived from a BIP-39 mnemonic via mina-signer.\n */\nexport interface MinaIdentity {\n /** Hex-encoded Pallas private key. */\n privateKey: string;\n /** Base58 Mina public key (B62 prefix). */\n publicKey: string;\n}\n\n/**\n * Full multi-chain identity derived from a single BIP-39 mnemonic.\n * Extends NodeIdentity (Nostr + EVM) with Solana and optionally Mina.\n */\nexport interface ToonIdentity extends NodeIdentity {\n /** Solana Ed25519 identity (always populated from mnemonic derivation). */\n solana: SolanaIdentity;\n /** Mina Pallas identity (undefined when mina-signer is not installed). */\n mina?: MinaIdentity;\n}\n\n/**\n * Options for mnemonic-based key derivation.\n */\nexport interface FromMnemonicOptions {\n /** Key index in the NIP-06 derivation path. Defaults to 0. */\n accountIndex?: number;\n}\n\n/**\n * Generates a valid 12-word BIP-39 mnemonic using 128-bit entropy.\n *\n * @returns A space-separated string of 12 BIP-39 English words.\n */\nexport function generateMnemonic(): string {\n return _generateMnemonic(wordlist, 128);\n}\n\n/**\n * Derives a complete NodeIdentity from a BIP-39 mnemonic phrase.\n *\n * Uses the NIP-06 derivation path: m/44'/1237'/0'/0/{accountIndex}\n *\n * @param mnemonic - A valid BIP-39 mnemonic (12 or 24 words).\n * @param options - Optional derivation options (accountIndex defaults to 0).\n * @returns The derived NodeIdentity with secretKey, pubkey, and evmAddress.\n * @throws {IdentityError} If the mnemonic is invalid.\n */\nexport function fromMnemonic(\n mnemonic: string,\n options?: FromMnemonicOptions\n): ToonIdentity {\n if (!validateMnemonic(mnemonic, wordlist)) {\n throw new IdentityError(\n `Invalid BIP-39 mnemonic: the provided words do not form a valid mnemonic phrase`\n );\n }\n\n const accountIndex = options?.accountIndex ?? 0;\n\n if (\n !Number.isInteger(accountIndex) ||\n accountIndex < 0 ||\n accountIndex > MAX_BIP32_INDEX\n ) {\n throw new IdentityError(\n `Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`\n );\n }\n\n const path = `m/44'/1237'/0'/0/${accountIndex}`;\n\n let seed: Uint8Array | undefined;\n try {\n seed = mnemonicToSeedSync(mnemonic);\n const hdKey = HDKey.fromMasterSeed(seed).derive(path);\n\n if (!hdKey.privateKey) {\n throw new IdentityError(`Failed to derive private key at path ${path}`);\n }\n\n const secretKey = hdKey.privateKey;\n const base = deriveIdentity(secretKey);\n\n // Derive Solana Ed25519 identity from the same seed via SLIP-0010,\n // varying by accountIndex so distinct indices yield distinct Solana keys.\n const solana = deriveSolanaIdentity(seed, accountIndex);\n\n return { ...base, solana };\n } catch (error: unknown) {\n if (error instanceof IdentityError) {\n throw error;\n }\n throw new IdentityError(\n `Key derivation failed at path ${path}: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n } finally {\n // Best-effort zeroing of the intermediate seed to reduce the window\n // during which sensitive material remains in memory. This is not a\n // guarantee (JS has no secure-erase primitive), but it limits exposure.\n if (seed) {\n seed.fill(0);\n }\n }\n}\n\n/**\n * Derives a complete NodeIdentity from an existing 32-byte secret key.\n *\n * @param secretKey - A 32-byte secp256k1 secret key.\n * @returns The derived NodeIdentity with secretKey, pubkey, and evmAddress.\n * @throws {IdentityError} If the secret key is not exactly 32 bytes.\n */\nexport function fromSecretKey(secretKey: Uint8Array): NodeIdentity {\n if (!(secretKey instanceof Uint8Array)) {\n throw new IdentityError(\n `Invalid secret key: expected Uint8Array, got ${secretKey === null ? 'null' : typeof secretKey}`\n );\n }\n\n if (secretKey.length !== 32) {\n throw new IdentityError(\n `Invalid secret key: expected 32 bytes, got ${secretKey.length} bytes`\n );\n }\n\n try {\n return deriveIdentity(secretKey);\n } catch (error: unknown) {\n if (error instanceof IdentityError) {\n throw error;\n }\n throw new IdentityError(\n `Invalid secret key: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Maximum valid BIP-32 non-hardened child index (2^31 - 1).\n * Values at or above 2^31 are reserved for hardened derivation.\n */\nconst MAX_BIP32_INDEX = 0x7fffffff;\n\n/**\n * Shared helper that computes pubkey and evmAddress from a 32-byte secret key.\n * Returns a defensive copy of the secret key to prevent external mutation.\n */\nfunction deriveIdentity(secretKey: Uint8Array): NodeIdentity {\n // Compute x-only Schnorr pubkey (32 bytes hex) via nostr-tools\n const pubkey = getPublicKey(secretKey);\n\n // Compute EVM address from the uncompressed secp256k1 public key\n const evmAddress = computeEvmAddress(secretKey);\n\n // Return a defensive copy of the secret key to prevent external mutation\n // from affecting the identity or vice versa\n return { secretKey: new Uint8Array(secretKey), pubkey, evmAddress };\n}\n\n/**\n * Computes an EIP-55 checksummed EVM address from a secp256k1 private key.\n *\n * Steps:\n * 1. Compute the full uncompressed public key (65 bytes: 0x04 + 64 bytes X,Y)\n * 2. Strip the 0x04 prefix to get 64 bytes\n * 3. Hash with Keccak-256\n * 4. Take the last 20 bytes\n * 5. Format as 0x-prefixed hex with EIP-55 checksum\n */\nfunction computeEvmAddress(secretKey: Uint8Array): string {\n // Get the uncompressed public key (65 bytes: 0x04 prefix + 64 bytes X,Y)\n const uncompressedPubkey = secp256k1.getPublicKey(secretKey, false);\n\n // Strip the 0x04 prefix (first byte), leaving 64 bytes of X,Y coordinates\n const pubkeyWithoutPrefix = uncompressedPubkey.slice(1);\n\n // Hash with Keccak-256\n const hash = keccak_256(pubkeyWithoutPrefix);\n\n // Take last 20 bytes\n const addressBytes = hash.slice(-20);\n const addressHex = bytesToHex(addressBytes);\n\n // Apply EIP-55 mixed-case checksum\n return toChecksumAddress(addressHex);\n}\n\n/**\n * Applies EIP-55 mixed-case checksum encoding to an Ethereum address.\n *\n * EIP-55 rules:\n * - Hash the lowercase hex address (without 0x) with Keccak-256\n * - For each hex character in the address:\n * - If the corresponding nibble in the hash is >= 8, uppercase it\n * - Otherwise, lowercase it\n *\n * @param addressHex - The 40-character lowercase hex address (without 0x prefix).\n * @returns The checksummed address with 0x prefix.\n */\nfunction toChecksumAddress(addressHex: string): string {\n const lower = addressHex.toLowerCase();\n const hash = bytesToHex(keccak_256(new TextEncoder().encode(lower)));\n\n let checksummed = '0x';\n for (let i = 0; i < 40; i++) {\n const char = lower[i];\n const hashChar = hash[i];\n if (char === undefined || hashChar === undefined) {\n throw new IdentityError(\n `Unexpected undefined at index ${i} during checksum computation`\n );\n }\n const hashNibble = parseInt(hashChar, 16);\n checksummed += hashNibble >= 8 ? char.toUpperCase() : char;\n }\n\n return checksummed;\n}\n\n// ---------------------------------------------------------------------------\n// SLIP-0010 Ed25519 HD Key Derivation\n// ---------------------------------------------------------------------------\n\n/**\n * SLIP-0010 master key and child key derivation for Ed25519.\n *\n * Ed25519 SLIP-0010 only supports hardened derivation. The path\n * m/44'/501'/0'/0' is standard for Solana wallets.\n *\n * @param seed - The BIP-39 seed (64 bytes).\n * @param path - Array of hardened indices (must have 0x80000000 bit set).\n * @returns The derived 32-byte Ed25519 private key.\n */\nfunction slip0010Derive(seed: Uint8Array, path: number[]): Uint8Array {\n const encoder = new TextEncoder();\n\n // Master key: HMAC-SHA512(\"ed25519 seed\", seed)\n let I = hmac(sha512, encoder.encode('ed25519 seed'), seed);\n let key = I.slice(0, 32);\n let chainCode = I.slice(32);\n\n // Child derivation (hardened only)\n for (const index of path) {\n const data = new Uint8Array(37);\n data[0] = 0x00;\n data.set(key, 1);\n // Write index as big-endian uint32\n data[33] = (index >>> 24) & 0xff;\n data[34] = (index >>> 16) & 0xff;\n data[35] = (index >>> 8) & 0xff;\n data[36] = index & 0xff;\n\n I = hmac(sha512, chainCode, data);\n key = I.slice(0, 32);\n chainCode = I.slice(32);\n }\n\n return key;\n}\n\n/**\n * SLIP-0010 path for Solana, varying the hardened account component by\n * `accountIndex`: m/44'/501'/{accountIndex}'/0' (all hardened). At\n * `accountIndex` 0 this is the canonical m/44'/501'/0'/0', so existing\n * (index-0) keys are unchanged.\n */\nfunction solanaPath(accountIndex: number): number[] {\n return [\n 0x8000002c, // 44'\n 0x800001f5, // 501'\n (0x80000000 + accountIndex) >>> 0, // {accountIndex}'\n 0x80000000, // 0'\n ];\n}\n\n/**\n * Derives a Solana Ed25519 identity from a BIP-39 seed using SLIP-0010.\n *\n * @param accountIndex - Hardened account index (default 0). Distinct indices\n * yield distinct keypairs from the same seed.\n */\nfunction deriveSolanaIdentity(\n seed: Uint8Array,\n accountIndex = 0\n): SolanaIdentity {\n const privateKey = slip0010Derive(seed, solanaPath(accountIndex));\n const publicKeyBytes = ed25519.getPublicKey(privateKey);\n\n // Solana keypair = 32-byte private key + 32-byte public key = 64 bytes\n const keypair = new Uint8Array(64);\n keypair.set(privateKey, 0);\n keypair.set(publicKeyBytes, 32);\n\n return { secretKey: keypair, publicKey: base58Encode(publicKeyBytes) };\n}\n\n// ---------------------------------------------------------------------------\n// Mina Pallas Key Derivation (optional, requires mina-signer)\n// ---------------------------------------------------------------------------\n\n/**\n * Derives a Mina Pallas identity from a BIP-39 seed.\n *\n * Uses BIP-32 secp256k1 derivation at path m/44'/12586'/{accountIndex}'/0/0 to\n * produce a 32-byte scalar, then converts to Mina key format via mina-signer.\n * At `accountIndex` 0 this is the canonical m/44'/12586'/0'/0/0, so existing\n * (index-0) keys are unchanged.\n *\n * @param seed - The BIP-39 seed (64 bytes).\n * @param accountIndex - Hardened account index (default 0). Distinct indices\n * yield distinct keys from the same seed.\n * @returns The Mina identity, or undefined if mina-signer is not installed.\n */\nasync function deriveMinaIdentity(\n seed: Uint8Array,\n accountIndex = 0\n): Promise<MinaIdentity | undefined> {\n const path = `m/44'/12586'/${accountIndex}'/0/0`;\n const hdKey = HDKey.fromMasterSeed(seed).derive(path);\n\n if (!hdKey.privateKey) {\n throw new IdentityError(\n `Failed to derive Mina private key at path ${path}`\n );\n }\n\n const keyBytes = new Uint8Array(hdKey.privateKey);\n // Clamp the top 2 bits so the big-endian scalar is within the Pallas\n // base-field order. A raw BIP-32 child scalar can exceed it (~75% of 256-bit\n // values do), and mina-signer then rejects it with\n // \"Scalar: inputs larger than … are not allowed\". Matches the client's\n // deriveMinaKey and mill's deriveMillKeys, so all three produce the SAME Mina\n // key for a given mnemonic + accountIndex.\n keyBytes[0] = (keyBytes[0] ?? 0) & 0x3f;\n const hexKey = bytesToHex(keyBytes);\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const MinaSignerLib: any = await import('mina-signer');\n const Client =\n 'default' in MinaSignerLib ? MinaSignerLib.default : MinaSignerLib;\n const client = new Client({ network: 'mainnet' });\n\n // mina-signer's derivePublicKey expects a Mina base58check (\"EK…\") private\n // key, NOT a raw hex scalar. Convert first — passing hex makes mina-signer\n // throw `fromBase58: invalid character`, which the old broad catch silently\n // swallowed (so Mina derivation always returned undefined). Keep the\n // exposed privateKey as hex for consumer compatibility (e.g. the client's\n // MinaSigner accepts hex and converts internally).\n const minaBase58PrivateKey = hexToMinaBase58PrivateKey(hexKey);\n const publicKey: string = client.derivePublicKey(minaBase58PrivateKey);\n return { privateKey: hexKey, publicKey };\n } catch (err: unknown) {\n // Degrade gracefully ONLY when mina-signer is genuinely absent (optional\n // dependency). Surface any other failure instead of masking it — the broad\n // catch previously hid the hex-vs-base58 key-format bug above.\n const code = (err as { code?: string } | undefined)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {\n return undefined;\n }\n throw new IdentityError(\n `Mina identity derivation failed at path ${path}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n err instanceof Error ? err : undefined\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public Multi-Chain Functions\n// ---------------------------------------------------------------------------\n\n/**\n * Derives a full multi-chain ToonIdentity from a BIP-39 mnemonic,\n * including async Mina derivation (requires mina-signer).\n *\n * Chains derived:\n * - Nostr (secp256k1): m/44'/1237'/0'/0/{accountIndex}\n * - EVM (secp256k1): same key as Nostr, Keccak-256 for address\n * - Solana (Ed25519): m/44'/501'/{accountIndex}'/0' (SLIP-0010)\n * - Mina (Pallas): m/44'/12586'/{accountIndex}'/0/0 (optional, requires mina-signer)\n *\n * All four chains vary by `accountIndex`, so distinct indices yield fully\n * distinct multi-chain identities from one seed. Index 0 is unchanged from the\n * historical fixed paths.\n *\n * @param mnemonic - A valid BIP-39 mnemonic (12 or 24 words).\n * @param options - Optional derivation options (accountIndex defaults to 0).\n * @returns The derived ToonIdentity with all chain identities populated.\n * @throws {IdentityError} If the mnemonic is invalid.\n */\nexport async function fromMnemonicFull(\n mnemonic: string,\n options?: FromMnemonicOptions\n): Promise<ToonIdentity> {\n // Derive Nostr + EVM + Solana synchronously\n const identity = fromMnemonic(mnemonic, options);\n const accountIndex = options?.accountIndex ?? 0;\n\n // Attempt async Mina derivation (varies by accountIndex, like the others)\n let seed: Uint8Array | undefined;\n try {\n seed = mnemonicToSeedSync(mnemonic);\n const mina = await deriveMinaIdentity(seed, accountIndex);\n if (mina) {\n return { ...identity, mina };\n }\n } finally {\n if (seed) {\n seed.fill(0);\n }\n }\n\n return identity;\n}\n\n/**\n * Generates a random Solana Ed25519 keypair (non-deterministic).\n *\n * For deterministic derivation from a mnemonic, use fromMnemonic() instead.\n *\n * @returns A SolanaIdentity with random keypair.\n */\nexport function generateSolanaKeypair(): SolanaIdentity {\n const privateKey = ed25519.utils.randomSecretKey();\n const publicKeyBytes = ed25519.getPublicKey(privateKey);\n\n const keypair = new Uint8Array(64);\n keypair.set(privateKey, 0);\n keypair.set(publicKeyBytes, 32);\n\n return { secretKey: keypair, publicKey: base58Encode(publicKeyBytes) };\n}\n\n// ---------------------------------------------------------------------------\n// Base58 Encoding/Decoding\n// ---------------------------------------------------------------------------\n\nconst BASE58_ALPHABET =\n '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';\n\n/**\n * Encodes a byte array to a Base58 string (Bitcoin/Solana alphabet).\n */\nexport function base58Encode(bytes: Uint8Array): string {\n // Count leading zeros\n let zeros = 0;\n for (let i = 0; i < bytes.length && bytes[i] === 0; i++) zeros++;\n\n let value = 0n;\n for (const byte of bytes) {\n value = value * 256n + BigInt(byte);\n }\n\n let result = '';\n while (value > 0n) {\n result = BASE58_ALPHABET[Number(value % 58n)] + result;\n value = value / 58n;\n }\n\n // Add leading '1's for leading zero bytes\n for (let i = 0; i < zeros; i++) {\n result = '1' + result;\n }\n\n return result || '1';\n}\n\n/**\n * Decodes a Base58 string to a byte array (Bitcoin/Solana alphabet).\n */\nexport function base58Decode(str: string): Uint8Array {\n // Count leading '1's (zero bytes)\n let zeros = 0;\n for (let i = 0; i < str.length && str[i] === '1'; i++) zeros++;\n\n let value = 0n;\n for (const ch of str) {\n const idx = BASE58_ALPHABET.indexOf(ch);\n if (idx === -1) throw new IdentityError(`Invalid base58 character: ${ch}`);\n value = value * 58n + BigInt(idx);\n }\n\n // Convert bigint to bytes\n const hex = value === 0n ? '' : value.toString(16);\n const hexPadded = hex.length % 2 ? '0' + hex : hex;\n const rawBytes: number[] = [];\n for (let i = 0; i < hexPadded.length; i += 2) {\n rawBytes.push(parseInt(hexPadded.slice(i, i + 2), 16));\n }\n\n const result = new Uint8Array(zeros + rawBytes.length);\n result.set(rawBytes, zeros);\n return result;\n}\n","/**\n * NIP-59 Gift Wrap Integration for ILP Packets (Story 12.2)\n *\n * Provides privacy-preserving encoding/decoding of swap packets using the\n * NIP-59 three-layer gift wrap construction (rumor -> seal -> gift wrap).\n * Intermediary peers routing ILP packets see only opaque TOON-encoded binary\n * in the data field -- they cannot determine the event kind, sender identity,\n * or swap intent. Only the destination Mill can unwrap and process.\n *\n * Also provides NIP-44 encryption/decryption for FULFILL claim return path\n * (D12-008), using ephemeral keys so intermediaries on the return path see\n * only opaque ciphertext.\n *\n * @module\n */\n\nimport { generateSecretKey, getPublicKey } from 'nostr-tools/pure';\nimport type { UnsignedEvent, NostrEvent } from 'nostr-tools/pure';\nimport { createRumor, createSeal, createWrap } from 'nostr-tools/nip59';\nimport {\n encrypt as nip44Encrypt,\n decrypt as nip44Decrypt,\n getConversationKey,\n} from 'nostr-tools/nip44';\nimport {\n encodeEventToToon,\n decodeEventFromToon,\n} from '@toon-protocol/core/toon';\nimport { buildIlpPrepare } from '@toon-protocol/core';\nimport type { IlpPreparePacket } from '@toon-protocol/core';\n\nimport { GiftWrapError } from './errors.js';\n\n// ---------------------------------------------------------------------------\n// Type definitions\n// ---------------------------------------------------------------------------\n\n/** Parameters for {@link wrapSwapPacket}. */\nexport interface WrapSwapPacketParams {\n /** Unsigned inner event containing swap metadata. */\n rumor: UnsignedEvent;\n /** Sender's secp256k1 secret key. */\n senderSecretKey: Uint8Array;\n /** Mill's compressed hex pubkey (64 chars). */\n recipientPubkey: string;\n}\n\n/** Result of {@link wrapSwapPacket}. */\nexport interface WrapSwapPacketResult {\n /** Fully-formed kind:1059 gift wrap event signed by a fresh ephemeral key. */\n giftWrap: NostrEvent;\n /** The ephemeral pubkey used for the outer gift wrap layer. */\n ephemeralPubkey: string;\n}\n\n/** Parameters for {@link unwrapSwapPacket}. */\nexport interface UnwrapSwapPacketParams {\n /** A kind:1059 gift wrap event. */\n giftWrap: NostrEvent;\n /** Recipient's (Mill's) secret key. */\n recipientSecretKey: Uint8Array;\n}\n\n/** Result of {@link unwrapSwapPacket}. */\nexport interface UnwrapSwapPacketResult {\n /** The decrypted inner rumor (unsigned). */\n rumor: UnsignedEvent;\n /** The sender's real pubkey (extracted from the seal layer). */\n senderPubkey: string;\n}\n\n/** Parameters for {@link wrapSwapPacketToToon}. */\nexport interface WrapSwapPacketToToonParams {\n /** Unsigned inner event containing swap metadata. */\n rumor: UnsignedEvent;\n /** Sender's secp256k1 secret key. */\n senderSecretKey: Uint8Array;\n /** Mill's compressed hex pubkey (64 chars). */\n recipientPubkey: string;\n /** ILP destination address. */\n destination: string;\n /** Payment amount in ILP units (bigint). */\n amount: bigint;\n /** Packet expiry. Default: 30s from now (inside buildIlpPrepare). */\n expiresAt?: Date;\n}\n\n/** Result of {@link wrapSwapPacketToToon}. */\nexport interface WrapSwapPacketToToonResult {\n /** Ready-to-send ILP PREPARE packet with TOON-encoded gift wrap as data. */\n ilpPrepare: IlpPreparePacket;\n /** The ephemeral pubkey used for the outer gift wrap layer. */\n ephemeralPubkey: string;\n}\n\n/** Parameters for {@link unwrapSwapPacketFromToon}. */\nexport interface UnwrapSwapPacketFromToonParams {\n /** The data field from an incoming ILP PREPARE (TOON-encoded gift wrap). */\n toonData: Uint8Array;\n /** Recipient's (Mill's) secret key. */\n recipientSecretKey: Uint8Array;\n}\n\n/** Parameters for {@link encryptFulfillClaim}. */\nexport interface EncryptFulfillClaimParams {\n /** The signed claim bytes to encrypt. */\n claimData: Uint8Array;\n /** The original sender's pubkey (recovered from unwrap). */\n senderPubkey: string;\n}\n\n/** Result of {@link encryptFulfillClaim}. */\nexport interface EncryptFulfillClaimResult {\n /** NIP-44 encrypted claim bytes. */\n ciphertext: Uint8Array;\n /** The Mill's ephemeral pubkey (included in FULFILL so sender can decrypt). */\n ephemeralPubkey: string;\n}\n\n/** Parameters for {@link decryptFulfillClaim}. */\nexport interface DecryptFulfillClaimParams {\n /** The encrypted claim bytes from the FULFILL data field. */\n ciphertext: Uint8Array;\n /** The Mill's ephemeral pubkey from the FULFILL data field. */\n ephemeralPubkey: string;\n /** The sender's (recipient of FULFILL) secret key. */\n recipientSecretKey: Uint8Array;\n}\n\n// ---------------------------------------------------------------------------\n// Input validation helpers\n// ---------------------------------------------------------------------------\n\n/** Validate a 32-byte secret key. */\nfunction validateSecretKey(key: Uint8Array, paramName: string): void {\n if (!(key instanceof Uint8Array) || key.length !== 32) {\n throw new GiftWrapError(\n `${paramName} must be a 32-byte Uint8Array, got ${key instanceof Uint8Array ? `${key.length} bytes` : typeof key}`\n );\n }\n}\n\n/** Validate a 64-char lowercase hex pubkey. */\nfunction validatePubkey(pubkey: string, paramName: string): void {\n if (typeof pubkey !== 'string' || !/^[0-9a-f]{64}$/.test(pubkey)) {\n throw new GiftWrapError(\n `${paramName} must be a 64-character lowercase hex string`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Core wrap/unwrap functions (AC-1, AC-2)\n// ---------------------------------------------------------------------------\n\n/**\n * NIP-59 three-layer gift wrap a swap packet.\n *\n * Constructs a kind:1059 gift wrap event using a fresh ephemeral keypair.\n * The rumor is sealed with the sender's real key, then wrapped with the\n * ephemeral key. Intermediaries see only the ephemeral pubkey.\n *\n * Each invocation generates a fresh ephemeral keypair for forward secrecy\n * and message unlinkability (risk R-006).\n *\n * @throws {GiftWrapError} If the wrap operation fails (invalid keys, crypto failure).\n */\nexport function wrapSwapPacket(\n params: WrapSwapPacketParams\n): WrapSwapPacketResult {\n const { rumor, senderSecretKey, recipientPubkey } = params;\n\n validateSecretKey(senderSecretKey, 'senderSecretKey');\n validatePubkey(recipientPubkey, 'recipientPubkey');\n\n try {\n // Use nostr-tools nip59 building blocks so we can capture the ephemeral pubkey.\n // Step 1: Create rumor (adds id + pubkey derived from senderSecretKey)\n const rumorEvent = createRumor(rumor, senderSecretKey);\n\n // Step 2: Create seal (kind:13, NIP-44 encrypted rumor, randomized timestamp)\n const seal = createSeal(rumorEvent, senderSecretKey, recipientPubkey);\n\n // Step 3: Create gift wrap (kind:1059, fresh ephemeral key, NIP-44 encrypted seal)\n const giftWrap = createWrap(seal, recipientPubkey);\n\n // The ephemeral pubkey is the pubkey on the gift wrap event\n const ephemeralPubkey = giftWrap.pubkey;\n\n return { giftWrap, ephemeralPubkey };\n } catch (error) {\n throw new GiftWrapError(\n `Failed to wrap swap packet: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n/**\n * Unwrap a NIP-59 gift-wrapped swap packet, recovering the inner rumor\n * and the sender's real pubkey.\n *\n * Performs two-layer decryption: gift wrap -> seal -> rumor.\n * The sender pubkey is extracted from the seal's pubkey field.\n *\n * @throws {GiftWrapError} If kind is not 1059, decryption fails, or structure is malformed.\n */\nexport function unwrapSwapPacket(\n params: UnwrapSwapPacketParams\n): UnwrapSwapPacketResult {\n const { giftWrap, recipientSecretKey } = params;\n\n validateSecretKey(recipientSecretKey, 'recipientSecretKey');\n\n // Guard against null/undefined/non-object giftWrap before property access\n if (!giftWrap || typeof giftWrap !== 'object') {\n throw new GiftWrapError('giftWrap must be a non-null object');\n }\n\n // Validate kind before attempting decryption\n if (giftWrap.kind !== 1059) {\n throw new GiftWrapError('Expected kind:1059 gift wrap');\n }\n\n let conversationKey1: Uint8Array | null = null;\n let conversationKey2: Uint8Array | null = null;\n\n try {\n // Layer 1: Decrypt the gift wrap to recover the seal.\n // The gift wrap was encrypted with ECDH(ephemeralPrivkey, recipientPubkey),\n // so we decrypt with ECDH(recipientSecretKey, giftWrap.pubkey).\n conversationKey1 = getConversationKey(recipientSecretKey, giftWrap.pubkey);\n const sealJson = nip44Decrypt(giftWrap.content, conversationKey1);\n const seal = JSON.parse(sealJson) as NostrEvent;\n\n // Validate seal kind (must be kind:13 per NIP-59)\n if (seal.kind !== 13) {\n throw new GiftWrapError(\n `Expected kind:13 seal inside gift wrap, got kind:${seal.kind}`\n );\n }\n\n // Extract and validate the sender's real pubkey from the seal\n const senderPubkey = seal.pubkey;\n validatePubkey(senderPubkey, 'seal.pubkey (sender identity)');\n\n // Layer 2: Decrypt the seal to recover the rumor.\n // The seal was encrypted with ECDH(senderSecretKey, recipientPubkey),\n // so we decrypt with ECDH(recipientSecretKey, senderPubkey).\n conversationKey2 = getConversationKey(recipientSecretKey, senderPubkey);\n const rumorJson = nip44Decrypt(seal.content, conversationKey2);\n const rumor = JSON.parse(rumorJson) as UnsignedEvent & {\n id?: string;\n sig?: string;\n };\n\n // Strip sig if present (rumor should be unsigned)\n delete rumor.sig;\n\n return { rumor, senderPubkey };\n } catch (error) {\n if (error instanceof GiftWrapError) {\n throw error;\n }\n throw new GiftWrapError(\n `Failed to unwrap swap packet: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n } finally {\n // Zero ECDH-derived conversation keys (defense-in-depth)\n if (conversationKey1) {\n conversationKey1.fill(0);\n conversationKey1 = null;\n }\n if (conversationKey2) {\n conversationKey2.fill(0);\n conversationKey2 = null;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Convenience wrappers for TOON + ILP integration (AC-3, AC-4)\n// ---------------------------------------------------------------------------\n\n/**\n * Convenience: gift wrap a swap packet, encode to TOON binary, and build\n * an ILP PREPARE packet.\n *\n * This is the path that `streamSwap()` (Story 12.5) will use in its\n * per-packet loop.\n */\nexport function wrapSwapPacketToToon(\n params: WrapSwapPacketToToonParams\n): WrapSwapPacketToToonResult {\n const {\n rumor,\n senderSecretKey,\n recipientPubkey,\n destination,\n amount,\n expiresAt,\n } = params;\n\n // Step 1: Gift wrap\n const { giftWrap, ephemeralPubkey } = wrapSwapPacket({\n rumor,\n senderSecretKey,\n recipientPubkey,\n });\n\n // Step 2: Encode to TOON binary\n const toonBinary = encodeEventToToon(giftWrap);\n\n // Step 3: Build ILP PREPARE packet\n const ilpPrepare = buildIlpPrepare({\n destination,\n amount,\n data: toonBinary,\n expiresAt,\n });\n\n return { ilpPrepare, ephemeralPubkey };\n}\n\n/**\n * Convenience: decode TOON binary from an incoming ILP PREPARE data field\n * and unwrap the gift-wrapped swap packet.\n *\n * This is the path that the Mill handler (Story 12.3) will use to process\n * incoming swap packets.\n */\nexport function unwrapSwapPacketFromToon(\n params: UnwrapSwapPacketFromToonParams\n): UnwrapSwapPacketResult {\n const { toonData, recipientSecretKey } = params;\n\n if (!(toonData instanceof Uint8Array) || toonData.length === 0) {\n throw new GiftWrapError('toonData must be a non-empty Uint8Array');\n }\n\n try {\n // Step 1: Decode TOON binary to NostrEvent\n const giftWrap = decodeEventFromToon(toonData);\n\n // Step 2: Unwrap\n return unwrapSwapPacket({ giftWrap, recipientSecretKey });\n } catch (error) {\n if (error instanceof GiftWrapError) {\n throw error;\n }\n throw new GiftWrapError(\n `Failed to unwrap swap packet from TOON: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// FULFILL encryption/decryption (AC-5, AC-6)\n// ---------------------------------------------------------------------------\n\n/**\n * Encrypt a FULFILL claim for the return path (D12-008).\n *\n * Generates a fresh ephemeral keypair and NIP-44 encrypts the claim data.\n * The ephemeral pubkey is included alongside the ciphertext so the sender\n * can decrypt. The ephemeral privkey is discarded after encryption.\n *\n * @throws {GiftWrapError} If claimData is empty or encryption fails.\n */\nexport function encryptFulfillClaim(\n params: EncryptFulfillClaimParams\n): EncryptFulfillClaimResult {\n const { claimData, senderPubkey } = params;\n\n validatePubkey(senderPubkey, 'senderPubkey');\n\n if (!(claimData instanceof Uint8Array)) {\n throw new GiftWrapError(\n `claimData must be a Uint8Array, got ${typeof claimData}`\n );\n }\n\n if (claimData.length === 0) {\n throw new GiftWrapError('claimData must not be empty');\n }\n\n // Generate fresh ephemeral keypair (let-scoped for GC after return)\n let ephemeralSecretKey: Uint8Array | null = generateSecretKey();\n const ephemeralPubkey = getPublicKey(ephemeralSecretKey);\n let conversationKey: Uint8Array | null = null;\n\n try {\n // NIP-44 encrypt: ECDH(ephemeralPrivkey, senderPubkey)\n conversationKey = getConversationKey(ephemeralSecretKey, senderPubkey);\n\n // NIP-44 encrypts strings, so base64-encode the claim bytes\n const claimString = Buffer.from(claimData).toString('base64');\n const ciphertextString = nip44Encrypt(claimString, conversationKey);\n\n // Convert ciphertext string to Uint8Array for transport\n const ciphertext = new TextEncoder().encode(ciphertextString);\n\n return { ciphertext, ephemeralPubkey };\n } catch (error) {\n if (error instanceof GiftWrapError) {\n throw error;\n }\n throw new GiftWrapError(\n `Failed to encrypt FULFILL claim: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n } finally {\n // Zero sensitive key material before releasing for GC (defense-in-depth)\n if (ephemeralSecretKey) {\n ephemeralSecretKey.fill(0);\n ephemeralSecretKey = null;\n }\n if (conversationKey) {\n conversationKey.fill(0);\n conversationKey = null;\n }\n }\n}\n\n/**\n * Decrypt a FULFILL claim from the return path.\n *\n * Uses the sender's secret key and the Mill's ephemeral pubkey (from the\n * FULFILL data field) to NIP-44 decrypt the claim bytes.\n *\n * @throws {GiftWrapError} If decryption fails (wrong key, malformed ciphertext).\n */\nexport function decryptFulfillClaim(\n params: DecryptFulfillClaimParams\n): Uint8Array {\n const { ciphertext, ephemeralPubkey, recipientSecretKey } = params;\n\n validateSecretKey(recipientSecretKey, 'recipientSecretKey');\n validatePubkey(ephemeralPubkey, 'ephemeralPubkey');\n\n if (!(ciphertext instanceof Uint8Array) || ciphertext.length === 0) {\n throw new GiftWrapError('ciphertext must be a non-empty Uint8Array');\n }\n\n let conversationKey: Uint8Array | null = null;\n\n try {\n // NIP-44 decrypt: ECDH(recipientSecretKey, ephemeralPubkey)\n conversationKey = getConversationKey(recipientSecretKey, ephemeralPubkey);\n\n // Convert ciphertext Uint8Array back to string\n const ciphertextString = new TextDecoder().decode(ciphertext);\n\n // NIP-44 decrypt returns a string (base64-encoded claim bytes)\n const claimString = nip44Decrypt(ciphertextString, conversationKey);\n\n // Decode base64 back to Uint8Array\n return new Uint8Array(Buffer.from(claimString, 'base64'));\n } catch (error) {\n if (error instanceof GiftWrapError) {\n throw error;\n }\n throw new GiftWrapError(\n `Failed to decrypt FULFILL claim: ${error instanceof Error ? error.message : String(error)}`,\n error instanceof Error ? error : undefined\n );\n } finally {\n // Zero ECDH-derived conversation key (defense-in-depth)\n if (conversationKey) {\n conversationKey.fill(0);\n conversationKey = null;\n }\n }\n}\n","/**\n * Mill Swap Handler (Story 12.3)\n *\n * `createSwapHandler()` factory produces a kind:1059 `Handler` that:\n * 1. Unwraps an incoming NIP-59 gift-wrapped ILP swap packet (via Story 12.2).\n * 2. Identifies the requested `SwapPair` from inner-rumor `swap-from` / `swap-to` tags.\n * 3. Applies a per-packet exchange rate (pair.rate or live rateProvider hook).\n * 4. Delegates signed claim issuance to a pluggable `ClaimIssuer` (Story 12.4).\n * 5. NIP-44 encrypts the claim with an ephemeral key (Story 12.2) for return.\n *\n * The handler is a pure application-layer composition — no connector, routing,\n * or wallet code lives here. See `_bmad-output/epics/epic-12-token-swap-primitive.md`\n * for D12-001/D12-008/D12-009/D12-010 and the scope fence.\n *\n * Transport encoding: the `accept()` metadata emits `claim` as a base64-encoded\n * NIP-44 ciphertext, `ephemeralPubkey` as 64-char lowercase hex, and optional\n * `claimId`. The sender-side `streamSwap()` (Story 12.5) base64-decodes `claim`\n * before calling `decryptFulfillClaim`.\n *\n * @module\n */\n\nimport { createHash } from 'node:crypto';\nimport type { UnsignedEvent } from 'nostr-tools/pure';\nimport type { SwapPair } from '@toon-protocol/core';\n\nimport { GiftWrapError, SwapHandlerError } from './errors.js';\nimport { unwrapSwapPacketFromToon, encryptFulfillClaim } from './gift-wrap.js';\nimport type { Handler } from './handler-registry.js';\nimport { base58Decode } from './identity.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Parameters passed to a {@link ClaimIssuer.issueClaim} call. */\nexport interface IssueClaimParams {\n /** Source-asset amount received by the Mill (ILP packet amount, source micro-units). */\n sourceAmount: bigint;\n /** Target-asset amount owed to the sender (post-rate-conversion, target micro-units). */\n targetAmount: bigint;\n /** The `SwapPair` this packet is being priced against. */\n pair: SwapPair;\n /**\n * The sender's real Nostr pubkey (extracted from the unwrapped seal).\n *\n * Identity-layer key only: used by the Mill for inventory ledger keying and\n * the sender→channel sticky binding (`channelState.reserve()` /\n * `channelState.release()`). The Mill MUST NOT pass this to chain-layer\n * signers as the balance-proof `recipient` — use {@link chainRecipient}\n * for that (Story 12.9 D12-011).\n */\n senderPubkey: string;\n /**\n * The sender's chain-specific payout address for `pair.to.chain`\n * (Story 12.9 AC-10). Extracted and format-validated from the rumor's\n * `chain-recipient` tag by the swap handler. REQUIRED. This is the\n * address the Mill's `PaymentChannelSigner` MUST use as the balance-proof\n * `recipient` (e.g., 20-byte EVM address, 32-byte Solana Ed25519 pubkey).\n */\n chainRecipient: string;\n /** The inner rumor (for optional Mill-side context; may be ignored by the issuer). */\n rumor: UnsignedEvent;\n}\n\n/**\n * Result returned from {@link ClaimIssuer.issueClaim}.\n *\n * Story 12.6 extension: additive settlement-context fields let the sender\n * reconstruct the balance-proof message hash for signature verification and\n * on-chain settlement (see `buildSettlementTx()`).\n */\nexport interface IssueClaimResult {\n /** Signed claim bytes ready for NIP-44 encryption (chain-specific format). */\n claim: Uint8Array;\n /** Optional Mill-side claim ID for logging/tracing. */\n claimId?: string;\n // --- Story 12.6 settlement-context fields (additive, all optional for\n // one-story-cycle backward compat; the Mill SHOULD emit all of them) ---\n /** Channel identifier on the target chain. */\n channelId?: string;\n /** Balance-proof nonce (monotonically increasing per channel). */\n nonce?: bigint;\n /** Cumulative transferred amount on the channel (target micro-units). */\n cumulativeAmount?: bigint;\n /** Recipient address (the sender's target-asset address). */\n recipient?: string;\n /** Mill's on-chain signer address. */\n millSignerAddress?: string;\n}\n\n/**\n * Pluggable signed-claim issuer. Story 12.3 defines only the contract — the\n * concrete multi-chain implementation ships in Story 12.4.\n *\n * The issuer owns inventory accounting and signing-key material. The handler\n * relies on `issueClaim()` being atomic with inventory debit: if the call\n * resolves, the target-asset amount MUST be considered committed from the\n * Mill's reserves. If the call throws, no inventory change SHOULD have occurred.\n */\nexport interface ClaimIssuer {\n /**\n * Produce a signed off-chain payment-channel claim in the target asset.\n *\n * @throws Error (or subclass) on insufficient reserves, unsupported pair,\n * or signing failure. Errors with `code === 'INSUFFICIENT_INVENTORY'` or\n * messages matching `/insufficient/i` are surfaced as ILP T04; all other\n * errors as T00.\n */\n issueClaim(params: IssueClaimParams): Promise<IssueClaimResult>;\n}\n\n/** Parameters for {@link applyRate}. */\nexport interface ApplyRateParams {\n /** Source amount in source micro-units. */\n sourceAmount: bigint;\n /** `SwapPair.from.assetScale` (number of decimals on source side). */\n fromScale: number;\n /** `SwapPair.to.assetScale` (number of decimals on target side). */\n toScale: number;\n /** Decimal-string rate (target whole-units per source whole-unit). */\n rate: string;\n}\n\n/** Minimal pino-compatible logger interface. */\nexport interface SwapHandlerLogger {\n debug: (...args: unknown[]) => void;\n info: (...args: unknown[]) => void;\n warn: (...args: unknown[]) => void;\n error: (...args: unknown[]) => void;\n}\n\n/**\n * Minimal `Set`-like contract the handler requires from\n * `seenPacketIds`. Both the native `Set<string>` and\n * {@link BoundedSeenPacketIds} satisfy this; operators can swap in a\n * persistent/remote-backed replacement too.\n */\nexport interface SeenPacketIdsLike {\n has(value: string): boolean;\n add(value: string): unknown;\n delete(value: string): boolean;\n}\n\n/** Configuration for {@link createSwapHandler}. */\nexport interface CreateSwapHandlerConfig {\n /** Mill's secp256k1 secret key for unwrapping gift-wrapped packets (32 bytes). */\n recipientSecretKey: Uint8Array;\n /** Swap pairs this Mill currently supports. */\n swapPairs: SwapPair[];\n /** Claim issuer delegate (Story 12.4 plugs in the multi-chain implementation). */\n claimIssuer: ClaimIssuer;\n /**\n * Optional live-rate override hook. When provided, the handler calls this per\n * packet instead of reading `pair.rate`. MUST return a decimal string matching\n * `SwapPair.rate` format: /^(0|[1-9]\\d*)(\\.\\d+)?$/.\n */\n rateProvider?: (pair: SwapPair) => string | Promise<string>;\n /**\n * Optional replay-protection set. When provided, the handler uses it\n * VERBATIM (operator-owned — bounding/persistence is the operator's\n * responsibility). When OMITTED (Story 12.8 AC-14), the handler\n * defaults to a {@link BoundedSeenPacketIds} with cap\n * {@link DEFAULT_SEEN_PACKET_IDS_CAP}. Any `Set`-shaped object with\n * `has`/`add`/`delete` (and a `size` getter for test introspection)\n * satisfies the contract.\n */\n seenPacketIds?: SeenPacketIdsLike;\n /** Optional pino-compatible logger. Defaults to a no-op logger. */\n logger?: SwapHandlerLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Story 12.8 AC-14 — bounded seenPacketIds default\n// ---------------------------------------------------------------------------\n\n/**\n * Default ceiling for the `seenPacketIds` replay-protection set when the\n * operator does not supply a custom `Set`. Rationale: 10_000 packet-ids at\n * ~64 bytes each yields a ~640KB memory ceiling — high enough to absorb\n * legitimate bursts, low enough to bound DoS via distinct-id flooding.\n *\n * When the default bounded set is in use, eviction is LAST-ACCESS order\n * (LRU), NOT insertion order: a replay attacker who retries the same\n * packet-id forever would, under insertion-order LRU, have their entry\n * evicted after 10_000 new ids pass through — re-opening the replay\n * window. Access-order keeps frequently-replayed ids pinned, so the\n * window stays closed.\n *\n * Operators may override by supplying a custom `Set<string>` (e.g. a\n * persistent backing store). `createSwapHandler` uses the supplied set\n * verbatim without size-capping.\n */\nexport const DEFAULT_SEEN_PACKET_IDS_CAP = 10_000;\n\n/**\n * ILP REJECT codes emitted by `createSwapHandler()`.\n *\n * Exported as named constants so tests (Story 12.8 AC-3 forbids hardcoded\n * error-code strings) and downstream integrators can assert against the\n * same symbolic source the handler uses. Values follow RFC 27 / ILPv4\n * reject-code conventions (F01 = malformed, F02 = unreachable, F04 =\n * duplicate, F06 = unsupported pair, T00 = transient internal, T04 =\n * insufficient liquidity).\n */\nexport const SWAP_HANDLER_REJECT_CODES = {\n /** Malformed / invalid PREPARE content — gift-wrap shape or amount invalid. */\n INVALID_GIFT_WRAP: 'F01',\n /** No route — the handler did not match a registered destination. */\n UNREACHABLE: 'F02',\n /** Duplicate packet — `seenPacketIds` replay hit. */\n DUPLICATE_PACKET: 'F04',\n /** Requested swap pair is not advertised by this Mill. */\n UNSUPPORTED_PAIR: 'F06',\n /** Transient internal failure — signing, rate provider, or unexpected. */\n INTERNAL: 'T00',\n /** Insufficient Mill inventory for the requested amount. */\n INSUFFICIENT_LIQUIDITY: 'T04',\n} as const;\n\n/**\n * Human-readable reject messages emitted by `createSwapHandler()`.\n *\n * Tests may assert against these verbatim rather than matching regexes\n * (Story 12.8 AC-3 guidance).\n */\nexport const SWAP_HANDLER_REJECT_MESSAGES = {\n INVALID_GIFT_WRAP: 'Invalid gift wrap',\n INVALID_AMOUNT: 'Invalid amount',\n UNREACHABLE: 'Unreachable',\n DUPLICATE_PACKET: 'Duplicate packet',\n UNSUPPORTED_PAIR: 'Unsupported swap pair',\n INTERNAL: 'Internal error',\n INSUFFICIENT_LIQUIDITY: 'Insufficient liquidity',\n RATE_PROVIDER: 'Rate provider error',\n RATE_CONVERSION: 'Rate conversion error',\n} as const;\n\n/**\n * LRU-ish `Set<string>`: re-adding an existing element promotes it to\n * \"most recently accessed\"; `has()` also promotes. Eviction occurs on\n * `add()` when size exceeds the cap — the least-recently-accessed entry\n * (Map insertion-order head) is removed.\n *\n * Exported as `@internal` for Story 12.8 AC-10 test introspection.\n *\n * @internal\n */\nexport class BoundedSeenPacketIds {\n readonly #map = new Map<string, true>();\n readonly #cap: number;\n readonly [Symbol.toStringTag] = 'Set';\n\n constructor(cap: number = DEFAULT_SEEN_PACKET_IDS_CAP) {\n if (!Number.isInteger(cap) || cap <= 0) {\n throw new Error(\n `BoundedSeenPacketIds cap must be a positive integer, got ${cap}`\n );\n }\n this.#cap = cap;\n }\n\n get size(): number {\n return this.#map.size;\n }\n\n /** Exposed for AC-10 test introspection. */\n get cap(): number {\n return this.#cap;\n }\n\n has(value: string): boolean {\n if (!this.#map.has(value)) return false;\n // Access-order promotion: re-insert so iteration order puts this at tail.\n this.#map.delete(value);\n this.#map.set(value, true);\n return true;\n }\n\n add(value: string): this {\n if (this.#map.has(value)) {\n // Promote to most-recently-accessed.\n this.#map.delete(value);\n this.#map.set(value, true);\n return this;\n }\n this.#map.set(value, true);\n while (this.#map.size > this.#cap) {\n // Evict least-recently-accessed (Map iteration is insertion order;\n // first key is the oldest that hasn't been re-added).\n const first = this.#map.keys().next();\n if (first.done) break;\n this.#map.delete(first.value);\n }\n return this;\n }\n\n delete(value: string): boolean {\n return this.#map.delete(value);\n }\n\n clear(): void {\n this.#map.clear();\n }\n\n forEach(\n callback: (value: string, value2: string, set: Set<string>) => void,\n thisArg?: unknown\n ): void {\n for (const k of this.#map.keys()) {\n callback.call(thisArg, k, k, this);\n }\n }\n\n *[Symbol.iterator](): IterableIterator<string> {\n yield* this.#map.keys();\n }\n\n *keys(): IterableIterator<string> {\n yield* this.#map.keys();\n }\n\n *values(): IterableIterator<string> {\n yield* this.#map.keys();\n }\n\n *entries(): IterableIterator<[string, string]> {\n for (const k of this.#map.keys()) yield [k, k];\n }\n}\n\n// ---------------------------------------------------------------------------\n// applyRate helper (AC-8)\n// ---------------------------------------------------------------------------\n\nconst RATE_REGEX = /^(0|[1-9]\\d*)(\\.\\d+)?$/;\n\n/**\n * Apply a decimal-string exchange rate to a source amount across asset scales.\n * Uses BigInt arithmetic throughout — never coerces to `Number` — to preserve\n * 18-decimal EVM precision (Epic 11 retro MAX_SAFE_INTEGER guard).\n *\n * Rounds toward zero (integer division), which economically favors the Mill\n * (standard market-maker convention).\n *\n * @throws {SwapHandlerError} If rate format is invalid, rate is zero, or\n * sourceAmount is not positive.\n */\nexport function applyRate(params: ApplyRateParams): bigint {\n const { sourceAmount, fromScale, toScale, rate } = params;\n\n if (!RATE_REGEX.test(rate)) {\n throw new SwapHandlerError(`Invalid rate format: ${rate}`);\n }\n // Reject any zero-valued rate regardless of decimal presentation.\n // RATE_REGEX matches `'0'`, `'0.0'`, `'0.00'`, etc. — all semantically\n // \"not quoting\". Previous check only caught the bare `'0'` form, letting\n // `'0.0'` slip through and produce a zero-valued targetAmount that the\n // sender's rate-deviation guard could not catch (expectedTargetAmount=0n\n // skips the deviation math). (Story 12.5 code-review pass #3.)\n if (/^0(\\.0+)?$/.test(rate)) {\n throw new SwapHandlerError('Rate is zero (pair not quoting)');\n }\n if (sourceAmount <= 0n) {\n throw new SwapHandlerError(\n `sourceAmount must be positive, got ${sourceAmount}`\n );\n }\n\n const dotIdx = rate.indexOf('.');\n const integerPart = dotIdx === -1 ? rate : rate.slice(0, dotIdx);\n const fractionalPart = dotIdx === -1 ? '' : rate.slice(dotIdx + 1);\n\n const rateNumerator = BigInt(integerPart + fractionalPart);\n const rateDenominator = 10n ** BigInt(fractionalPart.length);\n\n const scaleUp = 10n ** BigInt(toScale);\n const scaleDown = 10n ** BigInt(fromScale);\n\n return (\n (sourceAmount * rateNumerator * scaleUp) / (rateDenominator * scaleDown)\n );\n}\n\n// ---------------------------------------------------------------------------\n// findSwapPair helper (AC-7)\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `SwapPair` identified by the rumor's `swap-from` / `swap-to` tags.\n *\n * Each tag value is parsed as `<assetCode>:<chain>`, split on the FIRST `:`\n * so multi-segment chain IDs like `evm:base:8453` remain intact as the chain\n * portion. Returns `null` for any malformed/missing tag — the handler\n * interprets `null` as \"unsupported pair\" and rejects via ILP F06.\n */\nexport function findSwapPair(\n rumor: UnsignedEvent,\n pairs: SwapPair[]\n): SwapPair | null {\n const fromTag = findTagValue(rumor, 'swap-from');\n const toTag = findTagValue(rumor, 'swap-to');\n\n if (!fromTag || !toTag) return null;\n\n const fromParts = splitAssetChain(fromTag);\n const toParts = splitAssetChain(toTag);\n if (!fromParts || !toParts) return null;\n\n for (const pair of pairs) {\n if (\n pair.from.assetCode === fromParts.assetCode &&\n pair.from.chain === fromParts.chain &&\n pair.to.assetCode === toParts.assetCode &&\n pair.to.chain === toParts.chain\n ) {\n return pair;\n }\n }\n return null;\n}\n\nfunction findTagValue(\n rumor: UnsignedEvent,\n tagName: string\n): string | undefined {\n if (!Array.isArray(rumor.tags)) return undefined;\n for (const t of rumor.tags) {\n if (Array.isArray(t) && t[0] === tagName && typeof t[1] === 'string') {\n return t[1];\n }\n }\n return undefined;\n}\n\nfunction splitAssetChain(\n raw: string\n): { assetCode: string; chain: string } | null {\n const idx = raw.indexOf(':');\n if (idx <= 0 || idx === raw.length - 1) return null;\n const assetCode = raw.slice(0, idx);\n const chain = raw.slice(idx + 1);\n if (!assetCode || !chain) return null;\n return { assetCode, chain };\n}\n\n// ---------------------------------------------------------------------------\n// Story 12.9 AC-2 / AC-8 — chain-recipient format validation\n// ---------------------------------------------------------------------------\n//\n// Duplicated (intentionally small) from `stream-swap.ts` to avoid a circular\n// module cycle (`stream-swap` imports `applyRate` from this file). Guardrail\n// 8.5 sanctions local duplication rather than introducing a shared helper\n// package. Rules MUST match the sender-side validator byte-for-byte.\n\nconst SWAP_HANDLER_EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;\nconst SWAP_HANDLER_BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;\n\n/**\n * Validate a chain-recipient address against `chain`. MUST remain in sync\n * with `validateChainAddress(value, chain, 'address')` in `stream-swap.ts`.\n */\nexport function validateChainRecipient(value: string, chain: string): boolean {\n if (typeof value !== 'string' || value.length === 0) return false;\n if (chain.startsWith('evm:')) {\n return SWAP_HANDLER_EVM_ADDRESS_REGEX.test(value);\n }\n if (chain.startsWith('solana:')) {\n if (!SWAP_HANDLER_BASE58_REGEX.test(value)) return false;\n if (value.length < 32 || value.length > 44) return false;\n try {\n return base58Decode(value).length === 32;\n } catch {\n return false;\n }\n }\n if (chain.startsWith('mina:')) {\n return SWAP_HANDLER_BASE58_REGEX.test(value) && value.length >= 32;\n }\n return value.length > 0;\n}\n\n/**\n * Extract the sender-supplied chain-recipient address from the rumor's\n * `chain-recipient` tag (Story 12.9 AC-8). Returns `null` if the tag is\n * missing or malformed for `chain`.\n */\nexport function findChainRecipient(\n rumor: UnsignedEvent,\n chain: string\n): string | null {\n const raw = findTagValue(rumor, 'chain-recipient');\n if (!raw) return null;\n if (!validateChainRecipient(raw, chain)) return null;\n return raw;\n}\n\n// ---------------------------------------------------------------------------\n// No-op logger\n// ---------------------------------------------------------------------------\n\nconst noop = (): void => undefined;\nconst NOOP_LOGGER: SwapHandlerLogger = {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n};\n\n// ---------------------------------------------------------------------------\n// createSwapHandler factory (AC-3..AC-12)\n// ---------------------------------------------------------------------------\n\n/**\n * Construct a kind:1059 Mill inbound-swap handler.\n *\n * The returned `Handler` is a pure closure over `config`; two calls with the\n * same config yield two independent-but-equivalent handlers. Register via\n * `node.handlers.on(1059, handler)` (Story 12.7).\n *\n * @throws {SwapHandlerError} At construction time if config is malformed.\n */\nexport function createSwapHandler(config: CreateSwapHandlerConfig): Handler {\n // Construction-time validation (pre-empt Story 12.2 retro finding #1).\n if (\n !(config.recipientSecretKey instanceof Uint8Array) ||\n config.recipientSecretKey.length !== 32\n ) {\n throw new SwapHandlerError(\n 'recipientSecretKey must be a 32-byte Uint8Array'\n );\n }\n if (!Array.isArray(config.swapPairs)) {\n throw new SwapHandlerError('swapPairs must be an array');\n }\n if (\n !config.claimIssuer ||\n typeof config.claimIssuer.issueClaim !== 'function'\n ) {\n throw new SwapHandlerError(\n 'claimIssuer must implement issueClaim(params): Promise<IssueClaimResult>'\n );\n }\n\n const logger = config.logger ?? NOOP_LOGGER;\n\n // Story 12.8 AC-14: when the operator does not supply a custom set,\n // default to a bounded access-order-LRU set. Operator-supplied sets are\n // used verbatim (we do not second-guess the operator's choice of\n // persistence / bounding).\n const seenPacketIds: SeenPacketIdsLike =\n config.seenPacketIds ?? new BoundedSeenPacketIds();\n\n return async (ctx) => {\n // AC-4: defensive kind guard. HandlerRegistry.dispatch already routes by\n // kind, but a mis-registered handler should fail loudly rather than\n // silently mutate unrelated traffic.\n if (ctx.kind !== 1059) {\n // Generic reject message -- do not leak handler role to the caller.\n // A swap handler registered for non-1059 traffic is a mis-configuration;\n // the caller doesn't need to know which handler fielded the packet.\n return ctx.reject('F02', 'Unreachable');\n }\n\n // Defense-in-depth: reject non-positive amounts eagerly with a dedicated\n // code so the sender gets an unambiguous error (otherwise applyRate would\n // throw and surface as the generic T00 \"Rate conversion error\"). ILP\n // connectors already enforce amount > 0, but we double-check at the\n // protocol boundary.\n if (typeof ctx.amount !== 'bigint' || ctx.amount <= 0n) {\n logger.warn({\n event: 'swap_handler.invalid_amount',\n destination: ctx.destination,\n });\n return ctx.reject('F01', 'Invalid amount');\n }\n\n // AC-4 / AC-5 / AC-6: decode and unwrap. `ctx.toon` is the base64 string\n // lifted verbatim from `ilpPrepare.data` (which `buildIlpPrepare` produces\n // by base64-encoding the raw TOON binary). Single decode -> TOON bytes.\n //\n // NOTE: `ctx.pubkey` is the OUTER ephemeral gift-wrap pubkey, NOT the real\n // sender. The real sender comes from the seal inside\n // unwrapSwapPacketFromToon. Do not use `ctx.pubkey` for sender identity.\n if (typeof ctx.toon !== 'string' || ctx.toon.length === 0) {\n logger.warn({\n event: 'swap_handler.invalid_toon',\n destination: ctx.destination,\n });\n return ctx.reject('F01', 'Invalid gift wrap');\n }\n let rumor: UnsignedEvent;\n let senderPubkey: string;\n try {\n const toonData = new Uint8Array(Buffer.from(ctx.toon, 'base64'));\n ({ rumor, senderPubkey } = unwrapSwapPacketFromToon({\n toonData,\n recipientSecretKey: config.recipientSecretKey,\n }));\n } catch (err) {\n if (err instanceof GiftWrapError) {\n logger.warn({\n event: 'swap_handler.unwrap_failed',\n destination: ctx.destination,\n error: err.message,\n });\n return ctx.reject('F01', 'Invalid gift wrap');\n }\n logger.error({\n event: 'swap_handler.unwrap_unexpected_error',\n destination: ctx.destination,\n error: err instanceof Error ? err.message : String(err),\n });\n return ctx.reject('F01', 'Invalid gift wrap');\n }\n\n // AC-7: pair lookup\n const pair = findSwapPair(rumor, config.swapPairs);\n if (!pair) {\n logger.debug({\n event: 'swap_handler.unsupported_pair',\n destination: ctx.destination,\n });\n return ctx.reject('F06', 'Unsupported swap pair');\n }\n\n // Story 12.9 AC-1 / AC-8: extract and validate the `chain-recipient`\n // tag from the inner rumor. Missing or malformed values are treated as\n // a malformed rumor (T00, consistent with other opaque-internal-error\n // paths) — the sender may retry with a corrected address.\n const chainRecipient = findChainRecipient(rumor, pair.to.chain);\n if (!chainRecipient) {\n logger.debug({\n event: 'swap_handler.malformed_rumor',\n destination: ctx.destination,\n reason: 'missing_or_malformed_chain_recipient',\n chain: pair.to.chain,\n });\n return ctx.reject('T00', 'Internal error');\n }\n\n // AC-11: replay protection check. We RESERVE the packetId synchronously\n // here (before the first `await`) so that two concurrent invocations with\n // an identical packet ID cannot both pass the `has()` gate. Because the\n // JS event loop is cooperative, the check-and-add pair is atomic relative\n // to other microtasks as long as it straddles no `await`. If issuance or\n // encryption later fails, we release the reservation so the sender can\n // legitimately retry (AC-11 requires retries of rejected packets).\n // Story 12.8 AC-14: replay check always runs (against the bounded default\n // when the operator did not supply a custom set).\n const packetId: string = computePacketId(senderPubkey, ctx.amount, rumor);\n if (seenPacketIds.has(packetId)) {\n logger.debug({\n event: 'swap_handler.duplicate_packet',\n packetId,\n });\n return ctx.reject('F04', 'Duplicate packet');\n }\n // Reserve eagerly to close the concurrent check-then-add race.\n seenPacketIds.add(packetId);\n\n // Helper: release the replay reservation on failure so the sender can retry.\n const releaseReservation = (): void => {\n seenPacketIds.delete(packetId);\n };\n\n // AC-9 rate resolution (optional live hook per D12-006).\n let rate: string;\n try {\n rate = config.rateProvider ? await config.rateProvider(pair) : pair.rate;\n } catch (err) {\n logger.error({\n event: 'swap_handler.rate_provider_failed',\n error: err instanceof Error ? err.message : String(err),\n });\n releaseReservation();\n return ctx.reject('T00', 'Rate provider error');\n }\n\n // AC-8: apply rate (BigInt throughout).\n let targetAmount: bigint;\n try {\n targetAmount = applyRate({\n sourceAmount: ctx.amount,\n fromScale: pair.from.assetScale,\n toScale: pair.to.assetScale,\n rate,\n });\n logger.debug({\n event: 'swap_handler.rate_applied',\n sourceAmount: ctx.amount.toString(),\n targetAmount: targetAmount.toString(),\n rate,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.warn({\n event: 'swap_handler.rate_conversion_failed',\n error: msg,\n });\n // Do NOT surface the internal rate string / validation detail to the\n // sender -- return a generic privacy-preserving message.\n releaseReservation();\n return ctx.reject('T00', 'Rate conversion error');\n }\n\n // AC-9: delegate to claim issuer\n let claim: Uint8Array;\n let claimId: string | undefined;\n let settlementChannelId: string | undefined;\n let settlementNonce: bigint | undefined;\n let settlementCumulative: bigint | undefined;\n let settlementRecipient: string | undefined;\n let settlementMillSigner: string | undefined;\n try {\n const result = await config.claimIssuer.issueClaim({\n sourceAmount: ctx.amount,\n targetAmount,\n pair,\n senderPubkey,\n chainRecipient,\n rumor,\n });\n claim = result.claim;\n claimId = result.claimId;\n settlementChannelId = result.channelId;\n settlementNonce = result.nonce;\n settlementCumulative = result.cumulativeAmount;\n settlementRecipient = result.recipient;\n settlementMillSigner = result.millSignerAddress;\n } catch (err) {\n const code = (err as { code?: unknown })?.code;\n const message = err instanceof Error ? err.message : String(err);\n if (code === 'INSUFFICIENT_INVENTORY' || /insufficient/i.test(message)) {\n logger.warn({\n event: 'swap_handler.insufficient_inventory',\n error: message,\n });\n releaseReservation();\n return ctx.reject('T04', 'Insufficient liquidity');\n }\n logger.error({\n event: 'swap_handler.issuer_failed',\n error: message,\n });\n releaseReservation();\n return ctx.reject('T00', 'Internal error');\n }\n\n // AC-10: NIP-44 encrypt the claim (Story 12.2 handles ephemeral-key zeroing).\n let ciphertext: Uint8Array;\n let ephemeralPubkey: string;\n try {\n const enc = encryptFulfillClaim({ claimData: claim, senderPubkey });\n ciphertext = enc.ciphertext;\n ephemeralPubkey = enc.ephemeralPubkey;\n } catch (err) {\n logger.error({\n event: 'swap_handler.encrypt_failed',\n error: err instanceof Error ? err.message : String(err),\n });\n releaseReservation();\n return ctx.reject('T00', 'Internal error');\n }\n\n // AC-11: packetId was reserved pre-issuance to close the concurrent\n // check-then-add race; it remains committed on this success path.\n\n const claimBase64 = Buffer.from(ciphertext).toString('base64');\n logger.info({\n event: 'swap_handler.claim_issued',\n claimId,\n ephemeralPubkey,\n });\n\n // Story 12.5: emit the Mill-computed `targetAmount` (decimal string) so\n // the sender can run an end-to-end rate-deviation check without having to\n // parse the opaque chain-specific `claimBytes`. This is additive and\n // backward compatible — legacy senders that ignore the field get the\n // same {claim, ephemeralPubkey, claimId?} shape they always did.\n const metadata: Record<string, unknown> = {\n claim: claimBase64,\n ephemeralPubkey,\n targetAmount: targetAmount.toString(),\n };\n if (claimId !== undefined) metadata['claimId'] = claimId;\n // Story 12.6: thread settlement-context fields through when the claim\n // issuer emits them. All-or-nothing: a Mill that supplies settlement\n // fields MUST supply all five, since the sender's FULFILL decoder\n // enforces all-present-or-all-absent.\n if (\n settlementChannelId !== undefined &&\n settlementNonce !== undefined &&\n settlementCumulative !== undefined &&\n settlementRecipient !== undefined &&\n settlementMillSigner !== undefined\n ) {\n metadata['channelId'] = settlementChannelId;\n metadata['nonce'] = settlementNonce.toString();\n metadata['cumulativeAmount'] = settlementCumulative.toString();\n metadata['recipient'] = settlementRecipient;\n metadata['millSignerAddress'] = settlementMillSigner;\n }\n\n return ctx.accept(metadata);\n };\n}\n\n// ---------------------------------------------------------------------------\n// Replay packet ID hash\n// ---------------------------------------------------------------------------\n\n/**\n * Compute a deterministic packet ID for replay protection.\n *\n * Uses explicit length-prefix delimiters between the three inputs so that\n * `('ab','c12','3')` and `('abc','12','3')` produce distinct digests. Without\n * delimiters, variable-width string concatenation is ambiguous under hashing.\n */\nfunction computePacketId(\n senderPubkey: string,\n sourceAmount: bigint,\n rumor: UnsignedEvent\n): string {\n const rumorId = (rumor as UnsignedEvent & { id?: string }).id ?? '';\n const hash = createHash('sha256');\n const parts = [senderPubkey, sourceAmount.toString(), rumorId];\n for (const p of parts) {\n // 4-byte big-endian length prefix followed by UTF-8 bytes.\n const buf = Buffer.from(p, 'utf8');\n const lenBuf = Buffer.alloc(4);\n lenBuf.writeUInt32BE(buf.length, 0);\n hash.update(lenBuf);\n hash.update(buf);\n }\n return hash.digest('hex');\n}\n","/**\n * Sender-side `streamSwap()` API (Story 12.5).\n *\n * Drives the sender side of the Token Swap Primitive: chunks a total source\n * amount into N sender-chosen packets, wraps each with `wrapSwapPacketToToon()`\n * using a fresh ephemeral gift-wrap key per packet (D12-003 / risk R-006),\n * sends each via `ToonClient.sendSwapPacket(...)`, decrypts each FULFILL's\n * NIP-44-encrypted claim with `decryptFulfillClaim()`, accumulates the\n * decrypted claims into an ordered array, invokes a per-packet rate monitoring\n * callback, and supports pause / resume / stop / AbortSignal so the sender\n * can abort when rate drifts past tolerance.\n *\n * Composition story: consumes Stories 12.1 (SwapPair type), 12.2 (gift wrap\n * primitives), 12.3 (handler wire contract). Produces `AccumulatedClaim[]`\n * consumed by Story 12.6 (`buildSettlementTx()`).\n *\n * Design decisions:\n * - `streamSwap()` does NOT throw on mid-stream failure. Caller gets\n * `StreamSwapResult` with `state` and `abortReason`. Only construction-time\n * validation throws synchronously.\n * - `streamSwap()` does NOT retry individual packets. BTP-level connection\n * retries are handled inside `BtpRuntimeClient`; application-layer retry\n * (e.g., T04 insufficient inventory) is a future-story concern.\n * - Rate deviation math stays BigInt throughout. The `effectiveRate` on\n * `PacketProgress` is a display-only `number` (Epic 11 retro MAX_SAFE_INTEGER\n * guard).\n *\n * @module\n */\n\nimport { getPublicKey } from 'nostr-tools/pure';\nimport type { UnsignedEvent } from 'nostr-tools/pure';\nimport type { SwapPair } from '@toon-protocol/core';\n\nimport { StreamSwapError } from './errors.js';\nimport { wrapSwapPacketToToon, decryptFulfillClaim } from './gift-wrap.js';\nimport { base58Decode } from './identity.js';\nimport { applyRate } from './swap-handler.js';\n\n// ---------------------------------------------------------------------------\n// Minimal structural shapes (avoid cross-package type imports)\n// ---------------------------------------------------------------------------\n\n/** Minimal `IlpSendResult` shape — mirrors `packages/client/src/types.ts`. */\ninterface IlpSendResultLike {\n accepted: boolean;\n data?: string;\n code?: string;\n message?: string;\n}\n\n/**\n * Minimal `SignedBalanceProof` structural type.\n *\n * We avoid importing from `@toon-protocol/client` so the SDK package does not\n * gain a runtime dep on the client package. The sender layer only forwards\n * this opaquely to `ToonClient.sendSwapPacket`.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- opaque forwarded type\ntype SignedBalanceProofLike = any;\n\n/**\n * The subset of `ToonClient` that `streamSwap()` requires. This narrow shape\n * exists so that consumers can pass a real `ToonClient` OR a mock without\n * importing the whole class.\n */\nexport interface StreamSwapClient {\n sendSwapPacket(params: {\n destination: string;\n amount: bigint;\n toonData: Uint8Array;\n timeout?: number;\n claim?: SignedBalanceProofLike;\n }): Promise<IlpSendResultLike>;\n getPublicKey(): string;\n}\n\n// ---------------------------------------------------------------------------\n// Public interfaces (AC-2, AC-7, AC-8, AC-9, AC-10)\n// ---------------------------------------------------------------------------\n\n/**\n * Parameters for {@link streamSwap} / {@link streamSwapControlled}.\n *\n * @stable — downstream Stories 12.6 and 12.8 depend on this shape.\n */\nexport interface StreamSwapParams {\n /** SDK client with BTP wiring (see {@link StreamSwapClient}). */\n client: StreamSwapClient;\n /** Mill's 64-char lowercase hex pubkey (recipient of gift wrap). */\n millPubkey: string;\n /** Mill's ILP destination address (e.g., 'g.toon.mill1'). */\n millIlpAddress: string;\n /** The `SwapPair` being executed (from kind:10032 discovery, Story 12.1). */\n pair: SwapPair;\n /** Sender's 32-byte secp256k1 secret key. Used for seal signing AND FULFILL decryption. */\n senderSecretKey: Uint8Array;\n /**\n * Sender's chain-specific payout address for `pair.to.chain` (Story 12.9 AC-4).\n *\n * REQUIRED. The Nostr `senderPubkey` (identity layer) cannot be used as the\n * on-chain `recipient` in a balance-proof because they are cryptographically\n * independent keys (D12-011). For `evm:*` chains this must be a 20-byte\n * lowercased `0x`-prefixed hex address; for `solana:*` a base58 pubkey that\n * decodes to 32 bytes; for `mina:*` a base58 public-key string. The value\n * is validated at `streamSwapControlled()` entry via `validateChainAddress`\n * and echoed on every rumor as the `chain-recipient` tag (AC-6).\n */\n chainRecipient: string;\n /** Total source-asset amount to swap (source micro-units). */\n totalAmount: bigint;\n /** Even-split packet count. EXACTLY ONE of this or `packetAmounts` is required. */\n packetCount?: number;\n /** Explicit per-packet amounts. EXACTLY ONE of this or `packetCount` is required. */\n packetAmounts?: readonly bigint[];\n /** Source-asset balance proof claim. Required unless ChannelManager is wired. */\n claim?: SignedBalanceProofLike;\n /** Rate monitoring callback (fires after each accepted FULFILL). */\n onPacket?: RateMonitorCallback;\n /** Rate deviation threshold (decimal, e.g., 0.02 = 2%). */\n rateDeviationThreshold?: number;\n /** Per-packet timeout in ms. Default 30000. */\n packetTimeoutMs?: number;\n /** Optional AbortSignal. */\n signal?: AbortSignal;\n /**\n * Optional pino-compatible logger. Defaults to a no-op.\n *\n * Note: `streamSwap` calls each method with a single structured-event object\n * (e.g., `logger.warn({ event: 'stream_swap.packet_rejected', code, ... })`).\n * Pino accepts this form (the object is logged as top-level fields and the\n * message is left empty); other loggers that expect `(msg, meta)` should\n * wrap accordingly.\n */\n logger?: {\n debug: (...a: unknown[]) => void;\n info: (...a: unknown[]) => void;\n warn: (...a: unknown[]) => void;\n error: (...a: unknown[]) => void;\n };\n}\n\n/**\n * Rate monitoring callback. Fires after each successful FULFILL decryption,\n * before the next packet is sent. If the callback throws or rejects, the\n * stream is treated as stopped.\n */\nexport type RateMonitorCallback = (\n progress: PacketProgress\n) => void | Promise<void>;\n\n/**\n * Progress payload delivered to {@link RateMonitorCallback}. Frozen to\n * prevent callback mutation.\n */\nexport interface PacketProgress {\n /** 0-indexed packet number within this streamSwap invocation. */\n index: number;\n /** Total number of packets scheduled. */\n total: number;\n /** Source-asset amount sent for this packet (micro-units). */\n sourceAmount: bigint;\n /** Target-asset claim amount derived for this packet (micro-units). */\n targetAmount: bigint;\n /** Rate advertised on the `SwapPair` at swap start (decimal string). */\n advertisedRate: string;\n /** Effective rate for this packet as JS Number (target / source in whole units). Display-only. */\n effectiveRate: number;\n /** Absolute deviation from advertisedRate as a decimal (e.g., 0.0125 = 1.25%). */\n rateDeviation: number;\n /** Cumulative source sent across accepted packets so far (including this one). */\n cumulativeSource: bigint;\n /** Cumulative target received so far (including this one). */\n cumulativeTarget: bigint;\n /** Controller state at callback time. */\n state: 'running' | 'paused' | 'stopped';\n}\n\n/**\n * An accumulated claim successfully harvested from a single packet.\n *\n * @stable — Story 12.6 (`buildSettlementTx()`) depends on this shape.\n * Breaking changes require a coordinated migration.\n *\n * Story 12.6 ADDITIVE extension: the settlement-context fields\n * `channelId`, `nonce`, `cumulativeAmount`, `recipient`, and\n * `millSignerAddress` are marked optional (`?:`) for one story-cycle of\n * backward compat but are REQUIRED in practice: Story 12.6's\n * `buildSettlementTx()` throws `MISSING_SETTLEMENT_METADATA` when any of\n * these are absent.\n */\nexport interface AccumulatedClaim {\n /** 0-indexed position in the swap's packet stream. */\n packetIndex: number;\n /** Source-asset amount sent for this packet (micro-units). */\n sourceAmount: bigint;\n /**\n * Target-asset amount claimed (micro-units).\n *\n * **Source of truth caveat:** This is the expected target amount computed\n * by `applyRate(pair.rate)`. The actual signed-claim amount lives inside\n * `claimBytes`; Story 12.6 is responsible for parsing `claimBytes` per\n * chain and verifying the on-wire signed amount equals this expected amount.\n */\n targetAmount: bigint;\n /** Decrypted signed claim bytes. Chain-specific encoding per Story 12.4. */\n claimBytes: Uint8Array;\n /** Mill's ephemeral pubkey from the FULFILL (64-char lowercase hex). */\n millEphemeralPubkey: string;\n /** Optional Mill-side claim ID (passed through from handler metadata). */\n claimId?: string;\n /** Swap pair this claim was priced against (copy of `pair` for settlement-time routing). */\n pair: SwapPair;\n /** Unix ms timestamp when this claim was accepted. */\n receivedAt: number;\n // --- Story 12.6 settlement-context fields (additive) ---\n /** Channel identifier on the target chain (lowercase hex with 0x prefix for EVM; base58 for Solana). */\n channelId?: string;\n /** Balance-proof nonce (decimal string). Monotonically increasing within a channel. */\n nonce?: string;\n /** Cumulative transferred amount on the channel (target micro-units, decimal string). */\n cumulativeAmount?: string;\n /** Recipient address (the sender's target-asset address). */\n recipient?: string;\n /** Mill's on-chain signer address. */\n millSignerAddress?: string;\n}\n\n/**\n * Aggregate result of a `streamSwap()` invocation.\n *\n * @stable — Stories 12.6 and 12.8 depend on this shape.\n */\nexport interface StreamSwapResult {\n state: 'completed' | 'failed' | 'stopped';\n claims: AccumulatedClaim[];\n rejections: {\n packetIndex: number;\n sourceAmount: bigint;\n code: string;\n message: string;\n }[];\n errors: { packetIndex: number; cause: Error }[];\n abortReason:\n | 'complete'\n | 'aborted'\n | 'stopped'\n | 'callback-stop'\n | 'callback-throw'\n | 'rate-deviation'\n | 'all-rejected';\n cumulativeSource: bigint;\n cumulativeTarget: bigint;\n packetsSent: number;\n packetsScheduled: number;\n}\n\n/** External controller returned by {@link streamSwapControlled}. */\nexport interface StreamSwapController {\n pause(): void;\n resume(): void;\n stop(): void;\n readonly state: 'running' | 'paused' | 'stopped' | 'completed' | 'failed';\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nconst RATE_REGEX = /^(0|[1-9]\\d*)(\\.\\d+)?$/;\nconst HEX64_REGEX = /^[0-9a-f]{64}$/;\n// Strict base64: only `=` padding at the end, 0/1/2 pad chars. The character\n// class is intentionally restrictive — previous form `/^[A-Za-z0-9+/=]+$/`\n// accepted `=` anywhere and non-multiple-of-4 lengths, which could funnel\n// malformed payloads into `Buffer.from`/`JSON.parse` and surface confusing\n// errors. (Story 12.5 code-review pass #3.)\nconst BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;\n\n/** True iff `s` is a non-empty, base64-shaped string of length multiple of 4. */\nfunction isBase64(s: string): boolean {\n if (s.length === 0 || s.length % 4 !== 0) return false;\n return BASE64_REGEX.test(s);\n}\n\n/** Length-split `total` into `count` bigints. Remainder absorbed on last element. */\nfunction chunkAmount(total: bigint, count: number): bigint[] {\n if (\n !Number.isInteger(count) ||\n count <= 0 ||\n count > Number.MAX_SAFE_INTEGER\n ) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `packetCount must be a positive integer, got ${count}`\n );\n }\n if (total < BigInt(count)) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `totalAmount (${total}) must be >= packetCount (${count}) so per-packet amount >= 1`\n );\n }\n const base = total / BigInt(count);\n const remainder = total - base * BigInt(count);\n const out: bigint[] = new Array(count);\n for (let i = 0; i < count; i++) out[i] = base;\n const last = out[count - 1] ?? 0n;\n out[count - 1] = last + remainder;\n return out;\n}\n\n/** Build the kind:20032 unsigned \"swap rumor\" event per AC-4.\n *\n * Story 12.9 AC-1/AC-6: the returned rumor MUST include a `chain-recipient`\n * tag carrying the sender-supplied chain-format payout address for\n * `pair.to.chain`. The value is echoed verbatim per packet — no case-folding\n * or transformation beyond what the sender-side `validateChainAddress`\n * accepts.\n */\nfunction buildSwapRumor(input: {\n senderPubkey: string;\n pair: SwapPair;\n sourceAmount: bigint;\n packetIndex: number;\n totalPackets: number;\n nonce: Uint8Array;\n createdAt: number;\n chainRecipient: string;\n}): UnsignedEvent {\n const {\n senderPubkey,\n pair,\n sourceAmount,\n packetIndex,\n totalPackets,\n nonce,\n createdAt,\n chainRecipient,\n } = input;\n return {\n kind: 20032,\n pubkey: senderPubkey,\n content: '',\n created_at: createdAt,\n tags: [\n ['swap-from', `${pair.from.assetCode}:${pair.from.chain}`],\n ['swap-to', `${pair.to.assetCode}:${pair.to.chain}`],\n ['amount', sourceAmount.toString()],\n ['seq', String(packetIndex), String(totalPackets)],\n ['nonce', Buffer.from(nonce).toString('hex')],\n ['chain-recipient', chainRecipient],\n ],\n };\n}\n\nconst EVM_CHANNEL_ID_REGEX = /^0x[0-9a-f]{64}$/;\nconst EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;\nconst DECIMAL_UINT_REGEX = /^(0|[1-9]\\d*)$/;\n// Permissive base58 charset (Bitcoin alphabet — no 0, O, I, l).\nconst BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;\n\n/**\n * Validate a chain-specific address or channelId string. `chain` is the\n * `pair.to.chain` string (e.g., `'evm:base:8453'`, `'solana:mainnet'`).\n *\n * Returns true for valid formats; false otherwise.\n */\nexport function validateChainAddress(\n value: string,\n chain: string,\n kind: 'channelId' | 'address'\n): boolean {\n if (chain.startsWith('evm:')) {\n // #153: viem / EIP-55 emits checksummed (mixed-case) addresses, and\n // callers commonly pass a checksummed `chainRecipient`. Lowercase-normalize\n // before the strict-lowercase-hex regex so a valid checksummed address (or\n // channelId) is accepted instead of being spuriously rejected with\n // INVALID_CHAIN_RECIPIENT / FULFILL_DECODE_FAILED.\n const normalized = value.toLowerCase();\n if (kind === 'channelId') return EVM_CHANNEL_ID_REGEX.test(normalized);\n return EVM_ADDRESS_REGEX.test(normalized);\n }\n if (chain.startsWith('solana:')) {\n // AC-3: base58 decode MUST succeed AND length MUST be 32 bytes. A pure\n // regex + char-length sanity check is too loose — a malformed \"32-char\"\n // base58 string may decode to 22–24 bytes and slip through.\n if (!BASE58_REGEX.test(value)) return false;\n if (value.length < 32 || value.length > 44) return false;\n try {\n return base58Decode(value).length === 32;\n } catch {\n return false;\n }\n }\n if (chain.startsWith('mina:')) {\n return BASE58_REGEX.test(value) && value.length >= 32;\n }\n // Unknown chain — permit; settlement layer will surface UNSUPPORTED_CHAIN.\n return value.length > 0;\n}\n\n/**\n * Decode the FULFILL response `data` (base64-encoded JSON metadata) into\n * the `{ claim, ephemeralPubkey, claimId?, targetAmount?, ...settlement }`\n * shape.\n *\n * Story 12.6 extension: also parses the settlement-context fields\n * (`channelId`, `nonce`, `cumulativeAmount`, `recipient`, `millSignerAddress`).\n * These are OPTIONAL and best-effort (#153): each is threaded through only\n * when it is a well-formed string for the target chain; an absent or malformed\n * settlement field is silently dropped rather than failing the whole decode,\n * so a fulfilled swap still surfaces its signed `claim` + `ephemeralPubkey`.\n * Only `claim` and `ephemeralPubkey` are strictly required.\n *\n * @param chain Optional `pair.to.chain` string for per-chain format validation\n * of channelId / recipient / millSignerAddress. When omitted, format checks\n * fall back to a length-only sanity check.\n */\nfunction decodeFulfillMetadata(\n data: string | undefined,\n chain?: string\n): {\n claim: string;\n ephemeralPubkey: string;\n claimId?: string;\n /** Optional Mill-reported actual target amount (decimal string). Used for rate deviation when present. */\n targetAmount?: string;\n channelId?: string;\n nonce?: string;\n cumulativeAmount?: string;\n recipient?: string;\n millSignerAddress?: string;\n} {\n if (data === undefined || data === null || data === '') {\n throw new StreamSwapError('FULFILL_DECODE_FAILED', 'FULFILL data missing');\n }\n // Quick-fail for obvious non-base64 input. Rejects '@' etc. and strings\n // that aren't a multiple-of-4 length.\n if (!isBase64(data)) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL data is not valid base64'\n );\n }\n let jsonBytes: Buffer;\n try {\n jsonBytes = Buffer.from(data, 'base64');\n } catch (err) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n `FULFILL data base64 decode failed: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(jsonBytes.toString('utf8'));\n } catch (err) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n `FULFILL data JSON parse failed: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n if (!parsed || typeof parsed !== 'object') {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL metadata is not an object'\n );\n }\n const obj = parsed as Record<string, unknown>;\n const claim = obj['claim'];\n const ephemeralPubkey = obj['ephemeralPubkey'];\n if (typeof claim !== 'string' || !isBase64(claim)) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL metadata.claim is missing or not base64 string'\n );\n }\n if (\n typeof ephemeralPubkey !== 'string' ||\n !HEX64_REGEX.test(ephemeralPubkey)\n ) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL metadata.ephemeralPubkey is missing or not 64-char hex'\n );\n }\n const result: {\n claim: string;\n ephemeralPubkey: string;\n claimId?: string;\n targetAmount?: string;\n channelId?: string;\n nonce?: string;\n cumulativeAmount?: string;\n recipient?: string;\n millSignerAddress?: string;\n } = {\n claim,\n ephemeralPubkey,\n };\n if (typeof obj['claimId'] === 'string') {\n result.claimId = obj['claimId'] as string;\n }\n // Mill-reported target amount (optional). MUST be a non-negative integer\n // decimal string — reject signed / fractional / non-numeric values so a\n // malicious or buggy Mill cannot poison `cumulativeTarget`, the deviation\n // calc, or the `AccumulatedClaim.targetAmount` surface that Story 12.6\n // settles against. Presence of the field with a malformed value is a\n // FULFILL_DECODE_FAILED — the sender cannot safely consume the metadata.\n if (obj['targetAmount'] !== undefined) {\n const ta = obj['targetAmount'];\n if (typeof ta !== 'string' || !/^(0|[1-9]\\d*)$/.test(ta)) {\n throw new StreamSwapError(\n 'FULFILL_DECODE_FAILED',\n 'FULFILL metadata.targetAmount must be a non-negative integer decimal string'\n );\n }\n result.targetAmount = ta;\n }\n // Story 12.6 settlement-context fields — OPTIONAL, best-effort (#153).\n //\n // These five fields are only consumed downstream by `buildSettlementTx()`\n // (Story 12.6), which performs on-chain target redemption — a flow that is\n // itself #82-bounded (synthetic devnet channels) and independently validates\n // every field it consumes before signing. They are NOT required to surface\n // the mill's signed claim to the caller.\n //\n // The previous contract was all-or-nothing AND hard-failed the entire FULFILL\n // decode (`FULFILL_DECODE_FAILED`) on any partial/malformed settlement field.\n // That rejected otherwise-valid mill FULFILLs whenever the mill's real\n // envelope carried, e.g., a cross-chain channelId (an EVM-style hex channelId\n // echoed on a `solana:`/`mina:` target) or a checksummed address — so a\n // fulfilled swap reported `state: failed` with an empty `claims[]`.\n //\n // New contract: thread each settlement field through ONLY when it is a\n // well-formed string for the target chain; silently drop any field that is\n // absent or malformed. A swap whose mill omits/garbles settlement metadata\n // still completes with the signed claim; `buildSettlementTx()` will then\n // surface `MISSING_SETTLEMENT_METADATA` at settlement time if the caller\n // actually attempts on-chain redemption with an incomplete bundle.\n //\n // `recipient` is special-cased: it is the anti-substitution security check in\n // `runLoop` (the mill must echo the sender-supplied `chainRecipient`). We thread\n // it through whenever it is a non-empty string — even if it fails the chain\n // format check — so the runLoop equality check still fires. The equality\n // comparison there is the real boundary; a format-only mismatch must not be\n // silently dropped (which would skip the check entirely).\n const channelId = obj['channelId'];\n if (\n typeof channelId === 'string' &&\n (!chain || validateChainAddress(channelId, chain, 'channelId'))\n ) {\n result.channelId = channelId;\n }\n const nonce = obj['nonce'];\n if (typeof nonce === 'string' && DECIMAL_UINT_REGEX.test(nonce)) {\n result.nonce = nonce;\n }\n const cumulativeAmount = obj['cumulativeAmount'];\n if (\n typeof cumulativeAmount === 'string' &&\n DECIMAL_UINT_REGEX.test(cumulativeAmount)\n ) {\n result.cumulativeAmount = cumulativeAmount;\n }\n const recipient = obj['recipient'];\n if (typeof recipient === 'string' && recipient.length > 0) {\n // Always thread a present recipient so the runLoop anti-substitution\n // equality check (`metadata.recipient === params.chainRecipient`) runs.\n result.recipient = recipient;\n }\n const millSignerAddress = obj['millSignerAddress'];\n if (\n typeof millSignerAddress === 'string' &&\n (!chain || validateChainAddress(millSignerAddress, chain, 'address'))\n ) {\n result.millSignerAddress = millSignerAddress;\n }\n return result;\n}\n\n/** Simple Deferred for pause/resume gating. */\nclass Deferred<T> {\n readonly promise: Promise<T>;\n resolve!: (v: T) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mirror Promise constructor\n reject!: (e: any) => void;\n constructor() {\n this.promise = new Promise<T>((res, rej) => {\n this.resolve = res;\n this.reject = rej;\n });\n }\n}\n\nconst noop = (): void => undefined;\nconst NOOP_LOGGER: NonNullable<StreamSwapParams['logger']> = {\n debug: noop,\n info: noop,\n warn: noop,\n error: noop,\n};\n\n// ---------------------------------------------------------------------------\n// Validation (AC-2) — synchronous, fires before any packet\n// ---------------------------------------------------------------------------\n\nfunction validateParams(params: StreamSwapParams): void {\n if (typeof params.totalAmount !== 'bigint' || params.totalAmount <= 0n) {\n throw new StreamSwapError(\n 'INVALID_AMOUNT',\n `totalAmount must be a positive bigint, got ${String(params.totalAmount)}`\n );\n }\n\n const hasCount = params.packetCount !== undefined;\n const hasAmounts = params.packetAmounts !== undefined;\n if (hasCount === hasAmounts) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n 'Exactly one of packetCount or packetAmounts must be provided'\n );\n }\n\n if (hasCount) {\n const c = params.packetCount as number;\n if (!Number.isInteger(c) || c <= 0) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `packetCount must be a positive integer, got ${c}`\n );\n }\n if (BigInt(c) > params.totalAmount) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `packetCount (${c}) exceeds totalAmount (${params.totalAmount}); per-packet amount would be < 1 micro-unit`\n );\n }\n } else {\n const arr = params.packetAmounts as readonly bigint[];\n if (!Array.isArray(arr) || arr.length === 0) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n 'packetAmounts must be a non-empty array'\n );\n }\n let sum = 0n;\n for (const a of arr) {\n if (typeof a !== 'bigint' || a <= 0n) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `packetAmounts entries must be positive bigint, got ${String(a)}`\n );\n }\n sum += a;\n }\n if (sum !== params.totalAmount) {\n throw new StreamSwapError(\n 'INVALID_CHUNKING',\n `sum(packetAmounts) (${sum}) !== totalAmount (${params.totalAmount})`\n );\n }\n }\n\n if (\n !(params.senderSecretKey instanceof Uint8Array) ||\n params.senderSecretKey.length !== 32\n ) {\n throw new StreamSwapError(\n 'INVALID_STATE',\n 'senderSecretKey must be a 32-byte Uint8Array'\n );\n }\n\n if (\n typeof params.millPubkey !== 'string' ||\n !HEX64_REGEX.test(params.millPubkey)\n ) {\n throw new StreamSwapError(\n 'INVALID_STATE',\n 'millPubkey must be a 64-char lowercase hex string'\n );\n }\n\n if (!params.pair || typeof params.pair !== 'object') {\n throw new StreamSwapError('INVALID_PAIR', 'pair is required');\n }\n // Guard the nested `from` / `to` shape before deep-reading `.assetScale` etc.\n // Otherwise malformed input produces a bare TypeError at `applyRate()` or\n // `buildSwapRumor()` instead of a categorized StreamSwapError.\n if (\n !params.pair.from ||\n typeof params.pair.from !== 'object' ||\n typeof params.pair.from.assetCode !== 'string' ||\n typeof params.pair.from.assetScale !== 'number' ||\n typeof params.pair.from.chain !== 'string'\n ) {\n throw new StreamSwapError(\n 'INVALID_PAIR',\n 'pair.from must have { assetCode: string, assetScale: number, chain: string }'\n );\n }\n if (\n !params.pair.to ||\n typeof params.pair.to !== 'object' ||\n typeof params.pair.to.assetCode !== 'string' ||\n typeof params.pair.to.assetScale !== 'number' ||\n typeof params.pair.to.chain !== 'string'\n ) {\n throw new StreamSwapError(\n 'INVALID_PAIR',\n 'pair.to must have { assetCode: string, assetScale: number, chain: string }'\n );\n }\n if (\n typeof params.pair.rate !== 'string' ||\n !RATE_REGEX.test(params.pair.rate)\n ) {\n throw new StreamSwapError(\n 'INVALID_PAIR',\n `pair.rate must match ${RATE_REGEX}, got ${params.pair.rate}`\n );\n }\n try {\n applyRate({\n sourceAmount: 1n,\n fromScale: params.pair.from.assetScale,\n toScale: params.pair.to.assetScale,\n rate: params.pair.rate,\n });\n } catch (err) {\n throw new StreamSwapError(\n 'INVALID_PAIR',\n `pair failed applyRate sanity check: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err }\n );\n }\n\n if (\n params.rateDeviationThreshold !== undefined &&\n (typeof params.rateDeviationThreshold !== 'number' ||\n !Number.isFinite(params.rateDeviationThreshold) ||\n params.rateDeviationThreshold < 0)\n ) {\n throw new StreamSwapError(\n 'INVALID_STATE',\n `rateDeviationThreshold must be a non-negative finite number, got ${params.rateDeviationThreshold}`\n );\n }\n\n // Story 12.9 AC-4 / AC-5: `chainRecipient` is REQUIRED and MUST validate\n // against `pair.to.chain`. Defense-in-depth for JS callers who bypass the\n // TS interface (the field is declared non-optional on StreamSwapParams).\n if (\n typeof params.chainRecipient !== 'string' ||\n params.chainRecipient.length === 0\n ) {\n throw new StreamSwapError(\n 'INVALID_STATE',\n 'chainRecipient must be a non-empty string (sender payout address for pair.to.chain)'\n );\n }\n if (\n !validateChainAddress(\n params.chainRecipient,\n params.pair.to.chain,\n 'address'\n )\n ) {\n throw new StreamSwapError(\n 'INVALID_CHAIN_RECIPIENT',\n `chainRecipient ${params.chainRecipient} is malformed for chain ${params.pair.to.chain}`\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Core: streamSwapControlled (AC-6, AC-7, AC-10)\n// ---------------------------------------------------------------------------\n\n/**\n * Drive a multi-packet swap against a Mill and return a `StreamSwapResult`.\n *\n * `streamSwap()` does NOT throw on mid-stream failure — inspect the result's\n * `state`, `abortReason`, `rejections[]`, and `errors[]` to diagnose.\n *\n * @example\n * ```ts\n * // Discover the SwapPair from the Mill's kind:10032 peer-info event (Story 12.1).\n * const result = await streamSwap({\n * client: toonClient,\n * millPubkey: mill.pubkey,\n * millIlpAddress: 'g.toon.mill1',\n * pair,\n * senderSecretKey,\n * totalAmount: 1_000_000n,\n * packetCount: 10,\n * onPacket: (p) => console.log('packet', p.index, 'rate', p.effectiveRate),\n * rateDeviationThreshold: 0.02,\n * });\n * // Feed result.claims into buildSettlementTx() from Story 12.6.\n * ```\n *\n * @throws {StreamSwapError} Only for construction-time validation failures\n * (INVALID_AMOUNT, INVALID_CHUNKING, INVALID_PAIR, INVALID_STATE).\n */\nexport async function streamSwap(\n params: StreamSwapParams\n): Promise<StreamSwapResult> {\n // Note: validation errors are thrown synchronously inside\n // `streamSwapControlled`. We wrap the call so construction-time throws\n // become Promise rejections for ergonomic `await` / `.rejects` handling.\n return streamSwapControlled(params).result;\n}\n\n/**\n * Two-form variant of {@link streamSwap} that additionally returns a\n * {@link StreamSwapController} with `pause()` / `resume()` / `stop()`.\n *\n * @throws {StreamSwapError} Synchronously for construction-time validation.\n */\nexport function streamSwapControlled(params: StreamSwapParams): {\n result: Promise<StreamSwapResult>;\n controller: StreamSwapController;\n} {\n // Construction-time validation — synchronous throw per AC-2 & AC-9.\n validateParams(params);\n\n const logger = params.logger ?? NOOP_LOGGER;\n\n // Derive schedule\n const schedule: bigint[] = params.packetAmounts\n ? [...params.packetAmounts]\n : chunkAmount(params.totalAmount, params.packetCount as number);\n\n // Freeze a defensive copy of `pair` so callers can't mutate the stored\n // reference on every AccumulatedClaim post-call. (Story 12.5 code-review\n // pass #3.) Shape matches SwapPair (Story 12.1 stable type).\n const frozenPair: SwapPair = Object.freeze({\n from: Object.freeze({ ...params.pair.from }),\n to: Object.freeze({ ...params.pair.to }),\n rate: params.pair.rate,\n }) as SwapPair;\n\n const senderPubkey = getPublicKey(params.senderSecretKey);\n\n // Controller state machine\n type State = 'running' | 'paused' | 'stopped' | 'completed' | 'failed';\n let streamState: State = 'running';\n let resumeDeferred: Deferred<'resume' | 'stop'> | null = null;\n\n const controller: StreamSwapController = {\n pause(): void {\n if (streamState === 'running') {\n streamState = 'paused';\n // resumeDeferred will be created on first await in loop\n }\n },\n resume(): void {\n if (streamState === 'paused') {\n streamState = 'running';\n if (resumeDeferred) {\n resumeDeferred.resolve('resume');\n resumeDeferred = null;\n }\n } else if (streamState === 'running') {\n // no-op\n } else {\n throw new StreamSwapError(\n 'INVALID_STATE',\n `Cannot resume from state \"${streamState}\"`\n );\n }\n },\n stop(): void {\n if (streamState === 'completed' || streamState === 'failed') return;\n const prev = streamState;\n streamState = 'stopped';\n if (prev === 'paused' && resumeDeferred) {\n resumeDeferred.resolve('stop');\n resumeDeferred = null;\n }\n },\n get state(): State {\n return streamState;\n },\n };\n\n const result = runLoop(\n params,\n frozenPair,\n schedule,\n senderPubkey,\n logger,\n () => streamState,\n (v: State) => {\n streamState = v;\n },\n () => {\n if (streamState !== 'paused') return Promise.resolve('resume');\n if (!resumeDeferred) resumeDeferred = new Deferred<'resume' | 'stop'>();\n return resumeDeferred.promise;\n }\n );\n\n return { result, controller };\n}\n\n// ---------------------------------------------------------------------------\n// Core loop\n// ---------------------------------------------------------------------------\n\nasync function runLoop(\n params: StreamSwapParams,\n pair: SwapPair,\n schedule: bigint[],\n senderPubkey: string,\n logger: NonNullable<StreamSwapParams['logger']>,\n getState: () => 'running' | 'paused' | 'stopped' | 'completed' | 'failed',\n setState: (\n v: 'running' | 'paused' | 'stopped' | 'completed' | 'failed'\n ) => void,\n waitForResumeOrStop: () => Promise<'resume' | 'stop'>\n): Promise<StreamSwapResult> {\n const claims: AccumulatedClaim[] = [];\n const rejections: StreamSwapResult['rejections'] = [];\n const errors: StreamSwapResult['errors'] = [];\n let cumulativeSource = 0n;\n let cumulativeTarget = 0n;\n let packetsSent = 0;\n let abortReason: StreamSwapResult['abortReason'] = 'complete';\n\n const totalPackets = schedule.length;\n\n // Snapshot the signal abort state at each check — we don't listen via\n // addEventListener because we check at loop boundaries (between packets).\n const isAborted = (): boolean => params.signal?.aborted === true;\n\n packetLoop: for (\n let packetIndex = 0;\n packetIndex < totalPackets;\n packetIndex++\n ) {\n // --- Abort/stop/pause checks at loop boundary ---\n if (isAborted()) {\n abortReason = 'aborted';\n break;\n }\n if (getState() === 'stopped') {\n abortReason = 'stopped';\n break;\n }\n if (getState() === 'paused') {\n const resumedBy = await waitForResumeOrStop();\n if (resumedBy === 'stop' || getState() === 'stopped') {\n abortReason = 'stopped';\n break;\n }\n // After resume, re-check abort signal\n if (isAborted()) {\n abortReason = 'aborted';\n break;\n }\n }\n\n // Bounds-checked at loop init (`packetIndex < totalPackets === schedule.length`).\n // Narrow out the `undefined` TS widening without silently masking with 0n,\n // which would hide a real bug if the schedule were ever mutated mid-loop.\n // (Story 12.5 code-review pass #3.)\n const sourceAmount = schedule[packetIndex];\n if (sourceAmount === undefined) {\n // Defensive: should be impossible given the bound check. Surface, don't mask.\n throw new StreamSwapError(\n 'INVALID_STATE',\n `schedule[${packetIndex}] is undefined; schedule was mutated mid-stream`\n );\n }\n\n // --- Build + wrap packet ---\n const nonce = new Uint8Array(16);\n getRandomValues(nonce);\n const rumor = buildSwapRumor({\n senderPubkey,\n pair,\n sourceAmount,\n packetIndex: packetIndex + 1,\n totalPackets,\n nonce,\n createdAt: Math.floor(Date.now() / 1000),\n chainRecipient: params.chainRecipient,\n });\n\n let toonData: Uint8Array;\n try {\n const wrapped = wrapSwapPacketToToon({\n rumor,\n senderSecretKey: params.senderSecretKey,\n recipientPubkey: params.millPubkey,\n destination: params.millIlpAddress,\n amount: sourceAmount,\n });\n // `wrapped.ilpPrepare.data` is base64 per buildIlpPrepare. Decode back\n // to raw bytes for the sender API (Uint8Array contract in AC-3).\n toonData = new Uint8Array(Buffer.from(wrapped.ilpPrepare.data, 'base64'));\n } catch (err) {\n logger.error({\n event: 'stream_swap.wrap_failed',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n continue;\n }\n\n // --- Send packet via client ---\n let sendResult: IlpSendResultLike;\n try {\n sendResult = await params.client.sendSwapPacket({\n destination: params.millIlpAddress,\n amount: sourceAmount,\n toonData,\n timeout: params.packetTimeoutMs ?? 30000,\n claim: params.claim,\n });\n packetsSent += 1;\n } catch (err) {\n logger.error({\n event: 'stream_swap.send_failed',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n continue;\n }\n\n // --- Rejection path ---\n if (!sendResult.accepted) {\n const code = sendResult.code ?? 'F00';\n const message = sendResult.message ?? 'rejected';\n logger.warn({\n event: 'stream_swap.packet_rejected',\n packetIndex,\n code,\n message,\n });\n rejections.push({ packetIndex, sourceAmount, code, message });\n continue;\n }\n\n // --- Decode FULFILL metadata ---\n let metadata: {\n claim: string;\n ephemeralPubkey: string;\n claimId?: string;\n targetAmount?: string;\n channelId?: string;\n nonce?: string;\n cumulativeAmount?: string;\n recipient?: string;\n millSignerAddress?: string;\n };\n try {\n metadata = decodeFulfillMetadata(sendResult.data, pair.to.chain);\n } catch (err) {\n logger.error({\n event: 'stream_swap.fulfill_decode_failed',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n continue;\n }\n\n // Story 12.9 AC-7: when the Mill echoes a `recipient` in FULFILL metadata\n // (Story 12.6 settlement context), it MUST equal the sender-supplied\n // `chainRecipient`. A mismatch indicates the Mill is substituting its own\n // address — refuse to accumulate the claim and surface a per-packet\n // rejection with a clear reason code. Missing recipient = legacy\n // (pre-12.6) metadata, permitted.\n //\n // #153: EVM addresses are case-insensitive (EIP-55 checksum casing is\n // purely a typo-detection hint). The mill lowercases its echoed recipient\n // while the sender may pass a checksummed `chainRecipient` (or vice versa);\n // compare case-insensitively on EVM targets so a casing-only difference is\n // NOT flagged as a substitution attack. Non-EVM chains keep the exact\n // (base58 case-sensitive) comparison.\n const isEvmTarget = pair.to.chain.startsWith('evm:');\n const recipientMatches =\n metadata.recipient === undefined ||\n (isEvmTarget\n ? metadata.recipient.toLowerCase() ===\n params.chainRecipient.toLowerCase()\n : metadata.recipient === params.chainRecipient);\n if (!recipientMatches) {\n logger.warn({\n event: 'stream_swap.recipient_mismatch',\n packetIndex,\n expected: params.chainRecipient,\n actual: metadata.recipient,\n });\n rejections.push({\n packetIndex,\n sourceAmount,\n code: 'MILL_RECIPIENT_MISMATCH',\n message: `Mill echoed recipient ${metadata.recipient} but sender expected ${params.chainRecipient}`,\n });\n continue;\n }\n\n // --- Decrypt claim ---\n let claimBytes: Uint8Array;\n try {\n const ciphertext = new Uint8Array(Buffer.from(metadata.claim, 'base64'));\n claimBytes = decryptFulfillClaim({\n ciphertext,\n ephemeralPubkey: metadata.ephemeralPubkey,\n recipientSecretKey: params.senderSecretKey,\n });\n } catch (err) {\n logger.error({\n event: 'stream_swap.decrypt_failed',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n continue;\n }\n\n if (claimBytes.length === 0) {\n logger.warn({\n event: 'stream_swap.empty_claim_bytes',\n packetIndex,\n });\n }\n\n // --- Compute expected + actual target + deviation ---\n const expectedTargetAmount = applyRate({\n sourceAmount,\n fromScale: pair.from.assetScale,\n toScale: pair.to.assetScale,\n rate: pair.rate,\n });\n\n // If the Mill includes a `targetAmount` in the FULFILL metadata, use it\n // (chain-specific parsing of `claimBytes` is Story 12.6's job). Otherwise\n // fall back to the advertised-rate expected amount so settlement-time\n // verification still has a baseline.\n // `metadata.targetAmount`, when present, is already validated as a\n // non-negative integer decimal string by `decodeFulfillMetadata`, so\n // `BigInt()` is safe here (no try/catch needed).\n const targetAmount: bigint =\n metadata.targetAmount !== undefined\n ? BigInt(metadata.targetAmount)\n : expectedTargetAmount;\n\n // BigInt-safe deviation: scale up by 1e6 before dividing so we don't lose\n // precision on 18-decimal assets. (Epic 11 retro MAX_SAFE_INTEGER guard.)\n let rateDeviation = 0;\n if (expectedTargetAmount > 0n) {\n const diff =\n targetAmount >= expectedTargetAmount\n ? targetAmount - expectedTargetAmount\n : expectedTargetAmount - targetAmount;\n const scaled = (diff * 1_000_000n) / expectedTargetAmount;\n rateDeviation = Number(scaled) / 1_000_000;\n }\n\n // Display-only effectiveRate for the callback payload. Guard against\n // non-finite results (e.g., advertisedRate=0 + any deviation -> 0;\n // parseFloat surprises) so callback consumers never observe NaN/Infinity.\n const advertisedRate = parseFloat(pair.rate);\n let effectiveRate: number;\n if (targetAmount === expectedTargetAmount) {\n effectiveRate = advertisedRate;\n } else {\n const signedDeviation =\n targetAmount >= expectedTargetAmount ? rateDeviation : -rateDeviation;\n effectiveRate = advertisedRate * (1 + signedDeviation);\n }\n if (!Number.isFinite(effectiveRate)) {\n effectiveRate = advertisedRate;\n }\n\n cumulativeSource += sourceAmount;\n cumulativeTarget += targetAmount;\n\n const accumulated: AccumulatedClaim = {\n packetIndex,\n sourceAmount,\n targetAmount,\n claimBytes,\n millEphemeralPubkey: metadata.ephemeralPubkey,\n pair,\n receivedAt: Date.now(),\n };\n if (metadata.claimId !== undefined) accumulated.claimId = metadata.claimId;\n // Story 12.6: thread settlement-context fields through when present.\n if (metadata.channelId !== undefined)\n accumulated.channelId = metadata.channelId;\n if (metadata.nonce !== undefined) accumulated.nonce = metadata.nonce;\n if (metadata.cumulativeAmount !== undefined)\n accumulated.cumulativeAmount = metadata.cumulativeAmount;\n if (metadata.recipient !== undefined)\n accumulated.recipient = metadata.recipient;\n if (metadata.millSignerAddress !== undefined)\n accumulated.millSignerAddress = metadata.millSignerAddress;\n claims.push(accumulated);\n\n logger.debug({\n event: 'stream_swap.packet_accepted',\n packetIndex,\n sourceAmount: sourceAmount.toString(),\n targetAmount: targetAmount.toString(),\n });\n\n // --- onPacket callback ---\n if (params.onPacket) {\n const progress: PacketProgress = Object.freeze({\n index: packetIndex,\n total: totalPackets,\n sourceAmount,\n targetAmount,\n advertisedRate: pair.rate,\n effectiveRate,\n rateDeviation,\n cumulativeSource,\n cumulativeTarget,\n state:\n getState() === 'paused'\n ? 'paused'\n : getState() === 'stopped'\n ? 'stopped'\n : 'running',\n });\n\n try {\n const maybePromise = params.onPacket(progress);\n if (\n maybePromise &&\n typeof (maybePromise as Promise<void>).then === 'function'\n ) {\n await maybePromise;\n }\n } catch (err) {\n logger.warn({\n event: 'stream_swap.callback_threw',\n packetIndex,\n error: err instanceof Error ? err.message : String(err),\n });\n errors.push({\n packetIndex,\n cause: err instanceof Error ? err : new Error(String(err)),\n });\n abortReason = 'callback-throw';\n break packetLoop;\n }\n }\n\n // --- Abort signal / stop check AFTER callback (so tests can abort\n // inside onPacket and we exit the loop on the next iteration's\n // boundary check — but honor stopped/aborted without running\n // additional packets). ---\n if (isAborted()) {\n abortReason = 'aborted';\n break;\n }\n if (getState() === 'stopped') {\n abortReason = 'stopped';\n break;\n }\n\n // --- Rate deviation check (after callback, after accumulation) ---\n if (\n params.rateDeviationThreshold !== undefined &&\n rateDeviation > params.rateDeviationThreshold\n ) {\n abortReason = 'rate-deviation';\n break;\n }\n }\n\n // --- Terminal state ---\n let finalState: 'completed' | 'failed' | 'stopped';\n if (abortReason === 'aborted' || abortReason === 'stopped') {\n finalState = 'stopped';\n } else if (\n claims.length === 0 &&\n (rejections.length > 0 || errors.length > 0)\n ) {\n finalState = 'failed';\n // If we drained the entire schedule without accepting any claim AND the\n // only failures were Mill rejections, surface that explicitly so callers\n // can distinguish \"all rejected\" from \"loop aborted early with no claims\".\n if (\n abortReason === 'complete' &&\n rejections.length > 0 &&\n errors.length === 0\n ) {\n abortReason = 'all-rejected';\n }\n } else {\n finalState = 'completed';\n }\n\n setState(finalState);\n\n return {\n state: finalState,\n claims,\n rejections,\n errors,\n abortReason,\n cumulativeSource,\n cumulativeTarget,\n packetsSent,\n packetsScheduled: totalPackets,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Crypto RNG — prefer globalThis.crypto; fall back to node:crypto.webcrypto.\n// ---------------------------------------------------------------------------\n\nfunction getRandomValues(buf: Uint8Array): Uint8Array {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- defensive env probe\n const g: any = globalThis as any;\n if (g.crypto && typeof g.crypto.getRandomValues === 'function') {\n g.crypto.getRandomValues(buf);\n return buf;\n }\n // Node 18/20 fallback\n // eslint-disable-next-line @typescript-eslint/no-require-imports -- fallback only\n const nodeCrypto = require('node:crypto') as {\n webcrypto?: { getRandomValues: (b: Uint8Array) => Uint8Array };\n randomFillSync?: (b: Uint8Array) => Uint8Array;\n };\n if (nodeCrypto.webcrypto?.getRandomValues) {\n nodeCrypto.webcrypto.getRandomValues(buf);\n return buf;\n }\n if (nodeCrypto.randomFillSync) {\n nodeCrypto.randomFillSync(buf);\n return buf;\n }\n throw new StreamSwapError(\n 'INVALID_STATE',\n 'No crypto.getRandomValues available in this environment'\n );\n}\n\n// Re-export `chunkAmount` / `decodeFulfillMetadata` / `buildSwapRumor` via a\n// single-purpose testing surface so unit tests can exercise helpers directly.\n// This surface is NOT part of the public SDK; it is intentionally excluded\n// from `packages/sdk/src/index.ts` per AC-1.\nexport const __testing = {\n chunkAmount,\n decodeFulfillMetadata,\n buildSwapRumor,\n};\n","/**\n * Persists the apex payment-channel id (+ its chain context) per\n * (destination, chain) so a daemon RESTART can resume the EXISTING on-chain\n * channel instead of opening a new one.\n *\n * Why this is needed: `ChannelManager` persists the off-chain nonce/cumulative\n * watermark (keyed by channelId) but NOT the peer→channelId mapping. So after a\n * restart `openChannel()` would open + re-deposit into a fresh channel, which\n * reverts on a chain where the deposit already exists. With the channelId saved\n * here, the runner instead calls `trackChannel(channelId, context)` — which\n * rehydrates the nonce from the channel store — and signs against the live\n * channel with zero on-chain writes.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\n/** Chain context needed to re-track a channel (matches ChannelManager.trackChannel). */\nexport interface PersistedChannelContext {\n chainType: string;\n chainId: number;\n tokenNetworkAddress: string;\n tokenAddress?: string;\n recipient?: string;\n}\n\nexport interface PersistedApexChannel {\n channelId: string;\n context: PersistedChannelContext;\n}\n\ntype Store = Record<string, PersistedApexChannel>;\n\nfunction key(destination: string, chain: string): string {\n return `${destination}|${chain}`;\n}\n\nfunction readStore(path: string): Store {\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as Store;\n } catch {\n return {};\n }\n}\n\n/** Load the saved apex channel for (destination, chain), or null. */\nexport function loadApexChannel(\n path: string,\n destination: string,\n chain: string\n): PersistedApexChannel | null {\n return readStore(path)[key(destination, chain)] ?? null;\n}\n\n/** Save the apex channel for (destination, chain) with mode 0o600. */\nexport function saveApexChannel(\n path: string,\n destination: string,\n chain: string,\n record: PersistedApexChannel\n): void {\n const store = readStore(path);\n store[key(destination, chain)] = record;\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, JSON.stringify(store, null, 2), { mode: 0o600 });\n}\n","/**\n * Persists DYNAMIC targets — relays added via `toon_add_relay` and apexes added\n * via `toon_add_apex` — to `~/.toon-client/targets.json` so they survive a\n * daemon restart. The config file's single `relayUrl`/`btpUrl` remain the\n * permanent \"default\" target and are NOT stored here; only runtime additions are.\n *\n * On boot the `ClientRunner` seeds the default from config, then replays this\n * store to re-instantiate every dynamically-added relay/apex. Add/remove tool\n * calls write straight back here (last-write-wins, mode 0o600), mirroring the\n * `apex-channel-store.ts` pattern.\n */\n\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { configDir } from './config.js';\nimport type { ApexNegotiationConfig } from './config.js';\n\n/** A persisted relay read target. */\nexport interface PersistedRelayTarget {\n /** Relay WS URL (the map key). */\n relayUrl: string;\n}\n\n/** A persisted apex write target with its discovered settlement negotiation. */\nexport interface PersistedApexTarget {\n /** BTP WS endpoint of the apex (the map key). */\n btpUrl: string;\n /** Settlement negotiation, discovered from the apex's kind:10032 announcement. */\n negotiation: ApexNegotiationConfig;\n /** Child peers reached via this apex's channel (e.g. `[\"dvm\",\"mill\"]`). */\n apexChildPeers?: string[];\n /** Per-write fee override (base units). Falls back to the daemon default. */\n feePerEvent?: string;\n /** Relay the negotiation was discovered on (re-discovery / provenance). */\n discoveredFrom?: string;\n}\n\nexport interface TargetsFile {\n relays: PersistedRelayTarget[];\n apexes: PersistedApexTarget[];\n}\n\nconst EMPTY: TargetsFile = { relays: [], apexes: [] };\n\n/** Default targets-store path: `~/.toon-client/targets.json`. */\nexport function defaultTargetsPath(): string {\n return join(configDir(), 'targets.json');\n}\n\n/** Read + parse the targets store, returning empty arrays when absent/invalid. */\nexport function loadTargets(path = defaultTargetsPath()): TargetsFile {\n let parsed: Partial<TargetsFile>;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf8')) as Partial<TargetsFile>;\n } catch {\n return { relays: [], apexes: [] };\n }\n return {\n relays: Array.isArray(parsed.relays) ? parsed.relays : [],\n apexes: Array.isArray(parsed.apexes) ? parsed.apexes : [],\n };\n}\n\nfunction write(path: string, data: TargetsFile): void {\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, JSON.stringify(data, null, 2), { mode: 0o600 });\n}\n\n/** Upsert a relay target (idempotent by `relayUrl`). */\nexport function saveRelayTarget(\n relayUrl: string,\n path = defaultTargetsPath()\n): void {\n const store = loadTargets(path);\n if (!store.relays.some((r) => r.relayUrl === relayUrl)) {\n store.relays.push({ relayUrl });\n write(path, store);\n }\n}\n\n/** Remove a relay target. Returns true if it was present. */\nexport function removeRelayTarget(\n relayUrl: string,\n path = defaultTargetsPath()\n): boolean {\n const store = loadTargets(path);\n const next = store.relays.filter((r) => r.relayUrl !== relayUrl);\n if (next.length === store.relays.length) return false;\n store.relays = next;\n write(path, store);\n return true;\n}\n\n/** Upsert an apex target (last-write-wins by `btpUrl`). */\nexport function saveApexTarget(\n target: PersistedApexTarget,\n path = defaultTargetsPath()\n): void {\n const store = loadTargets(path);\n store.apexes = store.apexes.filter((a) => a.btpUrl !== target.btpUrl);\n store.apexes.push(target);\n write(path, store);\n}\n\n/** Remove an apex target. Returns true if it was present. */\nexport function removeApexTarget(\n btpUrl: string,\n path = defaultTargetsPath()\n): boolean {\n const store = loadTargets(path);\n const next = store.apexes.filter((a) => a.btpUrl !== btpUrl);\n if (next.length === store.apexes.length) return false;\n store.apexes = next;\n write(path, store);\n return true;\n}\n\nexport { EMPTY as EMPTY_TARGETS };\n","/**\n * Discover an apex's settlement negotiation by reading its `kind:10032`\n * (`ILP_PEER_INFO_KIND`) announcement off a relay, rather than making the caller\n * hand-supply chain/settlement params. An apex (town node) publishes its\n * `IlpPeerInfo` — `btpEndpoint`, `supportedChains`, `settlementAddresses`,\n * `preferredTokens`, `tokenNetworks` — to its relay; this module subscribes for\n * it, parses via core's `parseIlpPeerInfo`, and maps it onto the daemon's\n * `ApexNegotiationConfig` so the runner can stand up a `ToonClient` against it.\n *\n * The relay is injected as a `RelaySubscription` (already started) so this is\n * unit-testable with a fake WS and reuses a relay the runner already manages.\n */\n\nimport { ILP_PEER_INFO_KIND, parseIlpPeerInfo } from '@toon-protocol/core';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { RelaySubscription } from '../relay-subscription.js';\nimport type { ApexNegotiationConfig } from './config.js';\nimport type { SettlementChain } from '../control-api.js';\n\nexport interface DiscoverApexParams {\n /** A started relay subscription to query for the apex's kind:10032. */\n relay: RelaySubscription;\n /** ILP address of the apex to match (e.g. `g.townhouse.town`). */\n ilpAddress: string;\n /** Optional apex Nostr pubkey to narrow the REQ filter (64-char hex). */\n pubkey?: string;\n /** Preferred settlement chain family; defaults to the first supported chain. */\n chain?: SettlementChain;\n /** Child peers reached via this apex's channel (e.g. `[\"dvm\",\"mill\"]`). */\n childPeers?: string[];\n /** Max time to wait for the announcement, ms. Default 15000. */\n timeoutMs?: number;\n /** Poll interval against the relay buffer, ms. Default 250. */\n pollMs?: number;\n}\n\nexport interface DiscoveredApex {\n btpUrl: string;\n negotiation: ApexNegotiationConfig;\n apexChildPeers?: string[];\n}\n\n/**\n * Thrown when discovery fails. `retryable` is true for a TIMEOUT (the apex may\n * just be slow/offline — the caller can retry once it's reachable) and false\n * for a malformed announcement (retrying won't help until the apex republishes).\n */\nexport class ApexDiscoveryError extends Error {\n constructor(\n message: string,\n readonly retryable = false\n ) {\n super(message);\n this.name = 'ApexDiscoveryError';\n }\n}\n\n/**\n * Subscribe for the apex's `kind:10032`, wait for a match, and map it to an\n * `ApexNegotiationConfig` + `btpUrl`. Rejects with {@link ApexDiscoveryError}\n * on timeout or when the announcement lacks settlement params for any chain.\n */\nexport async function discoverApex(\n params: DiscoverApexParams\n): Promise<DiscoveredApex> {\n const { relay, ilpAddress, pubkey, chain, childPeers } = params;\n const timeoutMs = params.timeoutMs ?? 15_000;\n const pollMs = params.pollMs ?? 250;\n\n const subId = relay.subscribe(\n [\n {\n kinds: [ILP_PEER_INFO_KIND],\n ...(pubkey ? { authors: [pubkey] } : {}),\n },\n ],\n `apex-discovery-${ilpAddress}`\n );\n\n try {\n const deadline = Date.now() + timeoutMs;\n let cursor = 0;\n while (Date.now() < deadline) {\n const { events, cursor: next } = relay.getEvents({ subId, cursor });\n cursor = next;\n const match = events.find((e) => matchesApex(e, ilpAddress));\n if (match) return mapAnnouncement(match, { chain, childPeers });\n await delay(pollMs);\n }\n throw new ApexDiscoveryError(\n `Timed out after ${timeoutMs}ms waiting for the apex kind:${ILP_PEER_INFO_KIND} ` +\n `announcement for \"${ilpAddress}\" on the relay. Is the relay reachable and the apex online?`,\n true // retryable: the apex may just be slow/offline\n );\n } finally {\n relay.unsubscribe(subId);\n }\n}\n\n/** Whether a kind:10032 event announces the target apex's ILP address. */\nfunction matchesApex(event: NostrEvent, ilpAddress: string): boolean {\n if (event.kind !== ILP_PEER_INFO_KIND) return false;\n try {\n const info = parseIlpPeerInfo(event);\n const addrs = info.ilpAddresses ?? [info.ilpAddress];\n return addrs.includes(ilpAddress) || info.ilpAddress === ilpAddress;\n } catch {\n return false;\n }\n}\n\n/** Map a parsed kind:10032 announcement onto the daemon's negotiation config. */\nfunction mapAnnouncement(\n event: NostrEvent,\n opts: { chain?: SettlementChain; childPeers?: string[] }\n): DiscoveredApex {\n const info = parseIlpPeerInfo(event);\n const chains = info.supportedChains ?? [];\n if (chains.length === 0) {\n throw new ApexDiscoveryError(\n `Apex \"${info.ilpAddress}\" announced no supportedChains — cannot settle.`\n );\n }\n\n // Pick the chainKey: prefer one whose family matches the requested chain,\n // else the first advertised chain.\n const chainKey =\n (opts.chain\n ? chains.find((c) => c.split(':')[0] === opts.chain)\n : undefined) ?? chains[0];\n if (!chainKey) {\n throw new ApexDiscoveryError(\n `Apex \"${info.ilpAddress}\" announced no usable settlement chain.`\n );\n }\n const family = chainKey.split(':')[0] as SettlementChain;\n const settlementAddress = info.settlementAddresses?.[chainKey];\n if (!settlementAddress) {\n throw new ApexDiscoveryError(\n `Apex \"${info.ilpAddress}\" announced no settlementAddress for chain \"${chainKey}\".`\n );\n }\n\n const btpUrl = info.btpEndpoint;\n if (!btpUrl) {\n throw new ApexDiscoveryError(\n `Apex \"${info.ilpAddress}\" announced an empty btpEndpoint — cannot open a BTP session.`\n );\n }\n\n const negotiation: ApexNegotiationConfig = {\n destination: info.ilpAddress,\n peerId: info.ilpAddress.split('.').at(-1) ?? info.ilpAddress,\n chain: family,\n chainKey,\n // EVM chainKeys are `evm:<network>:<chainId>`; non-EVM carry no numeric id.\n chainId: family === 'evm' ? Number(chainKey.split(':')[2] ?? 0) : 0,\n settlementAddress,\n ...(info.preferredTokens?.[chainKey]\n ? { tokenAddress: info.preferredTokens[chainKey] }\n : {}),\n ...(info.tokenNetworks?.[chainKey]\n ? { tokenNetwork: info.tokenNetworks[chainKey] }\n : {}),\n };\n\n return {\n btpUrl,\n negotiation,\n ...(opts.childPeers && opts.childPeers.length > 0\n ? { apexChildPeers: opts.childPeers }\n : {}),\n };\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n","/**\n * Fastify route registration for the `toon-clientd` control plane. Each route\n * is a thin adapter: parse/validate the request, call the `ClientRunner`, map\n * errors to the uniform `ErrorResponse` envelope.\n *\n * Bound to loopback only by the daemon entry — there is no auth layer because\n * the surface never leaves `127.0.0.1`.\n */\n\nimport type { FastifyInstance, FastifyReply } from 'fastify';\nimport type { NostrEvent } from 'nostr-tools/pure';\nimport type { ClientRunner } from './client-runner.js';\nimport {\n NotReadyError,\n PublishRejectedError,\n TargetError,\n} from './client-runner.js';\nimport { ApexDiscoveryError } from './apex-discovery.js';\nimport type {\n AddApexRequest,\n AddRelayRequest,\n EventsQuery,\n OpenChannelRequest,\n PublishRequest,\n RemoveApexRequest,\n RemoveRelayRequest,\n SubscribeRequest,\n SwapRequest,\n} from '../control-api.js';\n\nexport function registerRoutes(\n app: FastifyInstance,\n runner: ClientRunner\n): void {\n app.get('/status', async () => runner.getStatus());\n\n app.post<{ Body: PublishRequest }>('/publish', async (req, reply) => {\n const body = req.body;\n if (!body || !isSignedEvent(body.event)) {\n return sendError(reply, 400, 'invalid_event', {\n detail: 'body.event must be a fully-signed Nostr event (id + sig).',\n });\n }\n try {\n return await runner.publish(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: SubscribeRequest }>('/subscribe', async (req, reply) => {\n const body = req.body;\n if (!body || body.filters === undefined) {\n return sendError(reply, 400, 'invalid_filters', {\n detail:\n 'body.filters is required (a NIP-01 filter or array of filters).',\n });\n }\n try {\n return runner.subscribe(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.get<{\n Querystring: {\n subId?: string;\n cursor?: string;\n limit?: string;\n relayUrl?: string;\n };\n }>('/events', async (req) => {\n const q = req.query;\n const query: EventsQuery = {};\n if (q.subId) query.subId = q.subId;\n if (q.cursor !== undefined) query.cursor = Number(q.cursor);\n if (q.limit !== undefined) query.limit = Number(q.limit);\n if (q.relayUrl) query.relayUrl = q.relayUrl;\n return runner.getEvents(query);\n });\n\n app.post<{ Body: OpenChannelRequest }>('/channels', async (req, reply) => {\n try {\n return await runner.openChannel(req.body?.destination);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.get('/channels', async () => runner.getChannels());\n\n app.post<{ Body: SwapRequest }>('/swap', async (req, reply) => {\n const body = req.body;\n if (\n !body ||\n !body.destination ||\n body.amount === undefined ||\n !body.millPubkey ||\n !body.pair ||\n !body.chainRecipient\n ) {\n return sendError(reply, 400, 'invalid_swap', {\n detail:\n 'body.destination, amount, millPubkey, pair, and chainRecipient are required.',\n });\n }\n try {\n return await runner.swap(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.get('/targets', async () => runner.getTargets());\n\n app.post<{ Body: AddRelayRequest }>('/relays', async (req, reply) => {\n const url = req.body?.relayUrl;\n if (!url) {\n return sendError(reply, 400, 'invalid_relay', {\n detail: 'body.relayUrl is required.',\n });\n }\n try {\n await runner.addRelay(url);\n return runner.getTargets();\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.delete<{ Body: RemoveRelayRequest }>('/relays', async (req, reply) => {\n const url = req.body?.relayUrl;\n if (!url) {\n return sendError(reply, 400, 'invalid_relay', {\n detail: 'body.relayUrl is required.',\n });\n }\n try {\n runner.removeRelay(url);\n return runner.getTargets();\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.post<{ Body: AddApexRequest }>('/apex', async (req, reply) => {\n const body = req.body;\n if (!body || !body.ilpAddress || !body.relayUrl) {\n return sendError(reply, 400, 'invalid_apex', {\n detail: 'body.ilpAddress and body.relayUrl are required.',\n });\n }\n try {\n return await runner.addApex(body);\n } catch (err) {\n return mapError(reply, err);\n }\n });\n\n app.delete<{ Body: RemoveApexRequest }>('/apex', async (req, reply) => {\n const url = req.body?.btpUrl;\n if (!url) {\n return sendError(reply, 400, 'invalid_apex', {\n detail: 'body.btpUrl is required.',\n });\n }\n try {\n await runner.removeApex(url);\n return runner.getTargets();\n } catch (err) {\n return mapError(reply, err);\n }\n });\n}\n\nfunction isSignedEvent(event: unknown): event is NostrEvent {\n if (typeof event !== 'object' || event === null) return false;\n const e = event as Record<string, unknown>;\n return (\n typeof e['id'] === 'string' &&\n typeof e['sig'] === 'string' &&\n typeof e['pubkey'] === 'string' &&\n typeof e['kind'] === 'number'\n );\n}\n\nfunction mapError(reply: FastifyReply, err: unknown): FastifyReply {\n if (err instanceof NotReadyError) {\n return sendError(reply, 503, 'bootstrapping', {\n detail: err.message,\n retryable: true,\n });\n }\n if (err instanceof PublishRejectedError) {\n return sendError(reply, 502, 'rejected', { detail: err.message });\n }\n if (err instanceof TargetError) {\n // 404 for \"no such target\", 400 otherwise — both are caller-fixable.\n const status = /no such/i.test(err.message) ? 404 : 400;\n return sendError(reply, status, 'invalid_target', { detail: err.message });\n }\n if (err instanceof ApexDiscoveryError) {\n // A timeout is a retryable 504 (apex may be slow/offline); a malformed\n // announcement is a non-retryable 502 (the apex must republish).\n return err.retryable\n ? sendError(reply, 504, 'discovery_timeout', {\n detail: err.message,\n retryable: true,\n })\n : sendError(reply, 502, 'discovery_failed', { detail: err.message });\n }\n return sendError(reply, 500, 'internal_error', {\n detail: err instanceof Error ? err.message : String(err),\n });\n}\n\nfunction sendError(\n reply: FastifyReply,\n status: number,\n error: string,\n extra: { detail?: string; retryable?: boolean } = {}\n): FastifyReply {\n return reply.status(status).send({\n error,\n ...(extra.detail ? { detail: extra.detail } : {}),\n ...(extra.retryable ? { retryable: true } : {}),\n });\n}\n"],"mappings":"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","names":["delay","generateSecretKey","__require","getPublicKey","getPublicKey","key","RATE_REGEX","noop","NOOP_LOGGER","getPublicKey","__require","mkdirSync","writeFileSync","dirname","mkdirSync","readFileSync","writeFileSync","dirname","join","join","readFileSync","mkdirSync","dirname","writeFileSync","errMsg","generateSecretKey"]}