From 112ef0536a2bf653cd461b746ee0e7fdd28dc593 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 26 Jun 2026 18:27:32 -0400 Subject: [PATCH] fix(cli): --server reuses NamecoinSettings.parseServerString (keeps pinned trust store) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `amy namecoin resolve --server` hand-rolled its own ElectrumX server-string parser that constructed `ElectrumxServer(host, port, useSsl)` and left `usePinnedTrustStore` at its `false` default. The Namecoin ElectrumX servers use self-signed certs, so a TLS connection with the default system trust manager fails the handshake — meaning `--server electrumx.testls.space:50002` could not connect even though that exact host resolves fine via the default list. It also duplicated logic already in `commons`, violating the cli thin-assembly-layer rule. Delegate each comma-separated entry to the shared `NamecoinSettings.parseServerString` (the same parser the Android/Desktop Settings use), so the CLI inherits both the `host:port[:tcp]` syntax and `usePinnedTrustStore = true`. The README claim that it "reuses the same … pinned trust store as the apps" is now actually true for `--server` overrides. Also: reject a non-integer `--timeout` as bad_args instead of silently falling back to the default, and document exit code 2 + the `host:port[:tcp]` syntax accurately. Co-Authored-By: Claude Opus 4.8 (1M context) --- cli/README.md | 2 +- .../amethyst/cli/commands/NamecoinCommand.kt | 67 +++++++++---------- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/cli/README.md b/cli/README.md index b10bd54726..0601d5010a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -219,7 +219,7 @@ Army-knife verbs that operate purely on their arguments. They never touch | `amy filter [filter flags]` | Assemble and print a NIP-01 filter JSON from the same flags `fetch`/`subscribe` use — no query is sent. | | `amy nip N` / `amy nip list` | Look up a NIP — the `nostr-protocol/nips` repo first, then a Nostr wiki/long-form fallback. `list` fetches the index. | | `amy kind N` / `amy kind NAME` | Look up an event kind's label + defining NIP (number), or search labels by name. Backed by quartz's `KindNames` registry. | -| `amy namecoin resolve IDENT [--server URL[,URL]] [--timeout SECS]` | Resolve a Namecoin identifier (`.bit`, `d/`, `id/`, `alice@example.bit`) to a Nostr pubkey + relays via the Namecoin blockchain. Stateless: talks directly to one or more ElectrumX servers over TLS, no account needed. Reuses the same NIP-05-Namecoin parser, server set, and pinned trust store as the Android and Desktop apps. | +| `amy namecoin resolve IDENT [--server HOST:PORT[:tcp][,…]] [--timeout SECS]` | Resolve a Namecoin identifier (`.bit`, `d/`, `id/`, `alice@example.bit`) to a Nostr pubkey + relays via the Namecoin blockchain. Stateless: talks directly to one or more ElectrumX servers over TLS (`:tcp` for plaintext), no account needed. Reuses the same NIP-05-Namecoin parser, server set, and pinned trust store as the Android and Desktop apps. | | `amy namecoin servers` | Print the default ElectrumX server list (host, port, TLS flag). | | `amy relay info URL` | Fetch and print a relay's NIP-11 information document. | diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NamecoinCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NamecoinCommand.kt index 619cbb1e25..900f8dc7de 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NamecoinCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/NamecoinCommand.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.cli.commands import com.vitorpamplona.amethyst.cli.Args import com.vitorpamplona.amethyst.cli.Output +import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.namecoin.NamecoinSettings import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer @@ -52,14 +53,17 @@ import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinResolveOutc * alice@example.bit NIP-05-style local-part in a `.bit` domain * * Flags (resolve): - * --server URL[,URL] override the ElectrumX server list (host:port, - * one or more; default: the same hard-coded - * mainnet set the apps ship with) + * --server HOST:PORT[:tcp][,…] override the ElectrumX server list (one or + * more `host:port[:tcp]` entries, same form the apps + * accept; TLS by default, `:tcp` for plaintext; + * default: the same hard-coded mainnet set the apps ship with) * --timeout SECS overall lookup timeout (default 20) * * Exit codes follow amy convention: * 0 success (Output.emit was called) * 1 Output.error was called (bad_args, network, name not found, …) + * 2 usage error raised before the verb runs (e.g. a missing positional, + * thrown by Args and mapped to 2 by Main) * * The JSON shape on success (resolve): * { @@ -113,7 +117,14 @@ object NamecoinCommand { return Output.error("bad_args", "--server: no valid host:port entries in '$serverFlag'") } - val timeoutSecs = args.flag("timeout")?.toLongOrNull() ?: DEFAULT_TIMEOUT_SECS + val timeoutFlag = args.flag("timeout") + val timeoutSecs = + if (timeoutFlag != null) { + timeoutFlag.toLongOrNull() + ?: return Output.error("bad_args", "--timeout must be an integer (was '$timeoutFlag')") + } else { + DEFAULT_TIMEOUT_SECS + } if (timeoutSecs <= 0) { return Output.error("bad_args", "--timeout must be positive (was $timeoutSecs)") } @@ -186,45 +197,27 @@ object NamecoinCommand { } /** - * Parse the `--server URL[,URL]` flag. + * Parse the `--server HOST:PORT[:tcp][,…]` flag — one or more comma-separated + * entries in the same `host:port[:tcp]` form the Android/Desktop Namecoin + * Settings accept (TLS by default; a trailing `:tcp` selects plaintext). * - * Accepted forms per entry: - * host → host:50002 TLS - * host:port → TLS - * tcp://host:port → plaintext - * tls://host:port → TLS - * ssl://host:port → TLS (alias for tls://) + * Each entry is parsed by the shared [NamecoinSettings.parseServerString], so + * the CLI inherits the apps' exact syntax **and** their trust model — notably + * `usePinnedTrustStore = true`, which is required for the self-signed certs the + * Namecoin ElectrumX servers use (a hand-rolled parser that left it `false` + * would fail the TLS handshake against those servers). * - * Returns null when the flag is absent (caller falls back to the - * default server list). Returns an empty list when the flag is - * present but produced zero valid entries — the caller treats that - * as a hard `bad_args` rather than silently using defaults, so a - * fat-fingered `--server foo:bar` is impossible to overlook. + * Returns null when the flag is absent (caller falls back to the default server + * list). Returns an empty list when the flag is present but produced zero valid + * entries — the caller treats that as a hard `bad_args` rather than silently + * using defaults, so a fat-fingered `--server foo:bar` is impossible to overlook. */ private fun parseServerFlag(raw: String?): List? { if (raw == null) return null return raw .split(',') - .mapNotNull { entry -> - val trimmed = entry.trim() - if (trimmed.isEmpty()) return@mapNotNull null - val (scheme, rest) = - when { - trimmed.startsWith("tcp://") -> "tcp" to trimmed.removePrefix("tcp://") - trimmed.startsWith("tls://") -> "tls" to trimmed.removePrefix("tls://") - trimmed.startsWith("ssl://") -> "tls" to trimmed.removePrefix("ssl://") - else -> "tls" to trimmed - } - val (host, portStr) = - if (':' in rest) { - rest.substringBeforeLast(':') to rest.substringAfterLast(':') - } else { - rest to "50002" - } - if (host.isEmpty()) return@mapNotNull null - val port = portStr.toIntOrNull() ?: return@mapNotNull null - if (port !in 1..65535) return@mapNotNull null - ElectrumxServer(host = host, port = port, useSsl = scheme == "tls") - } + .map { it.trim() } + .filter { it.isNotEmpty() } + .mapNotNull { NamecoinSettings.parseServerString(it) } } }