fix: route ElectrumX through Tor proxy, add onion server

- ElectrumxClient accepts injected SocketFactory (lambda) so connections
  respect the user's Tor/proxy settings instead of leaking their IP
  through raw sockets
- Add ProxiedSocketFactory for SOCKS5 proxy routing
- Add .onion ElectrumX server as primary when Tor is enabled, with
  electrumx.testls.space as clearnet fallback
- NamecoinNameResolver accepts serverListProvider lambda for dynamic
  server selection based on current Tor settings
- RoleBasedHttpClientBuilder.socketFactoryForNip05() bridges Amethyst's
  Tor settings to the socket factory
- NamecoinNameService now requires explicit init with proxy-aware client
- Remove dead code: Nip05NamecoinAdapter (never referenced),
  NamecoinVerificationDisplay (never called)
- Update design documentation
This commit is contained in:
M
2026-03-03 06:28:01 +11:00
parent f6447d2020
commit 3f39f96e81
9 changed files with 241 additions and 386 deletions
@@ -42,6 +42,7 @@ import java.net.InetSocketAddress
import java.net.Socket
import java.security.MessageDigest
import java.util.concurrent.atomic.AtomicInteger
import javax.net.SocketFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
@@ -97,6 +98,7 @@ data class ElectrumxServer(
class ElectrumxClient(
private val connectTimeoutMs: Long = 10_000L,
private val readTimeoutMs: Long = 15_000L,
private val socketFactory: () -> SocketFactory = { SocketFactory.getDefault() },
) {
private val json =
Json {
@@ -107,7 +109,7 @@ class ElectrumxClient(
private val mutex = Mutex()
companion object {
/** Well-known public Namecoin ElectrumX servers. */
/** Well-known public Namecoin ElectrumX servers (clearnet). */
val DEFAULT_SERVERS =
listOf(
ElectrumxServer("electrumx.testls.space", 50002, useSsl = true, trustAllCerts = true),
@@ -115,6 +117,18 @@ class ElectrumxClient(
ElectrumxServer("nmc2.lelux.fi", 50006, useSsl = true),
)
/** Tor-preferred server list: onion primary, clearnet fallback. */
val TOR_SERVERS =
listOf(
ElectrumxServer(
"i665jpwsq46zlsdbnj4axgzd3s56uzey5uhotsnxzsknzbn36jaddsid.onion",
50002,
useSsl = true,
trustAllCerts = true,
),
ElectrumxServer("electrumx.testls.space", 50002, useSsl = true, trustAllCerts = true),
)
private const val PROTOCOL_VERSION = "1.4"
// Namecoin script opcodes
@@ -155,10 +169,16 @@ class ElectrumxClient(
}
/**
* Try each default server in order until one succeeds.
* Try each server in order until one succeeds.
*
* @param identifier Full Namecoin name, e.g. "d/example"
* @param servers Ordered server list to try; defaults to [DEFAULT_SERVERS]
*/
suspend fun nameShowWithFallback(identifier: String): NameShowResult? {
for (server in DEFAULT_SERVERS) {
suspend fun nameShowWithFallback(
identifier: String,
servers: List<ElectrumxServer> = DEFAULT_SERVERS,
): NameShowResult? {
for (server in servers) {
val result = nameShow(identifier, server)
if (result != null) return result
}
@@ -412,22 +432,25 @@ class ElectrumxClient(
return data
}
private fun createSocket(server: ElectrumxServer): Socket =
if (server.useSsl) {
val factory =
if (server.trustAllCerts) {
trustAllSslFactory()
} else {
SSLSocketFactory.getDefault() as SSLSocketFactory
}
factory.createSocket().apply {
private fun createSocket(server: ElectrumxServer): Socket {
// Create the base socket through the injected factory, which
// may route through a SOCKS proxy (e.g. Tor) if configured.
val baseSocket =
socketFactory().createSocket().apply {
connect(InetSocketAddress(server.host, server.port), connectTimeoutMs.toInt())
}
} else {
Socket().apply {
connect(InetSocketAddress(server.host, server.port), connectTimeoutMs.toInt())
if (!server.useSsl) return baseSocket
// Upgrade to TLS over the already-connected (possibly proxied) socket.
val sslFactory =
if (server.trustAllCerts) {
trustAllSslFactory()
} else {
SSLSocketFactory.getDefault() as SSLSocketFactory
}
}
return sslFactory.createSocket(baseSocket, server.host, server.port, true)
}
/**
* Create an SSLSocketFactory that accepts any certificate.
@@ -52,6 +52,7 @@ data class NamecoinNostrResult(
class NamecoinNameResolver(
private val electrumxClient: ElectrumxClient = ElectrumxClient(),
private val lookupTimeoutMs: Long = 20_000L,
private val serverListProvider: () -> List<ElectrumxServer> = { ElectrumxClient.DEFAULT_SERVERS },
) {
private val json =
Json {
@@ -165,7 +166,7 @@ class NamecoinNameResolver(
// ── Lookup & Value Parsing ─────────────────────────────────────────
private suspend fun performLookup(parsed: ParsedIdentifier): NamecoinNostrResult? {
val nameResult = electrumxClient.nameShowWithFallback(parsed.namecoinName) ?: return null
val nameResult = electrumxClient.nameShowWithFallback(parsed.namecoinName, serverListProvider()) ?: return null
val valueJson = tryParseJson(nameResult.value) ?: return null
return when (parsed.namespace) {