Merge pull request #1771 from mstrofnone/namecoin-nip05-improvements
fix: address race conditions and improve Namecoin NIP-05 resolution
This commit is contained in:
@@ -147,12 +147,14 @@ class AppModules(
|
||||
// Custom fetcher that considers tor settings and avoids forwarding.
|
||||
val nip05Fetcher = OkHttpNip05Fetcher(roleBasedHttpClientBuilder::okHttpClientForNip05)
|
||||
|
||||
val namecoinElectrumxClient =
|
||||
ElectrumXClient(
|
||||
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
|
||||
)
|
||||
|
||||
val namecoinResolver =
|
||||
NamecoinNameResolver(
|
||||
electrumxClient =
|
||||
ElectrumXClient(
|
||||
socketFactory = { roleBasedHttpClientBuilder.socketFactoryForNip05() },
|
||||
),
|
||||
electrumxClient = namecoinElectrumxClient,
|
||||
serverListProvider = {
|
||||
if (roleBasedHttpClientBuilder.shouldUseTorForNIP05("https://electrumx.example.com")) {
|
||||
TOR_ELECTRUMX_SERVERS
|
||||
|
||||
+13
-4
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.namecoin
|
||||
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_SERVERS
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumxServer
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinLookupCache
|
||||
@@ -43,12 +44,17 @@ class NamecoinNameService(
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
private val resolver = NamecoinNameResolver(electrumxClient)
|
||||
private val cache = NamecoinLookupCache()
|
||||
|
||||
// Custom server list (user-configurable)
|
||||
@Volatile
|
||||
private var customServers: List<ElectrumxServer> = emptyList()
|
||||
|
||||
private val resolver =
|
||||
NamecoinNameResolver(
|
||||
electrumxClient = electrumxClient,
|
||||
serverListProvider = { customServers.ifEmpty { DEFAULT_ELECTRUMX_SERVERS } },
|
||||
)
|
||||
private val cache = NamecoinLookupCache()
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -97,7 +103,10 @@ class NamecoinNameService(
|
||||
*
|
||||
* Useful for composable UIs that observe resolution state.
|
||||
*/
|
||||
fun resolveLive(identifier: String): StateFlow<NamecoinResolveState> {
|
||||
fun resolveLive(
|
||||
identifier: String,
|
||||
scope: CoroutineScope = this.scope,
|
||||
): StateFlow<NamecoinResolveState> {
|
||||
val state = MutableStateFlow<NamecoinResolveState>(NamecoinResolveState.Loading)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
+58
-10
@@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
|
||||
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinLookupException
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
|
||||
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -43,12 +44,13 @@ import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -75,27 +77,73 @@ class SearchBarViewModel(
|
||||
val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account)
|
||||
|
||||
/**
|
||||
* Resolves Namecoin identifiers (.bit / d/ / id/) via ElectrumX and
|
||||
* returns the matching [User] from LocalCache, or null.
|
||||
* Observable state for Namecoin resolution in the search bar.
|
||||
* Allows the UI to show loading spinners, resolved profiles, and
|
||||
* specific error messages ("name not found" vs "servers unreachable").
|
||||
*/
|
||||
private val namecoinResolvedUser =
|
||||
sealed class NamecoinSearchState {
|
||||
data object Idle : NamecoinSearchState()
|
||||
|
||||
data object Loading : NamecoinSearchState()
|
||||
|
||||
data class Resolved(
|
||||
val user: User,
|
||||
) : NamecoinSearchState()
|
||||
|
||||
data class NotFound(
|
||||
val name: String,
|
||||
) : NamecoinSearchState()
|
||||
|
||||
data class Expired(
|
||||
val name: String,
|
||||
) : NamecoinSearchState()
|
||||
|
||||
data object ServersUnreachable : NamecoinSearchState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves Namecoin identifiers (.bit / d/ / id/) via ElectrumX.
|
||||
* Emits typed state so the UI can show loading, resolved profile, or
|
||||
* specific error messages.
|
||||
*/
|
||||
val namecoinSearchState: StateFlow<NamecoinSearchState> =
|
||||
searchValueFlow
|
||||
.debounce(400)
|
||||
.distinctUntilChanged()
|
||||
.filter { NamecoinNameResolver.isNamecoinIdentifier(it) }
|
||||
.map { term ->
|
||||
.mapLatest { term ->
|
||||
if (!NamecoinNameResolver.isNamecoinIdentifier(term)) {
|
||||
return@mapLatest NamecoinSearchState.Idle
|
||||
}
|
||||
try {
|
||||
val result = Amethyst.instance.namecoinResolver.resolve(term)
|
||||
if (result != null) {
|
||||
LocalCache.getOrCreateUser(result.pubkey)
|
||||
NamecoinSearchState.Resolved(LocalCache.getOrCreateUser(result.pubkey))
|
||||
} else {
|
||||
null
|
||||
NamecoinSearchState.NotFound(term)
|
||||
}
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
throw e
|
||||
} catch (e: NamecoinLookupException.NameNotFound) {
|
||||
NamecoinSearchState.NotFound(e.name)
|
||||
} catch (e: NamecoinLookupException.NameExpired) {
|
||||
NamecoinSearchState.Expired(e.name)
|
||||
} catch (e: NamecoinLookupException.ServersUnreachable) {
|
||||
NamecoinSearchState.ServersUnreachable
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
NamecoinSearchState.ServersUnreachable
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(viewModelScope, WhileSubscribed(5000), null)
|
||||
.stateIn(viewModelScope, WhileSubscribed(5000), NamecoinSearchState.Idle)
|
||||
|
||||
/** Convenience: the resolved user (or null) for combining with local search results. */
|
||||
private val namecoinResolvedUser =
|
||||
namecoinSearchState
|
||||
.map { state ->
|
||||
when (state) {
|
||||
is NamecoinSearchState.Resolved -> state.user
|
||||
else -> null
|
||||
}
|
||||
}.stateIn(viewModelScope, WhileSubscribed(5000), null)
|
||||
|
||||
val searchResultsUsers =
|
||||
combine(
|
||||
|
||||
Reference in New Issue
Block a user