mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
ff2de15b20a076ac80eec94de7e4ba2cbdda8092
27
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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/').
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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)
|
||
|
|
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.
|
||
|
|
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' |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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*"`. |
||
|
|
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 |
||
|
|
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. |
||
|
|
9d84d01569 | style: spotlessApply formatting + fix icon imports for upstream MaterialSymbols | ||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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/). |
||
|
|
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. |
||
|
|
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. |