Commit Graph
27 Commits
Author SHA1 Message Date
m 9afa4a120f docs(namecoin): refresh design doc to match current main
- Update DEFAULT_ELECTRUMX_SERVERS table to the 6 clearnet entries
  actually shipped (testls.space + nmc2.bitcoins.sk + IP peer +
  relay.testls.bit + IP peer + electrum.nmc.ethicnology.com); remove
  ulrichard.ch and nmc2.lelux.fi which were retired.
- Update TOR_ELECTRUMX_SERVERS table to the current 7 entries
  including the relay.testls.bit hidden service on port 50001.
- Document the Namecoin Core RPC backend (NamecoinCoreRpcClient,
  NamecoinCoreRpcConfig, StartOS / umbrel / onion URL paths).
- Document CompositeNamecoinBackend + NamecoinFallbackPolicy chain
  (primary -> custom ElectrumX -> default ElectrumX) and the
  short-circuit-on-NameNotFound rule.
- Document the ifa-0001 `import` resolver (NamecoinImportResolver),
  including the bare-string / array short-hand forms and the
  default depth-4 / cycle-safe recursion.
- Document TOFU cert pinning (PINNED_ELECTRUMX_CERTS plus the
  user-supplied PEM store on Android and Desktop) and the Test
  Connection diagnostic returning ServerTestResult.
- Document name expiry enforcement on both backends and the
  NamecoinResolveOutcome sealed type used by resolveDetailed().
- Add a Commons module section covering NamecoinSettings (the shared
  serializable config) and NamecoinResolveState (UI state model).
- Add a Desktop section covering DesktopNamecoinNameService,
  DesktopNamecoinPreferences, LocalNamecoin lazy init, and the
  desktop NamecoinSettingsSection.
- Rename trustAllCerts -> usePinnedTrustStore throughout the doc,
  matching the rename in code.
- Update the Quartz paths from `nip05/namecoin/` to
  `nip05DnsIdentifiers/namecoin/` (the actual package layout) and
  refresh the file list to match what is on main today.
- Refresh the testing section: add backend-picker, on-chain zap,
  and NameNotFound cases; update tcpdump port set to cover 50001 /
  57002 / 8336; replace stale single IP example with reference to
  DEFAULT_ELECTRUMX_SERVERS.
- Architecture diagram now shows CompositeNamecoinBackend,
  NamecoinImportResolver, and NamecoinCoreRpcClient alongside
  ElectrumXClient.

No code changes.
2026-06-01 08:26:07 +10:00
m d9d7b44e91 feat(desktop): Namecoin Core RPC backend + composite fallback
Brings Amethyst Desktop to feature parity with Android for the Namecoin
resolution backend stack landed in #3056 / #3068. Three pieces:

- DesktopNamecoinPreferences now persists backend, namecoinCoreRpc,
  fallbackToCustomElectrumx and fallbackToDefaultElectrumx (KEY_BACKEND
  / KEY_CORE_RPC / KEY_FALLBACK_*), mirroring NamecoinSharedPreferences.
  Mutators are non-suspend because java.util.prefs is synchronous,
  unlike Android's coroutine-backed DataStore. The Jackson mapper now
  rejects unknown properties on read so kotlinx `@Serializable`
  computed getters (e.g. NamecoinCoreRpcConfig.isUsable) round-trip
  cleanly through java.util.prefs.

- DesktopNamecoinNameService takes an optional OkHttpClient provider,
  lazily constructs a NamecoinCoreRpcClient when supplied, and builds
  a fresh CompositeNamecoinBackend per lookup based on current
  NamecoinSettings. Same shape as AppModules#buildNamecoinBackend.
  Exposes the underlying RPC client (rpcClient) and a probeCoreRpc(cfg)
  helper for the Settings Test RPC button.

- NamecoinSettingsSection gains a backend radio selector, a Core RPC
  subform (URL / username / password / Save / Test RPC) with a TOFU
  cert-pin AlertDialog mirroring Android's NamecoinCoreRpcSection, and
  a fallback toggles section. The same KEY_PINNED_CERTS list is shared
  with both ElectrumXClient and NamecoinCoreRpcClient via
  setDynamicCerts(...), matching Android's behaviour where both
  backends consume one trust store.

- Main.kt wires DesktopHttpClient.currentClient() in as the Core RPC
  HTTP provider so .onion RPC URLs flow through the existing Tor
  routing without extra plumbing, and propagates the new mutators to
  the Settings UI.

- Extends DesktopNamecoinPreferencesTest with 8 new cases covering
  default state, backend round-trip, Core RPC URL/user/pass/pin-flag
  round-trip, fallback toggles, reset clearing, and a full
  multi-field round-trip across a fresh preferences instance.

Verification on the canonical workspace clone:
- ./gradlew :commons:jvmTest --tests *Namecoin* — BUILD SUCCESSFUL
- ./gradlew :amethyst:compilePlayDebugKotlin — BUILD SUCCESSFUL
- ./gradlew :desktopApp:compileKotlin :desktopApp:test — BUILD SUCCESSFUL
- ./gradlew :amethyst:testPlayDebugUnitTest --tests *Namecoin* — BUILD SUCCESSFUL
- ./gradlew :amethyst:spotlessCheck :commons:spotlessCheck
  :desktopApp:spotlessCheck — BUILD SUCCESSFUL
2026-05-28 06:01:54 +10:00
m b5b70fe693 feat(desktop): persist TOFU-pinned Namecoin ElectrumX certs
Mirrors Android's NamecoinSharedPreferences pinned-cert API on Desktop so
user-accepted TLS pins survive process restart. Same JSON-list shape, same
distinct-append semantics, same wipe-on-reset behaviour.

What's new
- DesktopNamecoinPreferences gains addPinnedCert / loadPinnedCerts /
  clearPinnedCerts (sync rather than suspend, since java.util.prefs is
  synchronous). reset() now clears pinned certs too, matching Android.
- DesktopNamecoinNameService accepts a pinnedCertsProvider and pushes the
  loaded list into ElectrumXClient.setDynamicCerts at init, mirroring
  Android's AppModules.kt wiring. Exposes the underlying client so the
  Settings UI can call testServer() and re-apply pins live.
- Desktop NamecoinSettingsSection grows an optional Test Connection + TOFU
  pin sub-section: runs ElectrumXClient.testServer per active server,
  collects PEM + SHA-256 fingerprint from successful TLS handshakes, and
  prompts the user to pin each new cert via AlertDialog. UI hidden when
  no service is wired (so existing call sites stay valid).
- Main.kt wires both halves together and updates the freshly-pinned cert
  list into the live client without waiting for restart.

Persistence is plain java.util.prefs (same backing store as the rest of
DesktopNamecoinPreferences) — explicitly NOT EncryptedSharedPreferences.
Pinned cert PEMs are public material; no secrets stored.

Tests
- DesktopNamecoinPreferencesTest: +6 cases covering empty default,
  persistence + reload, dedup, blank input ignored, reset wipes, and
  independence from settings copies.

Verification
- ./gradlew :desktopApp:compileKotlin — BUILD SUCCESSFUL
- ./gradlew :desktopApp:test — BUILD SUCCESSFUL (16 tests, 0 failures)
- ./gradlew :amethyst:compilePlayDebugKotlin — BUILD SUCCESSFUL
- ./gradlew :amethyst:spotlessCheck :commons:spotlessCheck :desktopApp:spotlessCheck — BUILD SUCCESSFUL

Stack note
Stacked behind #3072 (merged 2026-05-27). Next: PR-C for the full
Namecoin Core RPC backend + composite fallback persistence + UI.
2026-05-28 05:41:22 +10:00
m 67edb32fa7 refactor(namecoin): consolidate NamecoinSettings into commons
Two NamecoinSettings classes had drifted:

- commons (used by Desktop): only enabled + customServers
- amethyst service.namecoin (used by Android): full schema with backend,
  namecoinCoreRpc, fallbackToCustomElectrumx, fallbackToDefaultElectrumx

This left Desktop unable to persist any of the Namecoin Core RPC or
fallback-policy state introduced in the Android settings UI. Promote
the rich Android version into commons as the single source of truth
and delete the Android duplicate.

- Move the rich schema (backend, namecoinCoreRpc, fallback toggles,
  hasUsableCoreRpc, toFallbackPolicy) into the commons NamecoinSettings.
- Delete amethyst/service/namecoin/NamecoinSettings.kt and its test.
- Repoint the two Android imports (NamecoinSharedPreferences,
  NamecoinSettingsSection) at the commons class. No behaviour change on
  Android.
- Fold the Android-only backend/RPC/fallback test cases into the commons
  NamecoinSettingsTest so the shared schema stays covered.

Desktop persistence (DesktopNamecoinPreferences) still only reads/writes
enabled + customServers; the extra commons fields fall back to defaults
on the existing Desktop store. Wiring those new fields into Desktop is
the next change.
2026-05-28 05:06:44 +10:00
m 216176eeb7 fix(search): route d/ and id/ Namecoin namespaces through the resolution row
The inline Namecoin resolution row in the global search bar (and the
on-chain zap recipient field) is gated by looksLikeNamecoinIdentifier,
which previously only matched the '.bit' shapes. Direct namespace
references like 'id/mstrofnone' or 'd/mstrofnone' — both accepted by
NamecoinNameResolver.isNamecoinIdentifier — fell through the gate and
the resolver was never called, so no on-chain feedback appeared in the
search bar.

Bring the UI gate in line with the resolver:

  - accept 'd/<name>' (domain namespace, direct reference)
  - accept 'id/<name>' (identity namespace)
  - lower the length floor so single-character labels (valid, if
    expensive, on chain) still trigger; '.bit' inputs keep their
    5-char floor

Doc comment for the row's behaviour and the NamecoinResolutionRow
KDoc are updated to reflect the broader accepted set. New unit tests
cover both new prefixes (with case-insensitivity and leading '@'),
short single-label names, bare-prefix rejection, and explicit
non-routing for other Namecoin namespaces ('a/', 'u/').
2026-05-28 04:46:55 +10:00
m cfb3f1b9fa feat(namecoin): TOFU pin for Namecoin Core RPC TLS path
When the user picks the Namecoin Core RPC backend and points at a
self-hosted node behind a self-signed cert (StartOS / Start9, umbrel,
LAN reverse proxy, …) the previous flow only worked if the cert's CA
was already in the device trust store. There was no in-app way to
inspect or pin the certificate, so users had to install the StartOS
root CA at the OS level — or settle for an unencrypted onion path.

This change brings the Namecoin Core RPC path up to parity with the
existing ElectrumX path:

  - NamecoinCoreRpcClient.probe() now opens a short-lived, no-auth TLS
    socket alongside the JSON-RPC call to capture the server's leaf
    certificate (PEM + SHA-256 fingerprint). Capture is best-effort
    and only runs for https:// URLs. Credentials are never sent over
    the inspection socket.

  - RpcProbeResult exposes serverCertPem, certFingerprint, and
    tlsHandshakeFailed so the Settings UI can react. New fields are
    nullable / default false so existing callers compile unchanged.

  - NamecoinCoreRpcClient maintains its own dynamic-cert keystore and
    a lazy pinned SSLSocketFactory (same shape as ElectrumXClient's,
    minus the hardcoded list — Core RPC has no public defaults). When
    cfg.usePinnedTrustStore is true and the URL is https, callRpc()
    routes through the pinned factory with a permissive hostname
    verifier (LAN/onion certs commonly carry IP-only SANs).

  - NamecoinSettingsSection's Namecoin Core RPC card now shows a
    'Trust Server Certificate?' AlertDialog after Test RPC when the
    probe captured a cert and the user hasn't pinned yet, reusing the
    existing namecoin_pin_cert_* strings. Accept persists the PEM
    AND flips usePinnedTrustStore=true on the config. The result
    card also displays the captured fingerprint and a '(pinned)'
    marker so the user can see the current trust state at a glance.

  - The pinned PEM list is stored in the existing
    KEY_PINNED_CERTS DataStore entry, so a single TOFU confirmation
    covers both backends. AppModules' namecoinCoreRpcClient init now
    bootstraps the pinned list on app start, matching ElectrumX.

  - Tests cover the new probe fields' defaults and the addPinnedCert
    / setDynamicCerts surface.

Local verification: builds clean (assembleFdroidDebug), :quartz:jvmTest
NamecoinCoreRpcClientTest all green, :amethyst:testFdroidDebugUnitTest
namecoin suites all green.
2026-05-28 03:32:28 +10:00
m daa7c7913e feat(namecoin): mention umbrel alongside StartOS in docs and UI hints
Both umbrelOS (via getumbrel/umbrel-apps#4962) and StartOS / Start9
(via Start9-Community/namecoin-core-startos) ship a self-hosted
Namecoin Core that this backend can target. Generalize the
help/strings so umbrel users discover the feature too.

No logic changes.
2026-05-27 14:50:37 +10:00
m a58e7164b5 feat(namecoin): add Namecoin Core RPC backend with optional ElectrumX fallback
Adds a second resolution backend alongside the existing ElectrumX path:
users can now point Amethyst directly at a Namecoin Core full node
(e.g. a StartOS / Start9 installation) instead of (or in addition to)
trusting public ElectrumX operators.

Settings -> Namecoin grows three new pieces:

  1. Backend selector (radio) - ElectrumX | Namecoin Core RPC
  2. Core RPC section - URL, username, password, masked password,
     'Test RPC' button that calls getblockchaininfo and reports
     chain / height / sync %, error path with diagnostic message
  3. Fallback policy - independent toggles for falling back to the
     user's custom ElectrumX servers (Core RPC primary only) and/or
     the hardcoded public ElectrumX defaults

Quartz additions:
  - NamecoinBackend enum, NamecoinCoreRpcConfig (kotlinx.serialization),
    NamecoinFallbackPolicy
  - NamecoinNameBackend interface + ElectrumxNameBackend adapter +
    CompositeNamecoinBackend orchestrator (implements IElectrumXClient
    so NamecoinNameResolver is unchanged)
  - NamecoinCoreRpcClient (jvmAndroid) - JSON-RPC name_show /
    getblockchaininfo over OkHttp, reuses
    roleBasedHttpClientBuilder.okHttpClientForNip05() so Tor onion
    endpoints work without extra plumbing

Semantics:
  - Authoritative negatives (NameNotFound, NameExpired) short-circuit
    the chain - no silent privacy leak to other backends
  - Only transport / unreachable failures cascade through the chain
  - All fallback toggles default off (custom servers stay exclusive,
    matching existing behaviour)
  - Settings persisted via NamecoinSharedPreferences DataStore
  - HTTP transport delegated to roleBasedHttpClientBuilder so existing
    Tor/proxy/cert pinning all works for Core RPC too

Tests:
  - CompositeNamecoinBackendTest (8 cases) - short-circuit, cascade,
    authoritative-negative, electrumx-primary path, expired-name,
    random-exception-cascade
  - NamecoinCoreRpcClientTest (7 cases) - success parsing, auth header,
    name-not-found, expired, generic RPC errors, unusable config,
    probe success + auth failure
  - NamecoinSettingsTest (8 cases) - parser plus new backend / fallback
    fields

Builds clean: :amethyst:compileFdroidDebugKotlin, :quartz:jvmTest.
2026-05-27 14:03:47 +10:00
m 1c5230cfc5 feat(search): inline Namecoin resolution indicator in global search bar
Reuses the NamecoinResolutionRow composable already shipping for the
onchain-zap recipient field, promoting it from
ui/screen/loggedIn/wallet/ to a generic ui/components/namecoin/
location so it can be mounted anywhere a .bit-shaped search input may
race the local-cache prefix search.

In the global search bar, typing a bare ".bit" host (e.g.
"testls.bit") used to surface a cached sibling profile like
"m@testls.bit" first (LocalCache.findUsersStartingWith hits the
prefix) and only several seconds later be corrected by the slower
on-chain ElectrumX resolution from
SearchBarViewModel.directNip05Resolver. No in-flight indicator and no
feedback on hard failures (timeout, malformed record, etc.).

Changes:

  - git-rename NamecoinResolutionRow.kt and its test from
    ui/screen/loggedIn/wallet/ to ui/components/namecoin/, updating
    the package declaration only.
  - Add an optional `modifier: Modifier = Modifier` parameter to the
    composable (standard Compose convention) and wrap the spinner /
    result / error rows in a Column taking the caller-provided
    modifier. No visual change in OnchainZapSendDialog.
  - Update OnchainZapSendDialog import to the new package location.
  - Mount NamecoinResolutionRow in SearchScreen.SearchBar between
    SearchTextField and SearchFilterRow, with horizontal padding to
    match the rest of the bar. onUserResolved navigates to the user
    and clears the field, matching the bech32 auto-resolve path in
    SearchBarViewModel.directRouteResolver.

State is held in the shared
commons.NamecoinResolveState (no new state class introduced) and
diagnostic wording comes from the existing mapOutcomeToResolveState
helper, so every Namecoin surface continues to produce the same
message for the same outcome.
2026-05-19 06:33:47 +10:00
m 968a396779 feat(electrumx): add electrum.nmc.ethicnology.com to default server set
Adds a third public Namecoin ElectrumX server to the default and
Tor-preferred lists in DEFAULT_ELECTRUMX_SERVERS / TOR_ELECTRUMX_SERVERS:

  electrum.nmc.ethicnology.com:50002  (IPv4 142.44.246.181, OVH Canada)

Operated by @ethicnology, who ships the namecoind + ElectrumX + mempool
podman stack at github.com/ethicnology/namecoin-compose. Probed live:

  - server.version      -> ElectrumX 1.19.0, protocol 1.4
  - server.features     -> Namecoin mainnet genesis 000000000062b72c...c770
  - scripthash.get_history for d/testls -> full history (heights up to
    822885), and blockchain.transaction.get decodes the OP_NAME_UPDATE
    output correctly. Same code path used by ElectrumXClient against all
    other public servers, no client changes required.

TLS uses a publicly-trusted Let's Encrypt cert, so usePinnedTrustStore
is left at the default (false). This makes it the first entry in the
list whose TLS does NOT depend on PINNED_ELECTRUMX_CERTS, and adds
useful diversity:

  - electrumx.testls.space      (self-signed, pinned, often ECONNRESETs)
  - nmc2.bitcoins.sk / 46.229.238.187  (self-signed, pinned)
  - relay.testls.bit / 23.158.233.10   (self-signed, pinned)
  - electrum.nmc.ethicnology.com       (LE cert, system trust store)

If every self-signed peer is unreachable (e.g. corporate networks that
strip unknown CAs but allow LE chains), resolution can still succeed.

No bare-IP companion entry is added for 142.44.246.181: unlike the
46.229.238.187 / 23.158.233.10 pinned peers (which use DER-SHA256
pinning that ignores hostname verification), an IP-literal endpoint
against the LE cert would fail standard hostname verification under
the system trust manager (SAN covers only the hostname). The IP is
captured in this commit message and the source comment for reference.

Verification on this branch:
  - :quartz:spotlessCheck   OK
  - :quartz:jvmTest         OK (BitRelayResolverTest etc. unchanged)
2026-05-17 17:09:42 +10:00
m 931a217251 fix(desktop): mirror Android ProGuard strategy for release builds
Compose Multiplatform 1.11.0 wired ProGuard 7.7.0 into the release
build for the first time. With optimize + shrink + obfuscate all on,
the desktop v1.09.1 DMG hit four distinct runtime failures:

  A. Jackson's SerializationFeature.values()/valueOf() stripped, so
     Class.getEnumConstants() returned null and ObjectMapper failed
     to initialise (app didn't launch). Fixed in #2921.
  B. fr.acinq.secp256k1.* JNI bridge classes renamed by obfuscate;
     bundled libsecp256k1-jni.dylib could not FindClass the original
     names, so KeyPair / sign / verify crashed on first use.
  C. androidx.sqlite-bundled native declarations (nativeThreadSafeMode,
     nativeOpen) shrunk away because no Kotlin caller referenced them
     directly; libsqliteJni.dylib raised NoSuchMethodError on the first
     EventStore query.
  D. ProGuard's method/specialization/returntype optimize sub-pass
     generated a synthetic okio bridge (Okio__OkioKt.buffer$<hash>)
     whose declared return type was RealBufferedSource but whose body
     returned the BufferedSource super-interface. The JVM verifier
     rejected the bridge, killing every OkHttp connection.

The Android (mobile) module solved the same class of problems in
amethyst/proguard-rules.pro with a single global strategy:

    -dontobfuscate
    -keepnames class ** { *; }
    -keep enum ** { *; }
    + per-library -keep rules for JNA / libsodium / libscrypt
    + first-party -keep com.vitorpamplona.**

This commit replays that strategy in desktopApp/compose-rules.pro
and drops the previous optimize.set(false)/obfuscate.set(false)
escape hatch in desktopApp/build.gradle.kts. Shrink and optimize
both stay on; only the one optimize sub-pass that produced invalid
okio bytecode (method/specialization/returntype) is disabled.

Additional desktop-only keep rules (Jackson, full JNA, VLCj,
SLF4J, OkHttp / Conscrypt / BouncyCastle / OpenJSSE / Graal
dontwarns) stay in place. Two libraries shipped only on desktop
also get explicit -keep rules so their native callbacks survive
shrink:

  - androidx.sqlite.** + native <methods>; (nativeThreadSafeMode
    was previously stripped even with -keepnames, because shrink
    removes members the global rule doesn't cover).
  - pt.davidafsilva.apple.** + native <methods>; for the macOS
    Keychain JNI bridge.

Verified on macOS arm64 packageReleaseDmg:
  - javap on the post-ProGuard jars confirms:
      * fr.acinq.secp256k1.NativeSecp256k1 keeps its FQCN.
      * SerializationFeature.values() / valueOf() present.
      * androidx.sqlite.driver.bundled.BundledSQLiteDriverKt
        retains native nativeThreadSafeMode() + nativeOpen().
      * Okio__OkioKt only exposes buffer(Source): BufferedSource
        and buffer(Sink): BufferedSink — no specialized
        buffer$<hash> bridge.
  - Bundled app launches and reaches VLC MediaPlayerFactory init
    with zero VerifyError / NoSuchMethodError / UnsatisfiedLinkError
    in the log.
2026-05-16 11:38:15 +10:00
m 33c97c3991 feat(desktop): wire Import Follow List dialog into UI
The ImportFollowListDialog composable was already implemented but never
rendered. The File menu item set a boolean state that no observer
consumed.

Render the dialog from MainContent inside the CompositionLocalProvider
that supplies LocalNamecoinService, so Namecoin (.bit, d/, id/)
identifier resolution works in addition to npub/hex/NIP-05.

Also add a left-side launcher in both layouts so the feature is
discoverable without using the File menu:
- single-pane: NavigationRailItem (PersonAdd icon, 'Import' label)
- deck: IconButton in the DeckSidebar next to 'Add Column'
2026-05-16 11:25:46 +10:00
m d1f037c678 feat(desktop): wire NamecoinSettingsSection into Settings screen
The desktop client already ships DesktopNamecoinPreferences, the
DesktopNamecoinNameService that consumes them, and the NamecoinSettingsSection
composable that's a port of the Android UI. The section just wasn't surfaced
in the desktop Settings screen.

This wires NamecoinSettingsSection into RelaySettingsScreen, between the Tor
section and the Developer / Relay sections. Preferences come from the
namecoinPreferences parameter that the deck container already passes in, and
fall back to LocalNamecoinPreferences when called from another caller.

User-visible behaviour: the .bit / d/ / id/ ElectrumX server settings (master
toggle, custom servers, defaults indicator, add/remove, reset) are now
reachable from desktop Settings and persist via java.util.prefs.Preferences.
2026-05-16 10:09:12 +10:00
m 31e73e3865 fix(desktop): add ProGuard keep rules for reflection-heavy deps
The v1.09.1 desktop release (DMG/MSI/DEB/RPM) fails to launch on every
platform with:

  Exception in thread "main" java.lang.ExceptionInInitializerError
      at com.fasterxml.jackson.databind.ObjectMapper.<init>(ObjectMapper.java:700)
      at com.vitorpamplona.amethyst.desktop.ui.deck.DeckState.<clinit>
  Caused by: java.lang.NullPointerException:
      Cannot read the array length because the return value of
      "java.lang.Class.getEnumConstants()" is null
      at com.fasterxml.jackson.databind.cfg.MapperConfig
              .collectFeatureDefaults(MapperConfig.java:107)
  Failed to launch JVM

Root cause: PR #2914 wired Compose 1.11.0's new ProGuard pass into the
release build but added only `-dontwarn` rules. ProGuard then optimized
Jackson's enum classes (`SerializationFeature`, `MapperFeature`, etc.) and
stripped their synthetic `values()` / `valueOf()` methods because nothing
in Amethyst's own bytecode calls them. Jackson does call them — but
reflectively, at `ObjectMapper` construction time, long after ProGuard
finished. The result is `Class.getEnumConstants()` returning null and the
JVM giving up.

The same regression silently broke VLC media playback (VLCj's
`ServiceLoader` lookup of `DiscoveryDirectoryProvider` impls printed
`ConfigDirConfigFileDiscoveryDirectoryProvider not found` because
ProGuard stripped both the META-INF/services file and the impl class).

Fix: add explicit `-keep` rules for the four reflection-heavy libraries
on the desktop classpath:

- Jackson (databind, core, annotations, module-kotlin): keep classes
  intact and keep `values()`/`valueOf()` on every Jackson enum.
- JNA (com.sun.jna.\*\*): used by VLCj and kmp-tor — its `Structure`
  field-order reflection requires field metadata to survive.
- VLCj (uk.co.caprica.vlcj.\*\*): `ServiceLoader`-based discovery providers
  and reflective binding classes throughout.
- SLF4J: detected by Jackson via `Class.forName`.

Verified by building `:desktopApp:createReleaseDistributable` and
launching the resulting bundle on macOS arm64:
  - No `ExceptionInInitializerError`
  - `VLC: bundled discovery succeeded`
  - `VLC: MediaPlayerFactory created successfully`

The same ProGuard pass runs on all four target formats (Dmg, Msi, Deb,
Rpm) so this single change fixes the regression on every desktop OS.
2026-05-16 09:13:56 +10:00
m 2c3d006f05 docs: rename NIP-9A -> NIP-9B in code comments + user strings
The upstream NIP draft for kind:34551 community rules
(nostr-protocol/nips#2331) was renumbered from 9A to 9B per maintainer
feedback that slot 9a is already claimed by the push-notifications
draft (nostr-protocol/nips#2194):

  https://github.com/nostr-protocol/nips/pull/2331#issuecomment-4442813289

This commit updates all human-readable references in the merged
community-rules code:

- 7 Kotlin files in quartz (CommunityRulesEvent, CommunityRulesValidator,
  5 tag classes) — Kdoc comments
- 13 Kotlin files in amethyst (composer, feed filter, rules editor,
  Account, AccountSettings, tests) — code comments + Kdoc
- 1 English strings file (values/strings.xml) — 2 user-facing strings
- 54 translation strings files (values-*-r*/strings.xml) — same two
  strings, untranslated "NIP-9A" token replaced with "NIP-9B"

Kind number (34551), schema, tag names, and behaviour are unchanged.
No public API or DTO field renames. Pure docs/strings.

Verified: :quartz:spotlessCheck, :amethyst:spotlessCheck,
:commons:spotlessCheck all clean.
2026-05-14 09:29:59 +10:00
m 3383b60315 feat(namecoin): distinguish malformed-JSON values from missing-field
When a Namecoin record's value isn't valid JSON (a real failure mode
when an operator hand-builds the value and miscounts braces), the
NIP-05 path used to silently swallow the parser exception and surface
a misleading "no nostr field" message. That sends the publisher
chasing a phantom missing field when the actual problem is the value
itself.

Concrete case that triggered this: a `name_update` published a
474-byte d/testls value with one closing brace short of balanced. The
string parses up to the missing brace, after which kotlinx.serialization
throws "Unfinished JSON term at EOF at line 1, column 474". That error
was previously dropped, leaving the operator to debug "no nostr field"
without ever seeing the underlying JSON parse failure.

Changes:

- New NamecoinResolveOutcome.MalformedRecord(name, error). Distinct
  from NoNostrField. The `error` field is the parser's own diagnostic
  (e.g. "Unfinished JSON term at EOF at line 1, column 474") so the
  publisher can locate the broken byte without spelunking.
- NamecoinNameResolver.performLookupDetailed: parse via a new
  parseValueOrError helper and surface MalformedRecord instead of
  collapsing into NoNostrField. Also rejects non-object top-level
  values (arrays, primitives, null) with a useful diagnostic
  ("top-level value is JsonArray, expected JSON object").
- DesktopSearchScreen handles the new outcome by surfacing the parser
  error verbatim in the Namecoin status banner, so the column number
  reaches the publisher's screen.

Tests (commonTest / NamecoinImportTest):

- "NIP-05 lookup surfaces MalformedRecord with parser detail when
  value is broken JSON": a deliberately one-brace-short value yields
  MalformedRecord with a non-empty diagnostic.
- "NIP-05 lookup surfaces MalformedRecord when top-level value is a
  JSON array": ensures non-object top-level values are rejected with
  a useful "expected JSON object" message rather than silently
  parsing as something unusable.

Tests don't pin the exact parser wording (kotlinx.serialization can
change it across versions); they only pin that the message is
attributed to JSON parsing rather than to a missing field.
2026-05-09 10:02:26 +10:00
m ca1f76f4bc feat(community): opt-in NIP-9A feed filter for community moderation
Adds a "Hide posts that violate community rules" toggle in Security & Filters
that drops events from community feeds when their author/kind/size fails the
latest cached `kind:34551` (NIP-9A) rules document for that community.
Default OFF preserves pre-9A behaviour. Closes #2761.

When the toggle is on, both `CommunityFeedFilter` (approval-only feed) and
`CommunityModerationFeedFilter` (un-approved feed) construct a per-feed
`CommunityRulesValidator` from the latest `kind:34551` for the community and
drop candidates whose `validate(...)` returns a violation. When no rules
event is cached for the community, the validator is null and the filter
behaves exactly as before — no false positives on pre-9A communities.

`CommunityRulesFilterSubAssembler` (new) joins the existing
`CommunityFilterAssembler.group`, reusing the `CommunityQueryState` keyspace
so any screen mounting the community feed subscription also pulls the rules
document. Filter: `kinds=[34551]`, `authors=<owner+moderators>`, `#a=<addressTag>`.

`CommunityRulesLookup.kt` extracts the cache scan + violation check into pure
helpers (`latestCommunityRules`, `violatesCommunityRules`) so the filter
wiring is unit-testable without LocalCache. The `Account.settings.hideCommunityRulesViolations`
flag follows the existing `useLocalBlossomCache` plumbing pattern: persisted
in `LocalPreferences`, mutated through a `change*` setter on `AccountSettings`,
read into the feed view models at construction time.

Out of scope (deferred to follow-up issues): web-of-trust gates, per-day
quota enforcement (`postsTodayByKind`), stale-rules ratchet UI, "Send anyway"
override on validation. The validator skips wot/quota cleanly when their
callbacks are null, so the feed filter is a strict subset of NIP-9A's
checks for v1.

7 unit tests on `violatesCommunityRules`:
- comment passes when its kind is whitelisted
- text note fails when only comments are whitelisted
- denied author overrides any kind allow-list
- oversize per-kind, under-size per-kind, global max-event-size
- note without an event is treated as passing

Builds clean: `:amethyst:assemblePlayDebug`, `:amethyst:spotlessCheck`,
`:amethyst:testPlayDebugUnitTest --tests "*CommunityRulesLookupTest*"`.

Note: this PR also lands a copy of `CommunityRulesFilterSubAssembler`
identical to the one in PR #2798 (composer-side validation). When either
PR merges first, the other rebases to drop the duplicate; the file is the
same in both.
2026-05-09 09:26:29 +10:00
m f2bd58ce90 feat(community): structured NIP-9A rules editor in new-community flow
Adds an opt-in editor for NIP-9A `kind:34551` community rules alongside the
existing freeform `rules` text on `kind:34550`. Closes #2760.

Editor sections in `NewCommunityScreen` (rendered after relays):

- Allowed event kinds: filter chips for common community kinds (1, 20, 21,
  22, 1111, 30023) plus a custom-kind input. Per-kind limits dialog (long
  press / edit icon) for `max-bytes` and `max-per-author-per-day`.
- Banned users: pubkey list with `deny` policy, picked through the same
  user-suggestion field the moderator picker uses.
- Web-of-trust gate (optional): root npub-or-hex pubkey + depth, repeatable.
- Global max event size (optional): single bytes input.

`NewCommunityModel` carries the four collections/values as Compose state and
exposes a pure-helper `buildRulesPayload(...)` companion so the mapping from
editor drafts to Quartz tag types is unit-testable without an Account.

`Account.sendCommunityRules(...)` mirrors `sendCommunityDefinition(...)`:
builds a `CommunityRulesEvent` via the Quartz builder, signs with the same
key, and broadcasts on the same outbox path. Reuses the community's `dTag`
so the rules event replaces in place across edits.

`NewCommunityModel.publish(...)` only emits the rules event when at least
one structured rule is present (`hasStructuredRules()`), so existing
communities and form runs that don't touch the new section continue to
behave exactly as before.

Migrating existing communities, the `min_rules_created_at` ratchet UI, and
edit-flow preload of structured rules from `kind:34551` are deliberately
out of scope here (see issue #2760).

7 unit tests on the pure helper:
- empty editor produces a null payload (no event published)
- max-event-size only is enough to publish
- per-kind limits round-trip into the tag
- bare kind rule serialises to ["k", "<kind>"] (no empty fields)
- banned pubkey writes a deny rule
- WoT gate carries root pubkey + depth
- pubkey-input parser accepts hex and npub, rejects garbage

Builds clean: `:amethyst:assemblePlayDebug`, `:amethyst:spotlessCheck`,
`:amethyst:testPlayDebugUnitTest --tests "*NewCommunityModelRulesTest*"`.
2026-05-09 09:11:24 +10:00
m cb0de81f1a feat(community): validate posts against NIP-9A community rules in composer
When the comment composer is replying into a NIP-72 community
(`replyingTo.event is CommunityDefinitionEvent`), subscribe to the
community's latest `kind:34551` rules document, run
`CommunityRulesValidator.validate(...)` on every draft change, render an
inline banner above the bottom action row when the draft would be
rejected, and disable the post button until the violation is resolved.

- `CommunityRulesFilterSubAssembler` is added to the existing
  `CommunityFilterAssembler.group` so any screen that already mounts
  `CommunityFilterAssemblerSubscription` (community feed, this composer)
  also pulls the latest rules event from the community's relays. Filter
  is `kinds=[34551]`, `authors=<owner+moderators>`, `#a=<addressTag>`,
  matching the NIP-72 trust model.
- `CommentPostViewModel` observes `kind:34551` via
  `LocalCache.observeEvents` keyed on the reply target, picks the latest
  `created_at` matching the community address, and re-runs the validator
  on every draft change. The validator's `postsTodayByKind` and `wot`
  callbacks are intentionally null for this PR (per-day quota lookups
  and NIP-02 follow-graph traversal are deferred to follow-ups; the
  validator skips those checks cleanly).
- Draft size is conservatively estimated from `content.toByteArray(UTF-8)`
  — tags add bytes, so this can under-count on the boundary, but relays
  still enforce the real cap. Good enough for a pre-send preview.
- `CommunityRulesViolationBanner` renders the first
  `CommunityRulesValidator.Violation` with a localized message; new
  strings cover all 7 sealed-violation variants.
- `CommentPostViewModelTest` covers valid drafts, kind-not-allowed,
  oversize, denied-author, the under-size boundary, and multibyte UTF-8
  size accounting.

Compose state — not StateFlow — backs `validationResult` and
`communityRules` so `canPost()` and the banner recompose without an
explicit `collectAsState` site at the top bar (`isActive` reads
`validationResult` directly).

Refs nostr-protocol/nips#2331
Closes #2759
2026-05-09 08:51:28 +10:00
m 594e1fb98b feat(notes): show stale-relay hint on replaceable events using NIP-66 cache
Surface a soft "this content's relay may be stale" UX cue on addressable
replaceable events (kind:30xxx) when every delivering relay's most recent
NIP-66 kind:30166 Relay Discovery monitor report cached locally is older
than 14 days (or has never been observed).

Read-only: uses only what's already in LocalCache, populated by the
existing RelayInfoNip66FilterSubAssembler. No new network fetches.

Implementation:
- New `StaleRelayHint` composable in `ui/note/elements/`, hooked into
  `NoteBody` after the zap-splits row. Skips quietly for non-addressable
  events, empty relay sets, or any relay still observed within 14 days.
- Pure `isStaleByLatestMonitorReports(latestPerRelay, now, threshold)`
  predicate — `null` (never monitored) and "older than cutoff" both count
  as stale, but a single fresh relay short-circuits the hint to off.
- Reactively tracks the note's relay set via
  `baseNote.flow().relays.stateFlow` so newly-observed relays update the
  hint without recomposition tricks.
- Latest monitor `created_at` per relay is read from `LocalCache` with
  the same `Filter(kinds=[30166], #d=[relay.url], limit=1)` shape used
  on the Relay Information screen.

Out of scope (per issue):
- Auto-refreshing or hiding stale content.
- Heuristics beyond the "all delivering relays stale" check.

Tests: 9 unit tests for the pure predicate covering empty/single/mixed
sets, null-handling, exact-cutoff boundary, and a custom threshold.

Closes #2762.
2026-05-09 08:01:29 +10:00
m 9d84d01569 style: spotlessApply formatting + fix icon imports for upstream MaterialSymbols 2026-05-08 22:38:39 +10:00
m 23a7ac4770 fix(namecoin): accept single-identity {nostr:{pubkey,relays}} on d/ names
ifa-0001 doesn't mandate that domain records use the nostr.names
sub-dictionary. Operators who own a name outright commonly publish:

    {"nostr": {"pubkey": "<hex>", "relays": ["wss://..."]}}

(the same shape id/ records use). Before this fix d/-namespace branch
only accepted {"nostr":"<hex>"} or {"nostr":{"names":{...}}},
so a record like d/mstrofnone with the single-identity object form
silently failed with NoNostrField even though id/mstrofnone resolved
fine.

Resolution rules:
  1. nostr.names wins for any sub-identity.
  2. Root lookups fall back to bare pubkey when names["_"] is absent.
  3. Non-root lookups against names-only or single-identity records
     do NOT silently use the bare pubkey.
2026-05-08 22:33:06 +10:00
m 5efead42fb fix(quartz/electrumx): parse NAME_FIRSTUPDATE outputs alongside NAME_UPDATE
Names whose latest on-chain transaction is still the initial registration
(OP_NAME_FIRSTUPDATE = OP_2 = 0x52) were silently dropped because the
parser only matched OP_NAME_UPDATE (OP_3 = 0x53). The scripthash index
returns the FIRSTUPDATE tx in that case, so resolution looked
'unreachable' even though every server answered.

Accept both opcodes when scanning vouts and when parsing the script.
FIRSTUPDATE pushes <name> <rand> <value>, so skip the extra <rand> push
before reading the value.
2026-05-08 22:33:06 +10:00
m 4ab9cae213 fix(desktop): provide LocalNamecoin{Preferences,Service} so search can resolve
The lazy-init declarations and CompositionLocalProvider entries were lost
during the rebase, so SearchScreen always saw null services and skipped
Namecoin resolution. Add them back inside the LoggedIn branch alongside
LocalTorState.
2026-05-08 22:33:06 +10:00
m 50d0e15fe2 fix(desktop): show Namecoin lookup results in search
Re-add the Namecoin results UI block that was lost during the rebase.
Renders Loading/Resolved/NotFound/Error states above bech32/people/note
results when the query is a Namecoin identifier (.bit, d/, id/).
2026-05-08 22:31:46 +10:00
m 01f6c1c24b fix(desktop): make the single-pane navigation rail scrollable
The left navigation rail in single-pane mode renders a fixed list of
pinned screens (Home, Reads, Notifications, ...) plus a 'More' launcher
and a stack of bottom controls (relay health, bunker heartbeat, tor
status, account switcher).

Material3 `NavigationRail` lays its children out in a non-scrollable
`Column`. When the window is short — either because the OS window is
small or the user pinned several screens — the bottom items in the
list (and the 'More' button) get clipped and become unreachable.

Replace the `NavigationRail` with a `Column` that mirrors the rail's
container styling and splits content into two regions:

- A scrollable region (weight(1f) + verticalScroll) holding the pinned
  screens and the 'More' launcher. Overflow now scrolls instead of
  clipping.
- A fixed bottom region holding the relay health indicator, bunker
  heartbeat, tor status indicator, and the account switcher. These
  remain anchored at the bottom of the rail.

Item visuals are preserved by keeping `NavigationRailItem` for the
items themselves, with `NavigationRailItemDefaults.colors()`.

No behavior change when the rail content already fits the window.
2026-05-08 22:28:17 +10:00
m 3e246e9e0b fix(desktop): make error messages selectable so users can copy them
Several user-visible error messages on Desktop are rendered with plain
`Text` composables, which means they can't be selected or copied. That
makes it awkward to share an error in a bug report or paste a hex error
string into a search.

Wrap the error text in `SelectionContainer` at the canonical sites:

- `commons.ui.components.LoadingState`: wrap the description in
  `EmptyState` and the message in `ErrorState`. `EmptyState` is reused
  as the in-feed error renderer (e.g. 'Error loading feed' with the
  underlying error in `description`), so this covers feed/loading
  errors across screens that use these helpers.
- `ComposeNoteDialog`: wrap the validation error and the upload error
  in the compose-note dialog.
- `auth/LoginCard` (Nostr Connect): wrap the connection error.
- `auth/KeyInputField`: wrap the supporting-text error so the inline
  message under the nsec input field can be copied.

No visual changes \u2014 `SelectionContainer` does not affect layout or
styling. Selection works on Compose Desktop (mouse drag) and on Android
(long-press) without further changes.
2026-05-08 16:32:48 +10:00