* '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:
Vitor Pamplona
2026-03-07 16:47:16 -05:00
144 changed files with 807 additions and 370 deletions
@@ -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)
}
}