* 'main' of https://github.com/vitorpamplona/amethyst:
  New Crowdin translations by GitHub Action
  update translations: CZ, DE, PT, SE
  fix: restore local WatchAndDisplayNip05Row in ShowUserSuggestionList
  refactor: remove Namecoin label from search results
  feat: show Namecoin label on .bit search results without ViewModel state
  refactor: remove Namecoin label from search results per review
  refactor: simplify .bit search resolution per review feedback
  feat: resolve bare .bit domains and show Namecoin resolution status in UI
  New Crowdin translations by GitHub Action
  New Crowdin translations by GitHub Action
  refactor: rename trustAllCerts → usePinnedTrustStore
  fix: harden TLS for GrapheneOS and security-conscious Android ROMs
  doc: verify .onion ElectrumX cert via Tor, document pinning
  fix: bypass cert pinning for .onion ElectrumX servers
  feat: TOFU cert pinning for custom ElectrumX servers
  feat: add Test Connection diagnostics to Namecoin settings
  fix: pin ElectrumX server certs instead of trust-all TrustManager

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt
This commit is contained in:
Vitor Pamplona
2026-03-26 11:34:54 -04:00
18 changed files with 1144 additions and 95 deletions
@@ -44,8 +44,14 @@ data class ElectrumxServer(
val host: String,
val port: Int,
val useSsl: Boolean = true,
/** If true, accept any certificate (self-signed, expired, etc.) */
val trustAllCerts: Boolean = false,
/**
* If true, use the pinned trust store (hardcoded + TOFU-pinned certs
* plus system CAs) instead of the default system-only trust store.
*
* Required for ElectrumX servers that use self-signed certificates,
* which is the norm for the Namecoin ElectrumX ecosystem.
*/
val usePinnedTrustStore: Boolean = false,
)
/**
@@ -72,12 +78,27 @@ sealed class NamecoinLookupException(
) : NamecoinLookupException("All ElectrumX servers unreachable", lastError)
}
/**
* Result of testing connectivity to a single ElectrumX server.
*/
data class ServerTestResult(
val server: ElectrumxServer,
val success: Boolean,
val responseTimeMs: Long,
val error: String? = null,
val tlsVersion: String? = null,
/** PEM-encoded server certificate, captured during test for TOFU pinning. */
val serverCertPem: String? = null,
/** SHA-256 fingerprint of the server certificate. */
val certFingerprint: String? = null,
)
/** Well-known public Namecoin ElectrumX servers (clearnet). */
val DEFAULT_ELECTRUMX_SERVERS =
listOf(
ElectrumxServer("electrumx.testls.space", 50002, useSsl = true, trustAllCerts = true),
ElectrumxServer("nmc2.bitcoins.sk", 57002, useSsl = true, trustAllCerts = true),
ElectrumxServer("46.229.238.187", 57002, useSsl = true, trustAllCerts = true),
ElectrumxServer("electrumx.testls.space", 50002, useSsl = true, usePinnedTrustStore = true),
ElectrumxServer("nmc2.bitcoins.sk", 57002, useSsl = true, usePinnedTrustStore = true),
ElectrumxServer("46.229.238.187", 57002, useSsl = true, usePinnedTrustStore = true),
)
/** Tor-preferred server list: onion primary, clearnet fallback. */
@@ -87,8 +108,8 @@ val TOR_ELECTRUMX_SERVERS =
"i665jpwsq46zlsdbnj4axgzd3s56uzey5uhotsnxzsknzbn36jaddsid.onion",
50002,
useSsl = true,
trustAllCerts = true,
usePinnedTrustStore = true,
),
ElectrumxServer("electrumx.testls.space", 50002, useSsl = true, trustAllCerts = true),
ElectrumxServer("nmc2.bitcoins.sk", 57002, useSsl = true, trustAllCerts = true),
ElectrumxServer("electrumx.testls.space", 50002, useSsl = true, usePinnedTrustStore = true),
ElectrumxServer("nmc2.bitcoins.sk", 57002, useSsl = true, usePinnedTrustStore = true),
)
@@ -38,19 +38,22 @@ import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.InetSocketAddress
import java.net.Socket
import java.security.KeyStore
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.security.cert.CertificateFactory
import java.util.concurrent.ConcurrentHashMap
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
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
/**
@@ -87,25 +90,6 @@ class ElectrumXClient(
private val requestId = AtomicInteger(0)
private val serverMutexes = ConcurrentHashMap<String, Mutex>()
companion object {
private const val PROTOCOL_VERSION = "1.4"
/**
* Namecoin names expire this many blocks after their last update.
* From chainparams.cpp: consensus.nNameExpirationDepth = 36000
* (~250 days at ~10 min/block).
*/
const val NAME_EXPIRE_DEPTH = 36_000
// Namecoin script opcodes
private const val OP_NAME_UPDATE: Byte = 0x53 // OP_3 repurposed by Namecoin
private const val OP_2DROP: Byte = 0x6d
private const val OP_DROP: Byte = 0x75
private const val OP_RETURN: Byte = 0x6a
private const val OP_PUSHDATA1: Byte = 0x4c
private const val OP_PUSHDATA2: Byte = 0x4d
}
/**
* Perform a name_show lookup against the given ElectrumX server.
*
@@ -167,6 +151,162 @@ class ElectrumXClient(
throw NamecoinLookupException.ServersUnreachable(lastError)
}
/**
* Test connectivity to a single ElectrumX server.
*
* Connects, negotiates protocol version, and optionally resolves a test
* name. Returns detailed results including response time, TLS version,
* and human-readable error messages.
*
* @param server The server to test
* @param testName Optional name to resolve (e.g. "d/testls")
* @return [ServerTestResult] with success/failure details
*/
suspend fun testServer(
server: ElectrumxServer,
testName: String? = "d/testls",
): ServerTestResult =
withContext(Dispatchers.IO) {
val startTime = System.currentTimeMillis()
try {
val socket = createSocket(server)
socket.soTimeout = readTimeoutMs.toInt()
var tlsVersion: String? = null
var serverCertPem: String? = null
var certFingerprint: String? = null
if (socket is javax.net.ssl.SSLSocket) {
tlsVersion = socket.session.protocol
// Capture the server's leaf certificate for TOFU pinning
try {
val peerCerts = socket.session.peerCertificates
if (peerCerts.isNotEmpty() && peerCerts[0] is java.security.cert.X509Certificate) {
val x509 = peerCerts[0] as java.security.cert.X509Certificate
// PEM encode
val encoded =
java.util.Base64
.getMimeEncoder(76, "\n".toByteArray())
.encodeToString(x509.encoded)
serverCertPem = "-----BEGIN CERTIFICATE-----\n$encoded-----END CERTIFICATE-----"
// SHA-256 fingerprint
val digest = MessageDigest.getInstance("SHA-256").digest(x509.encoded)
certFingerprint = digest.joinToString(":") { "%02X".format(it) }
}
} catch (_: Exception) {
// Non-fatal — cert capture is best-effort
}
}
val writer = PrintWriter(socket.getOutputStream(), true)
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
try {
// Negotiate protocol version
val versionReq =
buildRpcRequest(
"server.version",
listOf("AmethystNMC/0.1", PROTOCOL_VERSION),
)
writer.println(versionReq)
val versionResponse =
reader.readLine()
?: return@withContext ServerTestResult(
server = server,
success = false,
responseTimeMs = System.currentTimeMillis() - startTime,
error = "Server returned empty response",
tlsVersion = tlsVersion,
)
// If a test name is provided, try to resolve it
if (testName != null) {
val nameScript =
buildNameIndexScript(testName.toByteArray(Charsets.US_ASCII))
val scriptHash = electrumScriptHash(nameScript)
val historyReq =
buildRpcRequest(
"blockchain.scripthash.get_history",
listOf(scriptHash),
)
writer.println(historyReq)
reader.readLine() // consume response
}
val elapsed = System.currentTimeMillis() - startTime
ServerTestResult(
server = server,
success = true,
responseTimeMs = elapsed,
tlsVersion = tlsVersion,
serverCertPem = serverCertPem,
certFingerprint = certFingerprint,
)
} finally {
runCatching { writer.close() }
runCatching { reader.close() }
runCatching { socket.close() }
}
} catch (e: java.net.ConnectException) {
ServerTestResult(
server = server,
success = false,
responseTimeMs = System.currentTimeMillis() - startTime,
error = "Connection refused",
)
} catch (e: java.net.SocketTimeoutException) {
ServerTestResult(
server = server,
success = false,
responseTimeMs = System.currentTimeMillis() - startTime,
error = "Connection timed out after ${connectTimeoutMs / 1000}s",
)
} catch (e: java.net.UnknownHostException) {
ServerTestResult(
server = server,
success = false,
responseTimeMs = System.currentTimeMillis() - startTime,
error = "Server unreachable (DNS resolution failed)",
)
} catch (e: javax.net.ssl.SSLHandshakeException) {
val detail =
if (e.message?.contains("self-signed", ignoreCase = true) == true ||
e.message?.contains("anchor", ignoreCase = true) == true
) {
"TLS handshake failed (self-signed certificate rejected)"
} else {
"TLS handshake failed: ${e.message?.take(100) ?: "unknown error"}"
}
ServerTestResult(
server = server,
success = false,
responseTimeMs = System.currentTimeMillis() - startTime,
error = detail,
)
} catch (e: javax.net.ssl.SSLException) {
ServerTestResult(
server = server,
success = false,
responseTimeMs = System.currentTimeMillis() - startTime,
error = "TLS error: ${e.message?.take(100) ?: "unknown"}",
)
} catch (e: java.io.IOException) {
ServerTestResult(
server = server,
success = false,
responseTimeMs = System.currentTimeMillis() - startTime,
error = "I/O error: ${e.message?.take(100) ?: "unknown"}",
)
} catch (e: Exception) {
ServerTestResult(
server = server,
success = false,
responseTimeMs = System.currentTimeMillis() - startTime,
error = e.message?.take(150) ?: "Unknown error",
)
}
}
// ── internals ──────────────────────────────────────────────────────
private fun connectAndQuery(
@@ -461,41 +601,298 @@ class ElectrumXClient(
if (!server.useSsl) return baseSocket
// Upgrade to TLS over the already-connected (possibly proxied) socket.
// When usePinnedTrustStore is set, we use a pinned trust store that
// contains the known ElectrumX server certs plus system CAs. This is
// required because Samsung One UI 7 (Android 16) and GrapheneOS
// reject connections that use a no-op "trust-all" X509TrustManager.
val sslFactory =
if (server.trustAllCerts) {
trustAllSslFactory()
if (server.host.endsWith(".onion")) {
// .onion addresses: prefer the pinned factory (the .onion server's
// cert is already in PINNED_ELECTRUMX_CERTS). Fall back to trust-all
// only if the pinned handshake fails — this keeps compatibility with
// .onion servers whose certs aren't pinned yet, while avoiding a
// blanket trust-all that hardened TLS stacks (GrapheneOS, Samsung
// Knox) may reject at the Conscrypt/BoringSSL layer.
cachedPinnedSslFactory()
} else if (server.usePinnedTrustStore) {
cachedPinnedSslFactory()
} else {
SSLSocketFactory.getDefault() as SSLSocketFactory
}
return sslFactory.createSocket(baseSocket, server.host, server.port, true)
val sslSocket: Socket
try {
sslSocket = sslFactory.createSocket(baseSocket, server.host, server.port, true)
} catch (e: javax.net.ssl.SSLHandshakeException) {
if (server.host.endsWith(".onion")) {
// Pinned factory failed for .onion — fall back to trust-all.
// This is safe: Tor provides E2E authentication via the onion
// address, and the proxied socket bypasses Knox/GrapheneOS
// trust-all rejection in practice.
val fallbackSocket = onionSslFactory().createSocket(baseSocket, server.host, server.port, true)
if (fallbackSocket is javax.net.ssl.SSLSocket) {
val supported = fallbackSocket.supportedProtocols
val modern = supported.filter { it == "TLSv1.2" || it == "TLSv1.3" }
if (modern.isNotEmpty()) {
fallbackSocket.enabledProtocols = modern.toTypedArray()
}
}
return fallbackSocket
}
throw e
}
// Enforce TLSv1.2+ — some OEM Conscrypt forks (Xiaomi MIUI, OnePlus ColorOS)
// may negotiate TLS 1.0/1.1 by default for raw socket upgrades.
if (sslSocket is javax.net.ssl.SSLSocket) {
val supported = sslSocket.supportedProtocols
val modern = supported.filter { it == "TLSv1.2" || it == "TLSv1.3" }
if (modern.isNotEmpty()) {
sslSocket.enabledProtocols = modern.toTypedArray()
}
}
return sslSocket
}
/**
* Create an SSLSocketFactory that accepts any certificate.
* Used for servers with self-signed certificates.
* Fallback SSLSocketFactory for .onion addresses when the pinned
* factory fails (e.g. cert rotated, unknown .onion server).
*
* Only used as a last resort after cachedPinnedSslFactory() throws
* SSLHandshakeException. This is safe because:
* 1. The connection is already end-to-end encrypted by Tor.
* 2. The onion address IS the server's identity proof (public key hash).
* 3. Proxied sockets via Tor SOCKS typically bypass OEM trust-all
* rejection (Samsung Knox, GrapheneOS hardened Conscrypt).
*
* Note: GrapheneOS or future Android versions may reject trust-all
* TrustManagers even for proxied sockets. If this fallback stops
* working, the .onion server's cert should be added to
* PINNED_ELECTRUMX_CERTS (it's already there for the known server).
*/
private fun trustAllSslFactory(): SSLSocketFactory {
val trustAllCerts =
private fun onionSslFactory(): SSLSocketFactory {
val trustAll =
arrayOf<TrustManager>(
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<X509Certificate>,
chain: Array<java.security.cert.X509Certificate>,
authType: String,
) {}
override fun checkServerTrusted(
chain: Array<X509Certificate>,
chain: Array<java.security.cert.X509Certificate>,
authType: String,
) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> = arrayOf()
},
)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, SecureRandom())
val ctx =
try {
SSLContext.getInstance("TLSv1.2")
} catch (_: Exception) {
SSLContext.getInstance("TLS")
}
ctx.init(null, trustAll, SecureRandom())
return ctx.socketFactory
}
/** User-supplied PEM certificates for custom servers (TOFU-pinned). */
private val dynamicCerts = mutableListOf<String>()
/** Lazy-cached SSLSocketFactory for pinned certs. Thread-safe via volatile + DCL. */
@Volatile
private var pinnedFactory: SSLSocketFactory? = null
private fun cachedPinnedSslFactory(): SSLSocketFactory {
pinnedFactory?.let { return it }
synchronized(this) {
pinnedFactory?.let { return it }
return buildPinnedSslFactory().also { pinnedFactory = it }
}
}
/**
* Add a PEM-encoded certificate to the dynamic trust store.
* Typically called after the user confirms a cert fingerprint via
* the "Test Connection" flow in settings.
*
* Invalidates the cached factory so the next connection picks it up.
*/
fun addPinnedCert(pem: String) {
synchronized(this) {
dynamicCerts.add(pem)
pinnedFactory = null // force rebuild
}
}
/**
* Replace all dynamic certs (e.g. loaded from preferences on startup).
*/
fun setDynamicCerts(pems: List<String>) {
synchronized(this) {
dynamicCerts.clear()
dynamicCerts.addAll(pems)
pinnedFactory = null
}
}
/**
* Build an SSLSocketFactory that trusts the pinned ElectrumX server
* certificates plus the system CA store.
*
* Previous versions used a "trust-all" TrustManager, but Samsung
* devices running One UI 7 (Android 16) silently reject connections
* that use a no-op X509TrustManager. Pinning the known self-signed
* certs avoids this while maintaining security.
*
* Also handles OEM-specific quirks:
* - Xiaomi MIUI/HyperOS: KeyStore.getDefaultType() may return unexpected
* types; we try the default first, then fall back to "PKCS12".
* - OnePlus ColorOS: some versions require explicit TLSv1.2 protocol.
* - All OEMs: SSLContext("TLSv1.2") is preferred over ("TLS") which may
* resolve to TLS 1.0 on older Conscrypt forks.
*/
private fun buildPinnedSslFactory(): SSLSocketFactory {
val ks =
try {
KeyStore.getInstance(KeyStore.getDefaultType()).apply { load(null, null) }
} catch (_: Exception) {
// Fallback for Xiaomi devices where getDefaultType() returns an unsupported type
KeyStore.getInstance("PKCS12").apply { load(null, null) }
}
val cf = CertificateFactory.getInstance("X.509")
// Load hardcoded + dynamic pinned certificates into the keystore
val allCerts = PINNED_ELECTRUMX_CERTS + dynamicCerts
for ((index, pem) in allCerts.withIndex()) {
try {
val cert = cf.generateCertificate(ByteArrayInputStream(pem.toByteArray(Charsets.US_ASCII)))
ks.setCertificateEntry("electrumx_$index", cert)
} catch (_: Exception) {
// Skip malformed certs — the remaining ones may still work
}
}
// Also load system CA certificates so that servers with real certs work too
val systemTmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
systemTmf.init(null as KeyStore?) // null = system default
val systemTm = systemTmf.trustManagers.filterIsInstance<X509TrustManager>().firstOrNull()
if (systemTm != null) {
for ((index, issuer) in systemTm.acceptedIssuers.withIndex()) {
try {
ks.setCertificateEntry("system_$index", issuer)
} catch (_: Exception) {
// Some OEMs return certs that can't be re-inserted; skip
}
}
}
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
tmf.init(ks)
// Prefer TLSv1.2 explicitly — SSLContext.getInstance("TLS") can resolve
// to TLS 1.0 on some OEM Conscrypt forks (Xiaomi, OnePlus).
val sslContext =
try {
SSLContext.getInstance("TLSv1.2")
} catch (_: Exception) {
SSLContext.getInstance("TLS")
}
sslContext.init(null, tmf.trustManagers, SecureRandom())
return sslContext.socketFactory
}
companion object {
private const val PROTOCOL_VERSION = "1.4"
/**
* Namecoin names expire this many blocks after their last update.
* From chainparams.cpp: consensus.nNameExpirationDepth = 36000
* (~250 days at ~10 min/block).
*/
const val NAME_EXPIRE_DEPTH = 36_000
// Namecoin script opcodes
private const val OP_NAME_UPDATE: Byte = 0x53 // OP_3 repurposed by Namecoin
private const val OP_2DROP: Byte = 0x6d
private const val OP_DROP: Byte = 0x75
private const val OP_RETURN: Byte = 0x6a
private const val OP_PUSHDATA1: Byte = 0x4c
private const val OP_PUSHDATA2: Byte = 0x4d
/**
* PEM-encoded certificates for the well-known Namecoin ElectrumX servers.
*
* These are self-signed certificates that cannot be verified by the
* system CA store. We pin them explicitly so that connections succeed
* on devices with strict TLS enforcement (e.g. Samsung One UI 7).
*
* To update: `echo | openssl s_client -connect HOST:PORT 2>/dev/null | openssl x509 -outform PEM`
* For .onion: `python3 -c "import socks,ssl,socket,base64; s=socks.socksocket(); s.set_proxy(socks.SOCKS5,'127.0.0.1',9050); s.connect(('HOST',PORT)); ctx=ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE; ss=ctx.wrap_socket(s); print(base64.encodebytes(ss.getpeercert(True)).decode())"`
*/
private val PINNED_ELECTRUMX_CERTS =
listOf(
// electrumx.testls.space:50002 — expires 2027-05-04
// Also covers the .onion hidden service (same operator, same cert):
// i665jpwsq46zlsdbnj4axgzd3s56uzey5uhotsnxzsknzbn36jaddsid.onion:50002
// SHA-256: 53:65:D5:BB:26:19:F5:40:1C:D8:8E:FC:AF:FB:A5:B2:A0:EA:7A:99:2D:F7:0F:05:7E:9B:CD:50:36:C7:79:9C
"""
-----BEGIN CERTIFICATE-----
MIIDwzCCAqsCFGGKT5mjh7oN98aNyjOCiqafL8VyMA0GCSqGSIb3DQEBCwUAMIGd
MQswCQYDVQQGEwJVUzEQMA4GA1UECAwHQ2hpY2FnbzEQMA4GA1UEBwwHQ2hpY2Fn
bzESMBAGA1UECgwJSW50ZXJuZXRzMQ8wDQYDVQQLDAZJbnRlcncxHjAcBgNVBAMM
FWVsZWN0cnVtLnRlc3Rscy5zcGFjZTElMCMGCSqGSIb3DQEJARYWbWpfZ2lsbF84
OUBob3RtYWlsLmNvbTAeFw0yMjA1MDUwNjIzNDFaFw0yNzA1MDQwNjIzNDFaMIGd
MQswCQYDVQQGEwJVUzEQMA4GA1UECAwHQ2hpY2FnbzEQMA4GA1UEBwwHQ2hpY2Fn
bzESMBAGA1UECgwJSW50ZXJuZXRzMQ8wDQYDVQQLDAZJbnRlcncxHjAcBgNVBAMM
FWVsZWN0cnVtLnRlc3Rscy5zcGFjZTElMCMGCSqGSIb3DQEJARYWbWpfZ2lsbF84
OUBob3RtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO4H
+PKCdiiz3jNOA77aAmS2YaU7eOQ8ZGliEVr/PlLcgF5gmthb2DI6iK4KhC1ad34G
1n9IhkXPhkVJ94i8wB3uoTBlA7mI5h59m01yhzSkJAoYoU/i6DM9ipbakqWFCTEp
P+yE216NTU5MbYwThZdRSAIIABe9RyIliMSidyrwHvKBLfnJPFScghW6rhBWN7PG
PA8k0MFGzf+HXbpnV/jAvz08ZC34qiBIjkJrTgh49JweyoZKdppyJcH4UbkslJ2t
YUJR3oURBvrPj+D7TwLVRbX36ul7r4+dP3IjgmljsSAHDK4N/PfWrCBdlj9Pc1Cp
yX+ZDh8X2NrL4ukHoVMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAeVj6VZNmY/Vb
nhzrC7xBSHqVWQ1wkLOClLsdvgKP8cFFJuUoCMQU5bPMi7nWnkfvvsIKH4Eibk5K
fqiA9jVsY0FHvQ8gP3KMk1LVuUf/sTcRe5itp3guBOSk/zXZUD5tUz/oRk3k+rdc
MsInqhomjNy/dqYmD6Wm4DNPjZh6fWy+AVQKVNOI2t4koaVdpoi8Uv8h4gFGPbdI
sVmtoGiIGkKNIWum+6mnF6PfynNrLk+ztH4TrdacVNeoJUPYEAxOuesWXFy3H4r+
HKBqA4xAzyjgKLPqoWnjSu7gxj1GIjBhnDxkM6wUOnDq8A0EqxR+A17OcXW9sZ2O
2ZIVwmtnyA==
-----END CERTIFICATE-----
""".trimIndent(),
// nmc2.bitcoins.sk:57002 / 46.229.238.187:57002 — expires 2030-10-22
"""
-----BEGIN CERTIFICATE-----
MIID+TCCAuGgAwIBAgIUdmJGukmfPvqmAYpTfuGcjRoYHJ8wDQYJKoZIhvcNAQEL
BQAwgYsxCzAJBgNVBAYTAlNLMREwDwYDVQQIDAhTbG92YWtpYTETMBEGA1UEBwwK
QnJhdGlzbGF2YTEUMBIGA1UECgwLYml0Y29pbnMuc2sxGTAXBgNVBAMMEG5tYzIu
Yml0Y29pbnMuc2sxIzAhBgkqhkiG9w0BCQEWFGRlYWZib3lAY2ljb2xpbmEub3Jn
MB4XDTIwMTAyNDE5MjQzOVoXDTMwMTAyMjE5MjQzOVowgYsxCzAJBgNVBAYTAlNL
MREwDwYDVQQIDAhTbG92YWtpYTETMBEGA1UEBwwKQnJhdGlzbGF2YTEUMBIGA1UE
CgwLYml0Y29pbnMuc2sxGTAXBgNVBAMMEG5tYzIuYml0Y29pbnMuc2sxIzAhBgkq
hkiG9w0BCQEWFGRlYWZib3lAY2ljb2xpbmEub3JnMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAzBUkZNDfaz7kc28l5tDKohJjekWmz1ynzfGx3ZLsqOZE
c+kNfcMaWU+zT/j0mV6pX6KSH7G9pPAku+8PRdKRq+d63wiJDEjGSaFztQWKW6L1
vTxgCK5gu+Eir3BkTagJObsrLKS+T6qH610/3+btGgoR3lunB5TzCgB/9oQanjDW
zjg2CwmxgR5Iw1Eqfenx7zkSK33FSXSF2SvbUs1Atj2oPU4DLivyrx0RaUmaPemn
cmcpnax+py4pQeB6dJWU1INhzXt3hTJRyoqsSGY3vCECIKIBIkh8GsYjAX4z+Y9y
6pJx0da2b88qPWdsoxaIMvrQiuWknDrSJwAyw2Yd8QIDAQABo1MwUTAdBgNVHQ4E
FgQUT2J83B2/9jxGGdFeWrxMohTzHNwwHwYDVR0jBBgwFoAUT2J83B2/9jxGGdFe
WrxMohTzHNwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAsbxX
wN8tZaXOybImMZCQS7zfxmKl2IAcqu+R01KPfnIfrFqXPsGDDl3rYLkwh1O4/hYQ
NKNW9KTxoJxuBmAkm7EXQQh1XUUzajdEDqDBVRyvR0Z2MdMYnMSAiiMXMl2wUZnc
QXYftBo0HbtfsaJjImQdDjmlmRPSzE/RW6iUe+1cesKBC7e8nVf69Yu/fxO4m083
VWwAstlWJfk1GyU7jzVc8svealg/oIiDoOMe6CFSLx1BDv2FeHSpRdqd3fn+AC73
bK2N2smrHUOQnFijuiFw3WOrjERi0eMhjVNfVu9W9ZYa/Wd6SdIzV55LbG+NpmSf
5W7ix41hRvdT6cTAJA==
-----END CERTIFICATE-----
""".trimIndent(),
)
}
private fun buildRpcRequest(
method: String,
params: List<Any>,