Merge branch 'main' of https://github.com/vitorpamplona/amethyst
* 'main' of https://github.com/vitorpamplona/amethyst: (35 commits) New Crowdin translations by GitHub Action fix chess tests Replace subsequent checks with '!isNullOrEmpty()' call use forEach: no intermediate list created. Use filterIsInstance to avoid dual iteration Lambda argument should be moved out of parentheses replace null checks with Elvis merge call chain with flatMap (slight performance improvement) New Crowdin translations by GitHub Action New Crowdin translations by GitHub Action update cz, pt, de, sv was this meant to be withContext? add names to boolean params remove unused imports Explicit type arguments can be inferred remove redundant modifiers correct package name If-Null return/break/... foldable to '?:' Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable. Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable. ... # Conflicts: # amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt
This commit is contained in:
+24
@@ -48,6 +48,30 @@ data class ElectrumxServer(
|
||||
val trustAllCerts: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Specific exception types for Namecoin resolution failures.
|
||||
* Allows callers to distinguish "name doesn't exist" from "servers unreachable".
|
||||
*/
|
||||
sealed class NamecoinLookupException(
|
||||
message: String,
|
||||
cause: Throwable? = null,
|
||||
) : Exception(message, cause) {
|
||||
/** The name was queried successfully but does not exist on the blockchain. */
|
||||
class NameNotFound(
|
||||
val name: String,
|
||||
) : NamecoinLookupException("Name not found: $name")
|
||||
|
||||
/** The name has expired (>36000 blocks since last update). */
|
||||
class NameExpired(
|
||||
val name: String,
|
||||
) : NamecoinLookupException("Name expired: $name")
|
||||
|
||||
/** All ElectrumX servers were unreachable or returned errors. */
|
||||
class ServersUnreachable(
|
||||
val lastError: Throwable? = null,
|
||||
) : NamecoinLookupException("All ElectrumX servers unreachable", lastError)
|
||||
}
|
||||
|
||||
/** Well-known public Namecoin ElectrumX servers (clearnet). */
|
||||
val DEFAULT_ELECTRUMX_SERVERS =
|
||||
listOf(
|
||||
|
||||
+31
-4
@@ -206,16 +206,43 @@ class NamecoinNameResolver(
|
||||
// Extended form: "nostr": { "names": {...}, "relays": {...} }
|
||||
if (nostrField is JsonObject) {
|
||||
val names = nostrField["names"]?.jsonObject ?: return null
|
||||
val pubkeyElem = names[parsed.localPart] ?: names["_"] // fall back to root
|
||||
val pubkey = (pubkeyElem as? JsonPrimitive)?.content ?: return null
|
||||
if (!isValidPubkey(pubkey)) return null
|
||||
|
||||
// Resolve: exact match → "_" root → first entry (root lookups only)
|
||||
val resolvedLocalPart: String
|
||||
val pubkey: String
|
||||
|
||||
val exactMatch = names[parsed.localPart]
|
||||
val rootMatch = names["_"]
|
||||
val firstEntry = if (parsed.localPart == "_") names.entries.firstOrNull() else null
|
||||
|
||||
when {
|
||||
exactMatch is JsonPrimitive && isValidPubkey(exactMatch.content) -> {
|
||||
resolvedLocalPart = parsed.localPart
|
||||
pubkey = exactMatch.content
|
||||
}
|
||||
|
||||
rootMatch is JsonPrimitive && isValidPubkey(rootMatch.content) -> {
|
||||
resolvedLocalPart = "_"
|
||||
pubkey = rootMatch.content
|
||||
}
|
||||
|
||||
firstEntry != null && firstEntry.value is JsonPrimitive &&
|
||||
isValidPubkey((firstEntry.value as JsonPrimitive).content) -> {
|
||||
resolvedLocalPart = firstEntry.key
|
||||
pubkey = (firstEntry.value as JsonPrimitive).content
|
||||
}
|
||||
|
||||
else -> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val relays = extractRelays(nostrField, pubkey)
|
||||
return NamecoinNostrResult(
|
||||
pubkey = pubkey.lowercase(),
|
||||
relays = relays,
|
||||
namecoinName = parsed.namecoinName,
|
||||
localPart = parsed.localPart,
|
||||
localPart = resolvedLocalPart,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+23
-6
@@ -44,6 +44,7 @@ import java.net.Socket
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.net.SocketFactory
|
||||
import javax.net.ssl.SSLContext
|
||||
@@ -83,7 +84,7 @@ class ElectrumXClient(
|
||||
isLenient = true
|
||||
}
|
||||
private val requestId = AtomicInteger(0)
|
||||
private val mutex = Mutex()
|
||||
private val serverMutexes = ConcurrentHashMap<String, Mutex>()
|
||||
|
||||
companion object {
|
||||
private const val PROTOCOL_VERSION = "1.4"
|
||||
@@ -121,9 +122,13 @@ class ElectrumXClient(
|
||||
server: ElectrumxServer = DEFAULT_ELECTRUMX_SERVERS.first(),
|
||||
): NameShowResult? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val mutex = serverMutexes.getOrPut("${server.host}:${server.port}") { Mutex() }
|
||||
mutex.withLock {
|
||||
try {
|
||||
connectAndQuery(identifier, server)
|
||||
} catch (e: NamecoinLookupException) {
|
||||
// Propagate name-not-found and expired — these are definitive answers.
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
// Log but don't crash — callers handle null gracefully.
|
||||
e.printStackTrace()
|
||||
@@ -137,16 +142,28 @@ class ElectrumXClient(
|
||||
*
|
||||
* @param identifier Full Namecoin name, e.g. "d/example"
|
||||
* @param servers Ordered server list to try; defaults to [DEFAULT_ELECTRUMX_SERVERS]
|
||||
* @throws NamecoinLookupException.NameNotFound if the name definitively doesn't exist
|
||||
* @throws NamecoinLookupException.NameExpired if the name has expired
|
||||
* @throws NamecoinLookupException.ServersUnreachable if all servers failed with connection errors
|
||||
*/
|
||||
override suspend fun nameShowWithFallback(
|
||||
identifier: String,
|
||||
servers: List<ElectrumxServer>,
|
||||
): NameShowResult? {
|
||||
var lastError: Exception? = null
|
||||
for (server in servers) {
|
||||
val result = nameShow(identifier, server)
|
||||
if (result != null) return result
|
||||
try {
|
||||
val result = nameShow(identifier, server)
|
||||
if (result != null) return result
|
||||
} catch (e: NamecoinLookupException.NameNotFound) {
|
||||
throw e // Definitive answer from blockchain — no point trying other servers
|
||||
} catch (e: NamecoinLookupException.NameExpired) {
|
||||
throw e // Definitive answer
|
||||
} catch (e: Exception) {
|
||||
lastError = e // Server error — try next server
|
||||
}
|
||||
}
|
||||
return null
|
||||
throw NamecoinLookupException.ServersUnreachable(lastError)
|
||||
}
|
||||
|
||||
// ── internals ──────────────────────────────────────────────────────
|
||||
@@ -175,7 +192,7 @@ class ElectrumXClient(
|
||||
writer.println(historyReq)
|
||||
val historyResponse = reader.readLine() ?: return null
|
||||
val historyEntries = parseHistoryResponse(historyResponse) ?: return null
|
||||
if (historyEntries.isEmpty()) return null
|
||||
if (historyEntries.isEmpty()) throw NamecoinLookupException.NameNotFound(identifier)
|
||||
|
||||
// 4. Get the latest transaction (last entry = most recent update)
|
||||
val latestEntry = historyEntries.last()
|
||||
@@ -196,7 +213,7 @@ class ElectrumXClient(
|
||||
if (currentHeight != null && height > 0) {
|
||||
val blocksSinceUpdate = currentHeight - height
|
||||
if (blocksSinceUpdate >= NAME_EXPIRE_DEPTH) {
|
||||
return null // Name has expired
|
||||
throw NamecoinLookupException.NameExpired(identifier)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+65
-4
@@ -115,6 +115,36 @@ class NamecoinNameResolverTest {
|
||||
assertEquals("aaaa000000000000000000000000000000000000000000000000000000000001", result!!.pubkey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `root lookup falls back to first entry when no underscore key`() {
|
||||
val value = """{
|
||||
"nostr": {
|
||||
"names": {
|
||||
"m": "6cdebccabda1dfa058ab85352a79509b592b2bdfa0370325e28ec1cb4f18667d"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
val result = extractNostrFromValue(value, "d/testls", "_")
|
||||
assertNotNull(result)
|
||||
assertEquals("6cdebccabda1dfa058ab85352a79509b592b2bdfa0370325e28ec1cb4f18667d", result!!.pubkey)
|
||||
assertEquals("m", result.localPart)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-root lookup does NOT fall back to first entry`() {
|
||||
val value = """{
|
||||
"nostr": {
|
||||
"names": {
|
||||
"m": "6cdebccabda1dfa058ab85352a79509b592b2bdfa0370325e28ec1cb4f18667d"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
val result = extractNostrFromValue(value, "d/testls", "alice")
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
// ── Value format: id/ namespace ────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@@ -209,9 +239,40 @@ class NamecoinNameResolverTest {
|
||||
// Extended form
|
||||
if (nostrField is kotlinx.serialization.json.JsonObject) {
|
||||
val names = nostrField["names"]?.jsonObject ?: return null
|
||||
val pubkeyElem = names[localPart] ?: names["_"] ?: return null
|
||||
val pubkey = (pubkeyElem as? kotlinx.serialization.json.JsonPrimitive)?.content ?: return null
|
||||
if (!pubkey.matches(Regex("^[0-9a-fA-F]{64}$"))) return null
|
||||
|
||||
// Resolve: exact match → "_" root → first entry (root lookups only)
|
||||
val resolvedLocalPart: String
|
||||
val pubkey: String
|
||||
|
||||
val exactMatch = names[localPart]
|
||||
val rootMatch = names["_"]
|
||||
val firstEntry = if (localPart == "_") names.entries.firstOrNull() else null
|
||||
|
||||
when {
|
||||
exactMatch is kotlinx.serialization.json.JsonPrimitive &&
|
||||
exactMatch.content.matches(Regex("^[0-9a-fA-F]{64}$")) -> {
|
||||
resolvedLocalPart = localPart
|
||||
pubkey = exactMatch.content
|
||||
}
|
||||
|
||||
rootMatch is kotlinx.serialization.json.JsonPrimitive &&
|
||||
rootMatch.content.matches(Regex("^[0-9a-fA-F]{64}$")) -> {
|
||||
resolvedLocalPart = "_"
|
||||
pubkey = rootMatch.content
|
||||
}
|
||||
|
||||
firstEntry != null && firstEntry.value is kotlinx.serialization.json.JsonPrimitive &&
|
||||
(firstEntry.value as kotlinx.serialization.json.JsonPrimitive)
|
||||
.content
|
||||
.matches(Regex("^[0-9a-fA-F]{64}$")) -> {
|
||||
resolvedLocalPart = firstEntry.key
|
||||
pubkey = (firstEntry.value as kotlinx.serialization.json.JsonPrimitive).content
|
||||
}
|
||||
|
||||
else -> {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val relays =
|
||||
try {
|
||||
@@ -227,7 +288,7 @@ class NamecoinNameResolverTest {
|
||||
pubkey = pubkey.lowercase(),
|
||||
relays = relays,
|
||||
namecoinName = namecoinName,
|
||||
localPart = localPart,
|
||||
localPart = resolvedLocalPart,
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user