feat: wire Namecoin search into SearchBarViewModel

Resolve .bit, d/, and id/ identifiers from the search bar via
ElectrumX blockchain lookups. Typing any Namecoin identifier format
(m@testls.bit, testls.bit, d/testls, id/alice) now queries the
Namecoin blockchain and shows the resolved user at the top of results.

The namecoinResolvedUser flow in SearchBarViewModel detects Namecoin
identifiers, resolves them through NamecoinNameService, and prepends
the result to the standard local cache search results.

Updated docs with search integration details and manual testing guide.
This commit is contained in:
M
2026-03-02 19:43:24 +11:00
parent d82ace2f63
commit f6447d2020
2 changed files with 80 additions and 8 deletions
@@ -32,8 +32,11 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.namecoin.NamecoinNameService
import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState
import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder
import com.vitorpamplona.quartz.nip05.namecoin.NamecoinNameResolver
import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import com.vitorpamplona.quartz.nip10Notes.content.findHashtags
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@@ -43,7 +46,9 @@ import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -69,12 +74,41 @@ class SearchBarViewModel(
val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account) val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account)
/**
* Resolves Namecoin identifiers (.bit / d/ / id/) via ElectrumX and
* returns the matching [User] from LocalCache, or null.
*/
private val namecoinResolvedUser =
searchValueFlow
.debounce(400)
.distinctUntilChanged()
.filter { NamecoinNameResolver.isNamecoinIdentifier(it) }
.map { term ->
try {
val result = NamecoinNameService.getInstance().resolve(term)
if (result != null) {
LocalCache.getOrCreateUser(result.pubkey)
} else {
null
}
} catch (_: Exception) {
null
}
}.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, WhileSubscribed(5000), null)
val searchResultsUsers = val searchResultsUsers =
combine( combine(
searchValueFlow.debounce(100), searchValueFlow.debounce(100),
invalidations.debounce(100), invalidations.debounce(100),
) { term, version -> namecoinResolvedUser,
LocalCache.findUsersStartingWith(term, account) ) { term, version, namecoinUser ->
val localResults = LocalCache.findUsersStartingWith(term, account)
if (namecoinUser != null && localResults.none { it.pubkeyHex == namecoinUser.pubkeyHex }) {
listOf(namecoinUser) + localResults
} else {
localResults
}
}.flowOn(Dispatchers.IO) }.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList())
+44 -6
View File
@@ -154,6 +154,17 @@ The integration is minimal and non-invasive:
3. Non-Namecoin identifiers are completely unaffected 3. Non-Namecoin identifiers are completely unaffected
4. **`AppModules`** wires up the resolver at construction time 4. **`AppModules`** wires up the resolver at construction time
## Search Integration
The search bar resolves Namecoin identifiers in real-time via `SearchBarViewModel`:
1. A `namecoinResolvedUser` flow watches the search input with a 400ms debounce
2. If the input matches any Namecoin format (`d/*`, `id/*`, `*.bit`, `*@*.bit`), it resolves via `NamecoinNameService``ElectrumxClient` → blockchain
3. The resolved pubkey is used to get/create a `User` in `LocalCache`
4. The Namecoin-resolved user is prepended to the standard local search results (deduplicated)
This means typing `alice@example.bit`, `example.bit`, `d/example`, or `id/alice` into the search bar will query the Namecoin blockchain and show the resolved user profile at the top of results.
## Default ElectrumX Server ## Default ElectrumX Server
``` ```
@@ -202,6 +213,7 @@ Search bar integration. When a user types a `.bit` identifier, shows a loading s
### Modified files ### Modified files
- `amethyst/.../AppModules.kt` — Wire up `NamecoinNameResolver` into `Nip05Client` - `amethyst/.../AppModules.kt` — Wire up `NamecoinNameResolver` into `Nip05Client`
- `amethyst/.../ui/screen/loggedIn/search/SearchBarViewModel.kt` — Namecoin search resolution
- `quartz/.../nip05DnsIdentifiers/Nip05Client.kt` — Route `.bit` identifiers to Namecoin resolver - `quartz/.../nip05DnsIdentifiers/Nip05Client.kt` — Route `.bit` identifiers to Namecoin resolver
- `amethyst/.../relays/RelayInformationScreen.kt` — Import reordering (spotless) - `amethyst/.../relays/RelayInformationScreen.kt` — Import reordering (spotless)
@@ -212,13 +224,34 @@ Search bar integration. When a user types a `.bit` identifier, shows a loading s
./gradlew :quartz:jvmTest --tests "*NamecoinNameResolverTest*" ./gradlew :quartz:jvmTest --tests "*NamecoinNameResolverTest*"
``` ```
### Manual testing (emulator) ### Manual testing (emulator or device)
1. Build and install: `./gradlew :amethyst:installFdroidDebug`
2. Search for `m@testls.bit` — should resolve to pubkey `6cdebcca...18667d`
3. Search for `testls.bit` — root domain lookup
4. Search for `d/testls` — direct namespace format
### Live verification Build and install the debug APK:
```bash
./gradlew assemblePlayDebug
adb install -r amethyst/build/outputs/apk/play/debug/amethyst-play-universal-debug.apk
```
**Search bar tests** — open the search bar and enter each of these:
| Search query | Expected result | What it tests |
|---|---|---|
| `m@testls.bit` | Resolves to Vitor Pamplona's profile | NIP-05 style `user@domain.bit` |
| `testls.bit` | Resolves to Vitor Pamplona's profile (root `_` entry) | Bare domain `.bit` lookup |
| `d/testls` | Resolves to Vitor Pamplona's profile | Direct `d/` namespace |
| `id/someuser` | Resolves if registered on-chain | Direct `id/` namespace |
**Verification test** — if a profile has a `.bit` address in its `nip05` field, the NIP-05 badge should verify via the blockchain instead of HTTP.
**Network verification** — to confirm ElectrumX calls are being made:
```bash
# Monitor traffic to ElectrumX ports on the emulator
adb root
adb shell tcpdump -i any -nn port 50002 or port 50006
```
You should see TCP connections to `162.212.154.52:50002` (electrumx.testls.space) when searching for `.bit` identifiers.
### Live test data
The name `d/testls` is registered on the Namecoin blockchain (block 551519+, last updated block 814278) with value: The name `d/testls` is registered on the Namecoin blockchain (block 551519+, last updated block 814278) with value:
```json ```json
{ {
@@ -229,3 +262,8 @@ The name `d/testls` is registered on the Namecoin blockchain (block 551519+, las
} }
} }
``` ```
This means:
- `m@testls.bit` → resolves `m` entry → pubkey `6cdebcca...18667d`
- `testls.bit` → resolves root `_` entry → falls back to first available entry
- `d/testls` → same as `testls.bit` (root lookup)