fix(cli): --server reuses NamecoinSettings.parseServerString (keeps pinned trust store)

`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) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-06-26 18:27:32 -04:00
co-authored by Claude Opus 4.8
parent 305d6bc733
commit 112ef0536a
2 changed files with 31 additions and 38 deletions
+1 -1
View File
@@ -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. |
@@ -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<ElectrumxServer>? {
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) }
}
}