LocalPreferences.setDefaultAccount called setCurrentAccount before
saveToEncryptedStorage. setCurrentAccount emits the new list onto the
savedAccounts MutableStateFlow, which AlwaysOnNotificationServiceManager
collects and reacts to by calling loadAccountConfigFromEncryptedStorage
for every saved account — including the just-added one. That call hit
encryptedPreferences(newNpub) before NOSTR_PUBKEY had been written, got
null, and cached the null in cachedAccounts.
cachedAccounts is a process-lifetime map, so the poisoned entry survived
the eventual disk write. Every subsequent switchUser to that account
took the cached null path, fell through to requestLoginUI(), and AccountScreen
rendered LoggedOffSetup — the onboarding screen with TOS unchecked, asking
the user to re-do the Amber handshake.
Write the per-npub file first, then seed the cache with the in-memory
AccountSettings, then publish onto the savedAccounts flow. Also stop
caching null returns in loadAccountConfigFromEncryptedStorage so any
future racy reader can't poison the cache either.
Test referenced formatSats, DEFAULT_ZAP_AMOUNTS, and ZapType which
were removed/made private in upstream merge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite SendDialog with sealed state machine that auto-detects input
type (BOLT11, LNURL bech32, lightning address). For LNURL/address:
resolves endpoint, shows amount form with min/max hint, optional
comment field, fetches invoice, then pays via NWC. Strips lightning:
URI prefix. Inline copiable errors with retry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SendDialog: switch to Dialog+Card with X close, inline copiable error
messages, button resets to "Pay Invoice" on error for retry.
LightningAddressResolver: return error body from callback responses so
server error messages (e.g. "Recipient wallet error") surface to user
instead of generic "Failed to fetch invoice". Also check "message"
field in addition to "reason" for error extraction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace AlertDialog with Dialog+Card pattern. Invoice created state now
shows centered amount, description, 240dp QR code, and full-width
"Copy Invoice" button. Close via top-right X button. Input form gets
full-width "Create Invoice" button.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add LaunchedEffect that rescans cache when followedUsers populates after
startup, fixing empty feed when contact list arrives after initial scan.
Remove diagnostic println from NwcPaymentHandler.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LoginScreen's fire-and-forget save coroutine used rememberCoroutineScope
which got cancelled when the composable left composition after login.
Move saveCurrentAccount() to onLoginSuccess in Main.kt which uses the
app-level scope that survives recomposition. Fixes both nsec login and
generate-new-account flows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On resizing CDNs the imeta `x` (post-resize hash) can differ from the
`ox` (original hash) embedded in the URL. The bridge previously preferred
`explicitHash` over the URL's sha for "authoritative casing", but the
upstream file on `xs` is named after the URL's sha, not the imeta hash.
For URLs like https://image.nostr.build/<ox>.png with imeta x=<post-resize>
the cache would request /<x>.png and 404 on miss.
Always use the sha parsed from the URL path; drop the explicitHash
parameter. `extractSha256FromUrlPath` already lowercases, so the casing
concern is moot.
- Add kind 1 (replies) to interaction subscriptions
- Key count reads on FlowSet state for reactive updates
- Wire Quote menu item to ComposeNoteDialog with q-tag support
- Add BoostsPopup on long-press repost icon (who boosted)
- ComposeNoteDialog now accepts quoteOf param with nostr: URI pre-fill
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove premature ensureRelayConnected check — NostrClient connects
on subscribe/publish via sendOrConnectAndSync
- Fix disconnect crash: use appScope instead of rememberCoroutineScope
to survive recomposition when nwcConnection goes null
- Surface balance errors/timeouts as snackbars instead of silent swallow
- Add ensureRelayConnected helper to RelayConnectionManager
- Add Phase 2 embedded wallet research doc
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a kind-23195 event arrives signed by someone other than the wallet
service we sent the request to, we now count it on the pending entry and
leave the entry in place so the legitimate reply can still resolve. But
if no legitimate reply arrives and the 30s timeout fires, the user used
to see a generic "Wallet request timed out" — indistinguishable from
"the wallet is just slow", even when an active attacker was forging
replies and dropping the real ones.
Carry the per-request spoof count through to the timeout error message:
- NwcPaymentTracker.PendingRequest gains an AtomicInteger spoofAttempts.
onResponseReceived increments it on WrongAuthor.
- New tracker method spoofAttemptsFor(requestId) reads the count.
- Account exposes nwcSpoofAttempts() and cleanupNwcRequest() so the
UI doesn't need to reach into LocalCache.
- Account.sendNwcRequestToWallet now returns the request event id so
callers can identify the pending entry.
- WalletViewModel.launchTimeout takes a () -> HexKey? provider and
fetches the spoof count when the timeout fires. The error becomes
"Wallet request timed out — N replies were rejected because they
were signed by an unexpected key. Your relay may be untrusted."
Also calls cleanupNwcRequest on timeout to avoid leaking the entry.
Silent on the happy path: a forged reply followed by the real one does
not trigger any user-facing message — the spoof count is discarded with
the matched entry.
Replace the placeholder "N assets bundled" line with a real list of
compact rows for each `e`-tagged Software Asset in the release.
Each row loads the referenced asset event id through
`LoadAssetNote` (uses LocalCache first, falls back to
`checkGetOrCreateNote` for ids never seen) and then
`observeNoteEvent<SoftwareAssetEvent>` — which both observes the
LocalCache flow and registers the note with `EventFinder` so the
relay round-trips the missing asset event. When the asset arrives
the row recomposes with MIME, version, optional variant, size,
platform chips, and a Download link to the asset url.
The standalone `RenderSoftwareAsset` card (kind 3063 in a feed or
thread) is unchanged; this only fills out the release detail view.
filterMissingChannelsById had an inverted isEmpty() check that emitted
zero filters, so kind 40 was never requested from any relay. Channels
discovered from kind 42 messages stayed as empty stubs unless the
creator also happened to publish a kind 41 metadata update findable on
the same relay — which is why most cards in the Public Chats feed
loaded with no name or picture.
Fix:
- Drop the inverted condition; mapOfSet guarantees non-empty values, so
emit a RelayBasedFilter for every (relay, channelIds) entry.
- Widen the relay set per channel to include the user's search and
indexer relay lists. Falls back to DefaultSearchRelayList /
DefaultIndexerRelayList when those lists are empty.
- Plumb the Account through ChannelFinderQueryState so the assembler
can read the search/indexer flows. Mirrors EventFinderQueryState.
The previous commit dropped `authors` and `#p` from the relay subscription
filter to match Primal's interop shape. Without those, the relay will
deliver any signed kind-23195 event that carries our request id in `#e`,
so an attacker who can observe the request on the relay could forge a
"response" with their own keypair: Amethyst would happily derive a shared
secret from `event.pubKey` (the attacker), decrypt the payload, and
display attacker-controlled balance/transaction data. Even worse,
`paymentTracker.onResponseReceived` removed the pending entry on first
match — so the legitimate wallet reply that followed was silently dropped.
Move the author check from the relay layer into NwcPaymentTracker:
- `registerRequest` now requires the expected wallet-service pubkey
(read from the request's `p` tag). LocalCache extracts it during
`consume(LnZapPaymentRequestEvent)` and refuses to register if the
request has no `p` tag.
- `onResponseReceived` takes the response author and returns a sealed
MatchResult of NoMatch / WrongAuthor / Matched. A WrongAuthor result
leaves the pending entry in the map so the legitimate response can
still resolve it.
- Android LocalCache and DesktopLocalCache both adopt the new API and
log a warning on suspected spoof attempts.
End-to-end the response is still encrypted under the per-connection shared
secret, so this is a second layer of defence rather than the only one,
but matching the author keeps a forged kind-23195 from consuming the
pending slot and DoSing the legitimate reply.
Wire NIP-82 Software Applications (kind 32267), Releases (kind 30063)
and Assets (kind 3063) into the Quartz event model and surface them
through a dedicated rendering path in Amethyst.
Quartz: extend the existing experimental NIP-82 builders with topic
(`t`) and NIP-34 app-link (`a`) helpers, and add a small detector
(`isNip82SoftwareRelease`/`asSoftwareRelease`) so kind 30063 events
can be disambiguated from NIP-51 ReleaseArtifactSetEvent at the
renderer layer. Pin behavior with unit tests covering build paths,
disambiguation, and the real-world Amethyst NIP-82 description event.
Amethyst: add modern card visualizations for each kind — application
header with icon/screenshots/platforms/topics/links, release header
with channel pill and bundled-asset count, and asset row with MIME,
size and platforms — and dispatch to them from both `NoteCompose`
and `NoteMaster` (`ThreadFeedView`).
A new "Apps" feed (left nav drawer) mirrors the Picture Feeds shape:
`SoftwareAppsFeedFilter` reads kind 32267 from `LocalCache`, a
`PerUserEoseManager`-backed subscription pulls applications and
releases from outbox relays, and a dedicated screen renders them in
a `LazyColumn` of `RenderSoftwareApplication` cards.
When the first item of either the pinned or unpinned block changes,
animate-scroll back to index 0 if the user was at or near the very top.
This mirrors the pattern from ChatFeedView so a new chat bubbling up
doesn't leave the user one row below it.
- Pin glyph moved from top-right (overlapping LikeReaction/ZapReaction)
to top-left, inside a surface-tinted circular badge that overlays the
cover image corner. Reads against any cover image and frees the
reaction buttons from being eclipsed and hit-tested through.
- 8dp Spacer item inserted between the pinned items{} and unpinned
itemsIndexed{} blocks, only when both sides have content, so the
section boundary reads as a section break instead of just another row.
Some wallet services and relays don't produce or index the `p` tag on NIP-47
response events the way the spec implies for ephemeral kinds, which made
Amethyst's strict relay-side filter (kinds + authors + #e + #p) match
nothing while looser clients (Primal uses just kinds + #e) work against the
same connection string.
Reduce the relay filter to the same shape Primal uses. The request event id
in #e is a unique 32-byte identifier, so the false-positive rate is
effectively zero, and the wallet's identity is still authenticated end-to-end
by NIP-04 decryption against the per-connection shared secret — the relay
filter was never the security boundary.
NWCPaymentQueryState no longer needs `fromServiceHex` or `toUserHex`; remove
them and propagate the simpler ctor through callers.
Overlay a small push-pin glyph at the top-right of pinned channel cards
so the user can tell at a glance why those rows are at the top. The
underlying ChannelCardCompose is untouched; only the row wrapper changes
to a Box to host the overlay.
Two problems caused wallet timeouts that affected only Amethyst users:
1. The NWC subscription went through a 500 ms BundledUpdate debounce while
the request event was published immediately. Kind 23195 responses are
ephemeral, so if the wallet replied faster than the REQ reached the
relay, the reply was dropped with no replay. Add a synchronous
subscribeAndFlush() that bypasses the bundler so the REQ is queued on
the WebSocket before the EVENT.
2. Three failure paths were swallowed: decryption returning null, an
unknown response subtype, and a response arriving with no matching
pending request. Users saw "Wallet request timed out" with no clue.
Surface a specific error in WalletViewModel for the first two, and
log a warning in LocalCache for the third.
Native-speaker review surfaced consistency and wording fixes:
- Calendar collection naming: use "kalender" for action labels
(New / Edit / Delete) matching the English source's user-facing
simplification; use "kalenderlista" only in the empty-state body
and route header (matches route_calendar_collections =
"Kalenderlistor" and English's "Calendar lists"). Same hybrid as
Czech, pt-BR, and German.
- Capitalize "Nostr" in calendar_share_nostr.
- "on-chain-backend" over "chain-backend" in wallet_onchain_no_backend
(rest of batch already uses the on-chain- compound).
- "blockchainen" (definite form, matches existing ots_info_description)
over bare "blockchain" in wallet_onchain_public_dialog_body.
- Tighten Lightning comparison: "större än för Lightning" over
"större än Lightning" (the bare comparison was elliptical).
- wallet_onchain_public_dialog_body: clearer privacy wording
("fyll på från icke-privata konton och skicka tillbaka medel dit"
over "fyll på och töm från och till"); make antecedent of "spendera"
explicit ("spendera medlen" over ambiguous "spendera dem").
- Symmetric RSVP pair: "Kommer" / "Kanske" / "Kommer inte"
(instead of mixed "Kommer" / "Kanske" / "Kan inte"). Matches the
same fix applied for German and pt-BR.
Note: calendar_rsvp_section / calendar_rsvp_none already use the
established Swedish "OSA" acronym (matching kind_appt_rsvp =
"OSA för möte"), so no rename needed unlike de/pt-BR where the
section noun ("Zusagen"/"Confirmações") had to be replaced.
Native-speaker review surfaced consistency and wording fixes:
- Calendar collection naming: use "Kalender" for action labels
(New / Edit / Delete) matching the English source's user-facing
simplification; use "Kalenderliste" only in the empty-state body
and route header (matches route_calendar_collections =
"Kalenderlisten" and English's "Calendar lists"). Same hybrid as
Czech and pt-BR.
- On-Chain hyphenation: "On-Chain-Backend" over "Chain-Backend"
in wallet_onchain_no_backend; "Stattdessen On-Chain senden" over
"Stattdessen on-chain senden" in send_onchain_instead. Matches the
rest of the batch's German compound-noun convention.
- "Nowhere Event" over "Nowhere Veranstaltung" — aligns with the
English source's all-English Nowhere brand prefixes (Drop / Forum /
Petition were already English).
- "RSVPs" / "Antworten" over "Zusagen" for calendar_rsvp_section and
calendar_rsvp_none — section also includes "Vielleicht" and "Komme
nicht", so "Zusagen" was misleading. Aligns with the existing
"RSVP für Termin" usage in kind_appt_rsvp.
- Symmetric RSVP pair: "Komme" / "Vielleicht" / "Komme nicht"
(instead of mixed "Komme" / "Vielleicht" / "Kann nicht").
- wallet_onchain_public_dialog_body: clearer privacy wording
("lade auf und sende zurück" over "befülle und entleere von und
zu"); make antecedent of "ausgeben" explicit ("die Mittel
ausgeben" over ambiguous "es").
- contact_list_containing_label: rephrase to dodge German declension
for n=1 ("Folgenliste (%1$d Nutzer):" over "Folgenliste mit %1$d
Nutzern:" which is dative-plural-only).
Native-speaker review surfaced consistency and wording fixes:
- Capitalize "Nostr" in calendar_share_nostr.
- "no blockchain" (masculine, matches ots_info_description) over
"na blockchain" in wallet_onchain_public_dialog_body.
- Calendar collection naming: use "calendário" for action labels
(New / Edit / Delete) matching the English source's user-facing
simplification; use "lista de calendário" only in the empty-state
body and route header (matches route_calendar_collections = "Listas
de calendário" and English's "Calendar lists"). Same hybrid we
resolved for Czech.
- Symmetric RSVP pair: "Vou" / "Talvez" / "Não vou" (instead of mixed
"Vou" / "Talvez" / "Não posso").
- "RSVPs" over "Confirmações" for calendar_rsvp_section and
calendar_rsvp_none — the section also includes "Talvez" / "Não vou"
responses, so "Confirmações" was misleading. Aligns with the
existing "RSVP do Compromisso" usage in kind_appt_rsvp.
- wallet_onchain_public_dialog_body: clearer privacy wording
("abasteça a partir de contas não privadas e envie os fundos de
volta" over "abasteça e esvazie a partir de e para"); make antecedent
of "gastar" explicit ("gastar os fundos" over ambiguous "-lo").
- calendar_empty_feed: reorder to drop trailing "ainda" — "Ainda não
há eventos…" reads more naturally than "…ainda" at end.
- calendar_collection_delete_confirm_message: align tenses (future +
future "será removido / não serão excluídos" over future + present).
- "backend on-chain" over "backend de chain" in wallet_onchain_no_backend.
Note: contact_list_* strings use the semantically-correct "seguidos"
(people you follow, per kind:3), even though existing
follow_list_selection / follow_sets use the inverted "seguidores".
Recommended follow-up: clean up legacy strings to match.
Native-speaker review surfaced several consistency and wording fixes
in the Czech translations from this branch:
- Use "Kanál" for "feed" (matches existing feed/home_feed/global_feed).
- Use "sats" not "satoshi" (matches new_amount_in_sats / amount_in_sats).
- Match Lightning sibling string's "odstraníte" (vs "odeberete") on
quick_zap_amounts_onchain_explainer.
- Capitalize "Nostr" in calendar_share_nostr.
- Calendar collection naming: use "kalendář" for actions (New / Edit /
Delete) matching the English source's user-facing simplification;
keep the disambiguating "seznamy" wording for the empty-state body
and title (matches route_calendar_collections = "Seznamy kalendářů"
and English's "No calendar collections yet").
- Rephrase contact_list_containing_label to dodge Czech count
inflection ("Seznam sledování (%1$d):" rather than "obsahující %1$d
uživatelů" which is only correct for n ≥ 5).
- Wallet privacy dialog: "vkládejte do peněženky a vybírejte z ní"
(deposit/withdraw verbs) over "plňte a vybírejte"; "váš nsec" (masc.
inanimate) over "vaše nsec"; "nesoukromých účtů" over "neprivátních".
- "on-chain backend" over "chain backend" in wallet_onchain_no_backend.
clearNwcConnection and setNwcConnection now require npub param and are
suspend functions. ConnectWalletDialog validates URI prefix inline
before firing the async connect callback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Map Desktop ZapType (Public/Private/Anonymous) through to
LnZapEvent.ZapType and pass to ZapAction.fetchZapInvoice
- Add relayHint + authorRelayHint params to NoteActionsRow;
FeedScreen now passes Note.relayHintUrl() for reactions/reposts
- Replace metadata preload TODO with design rationale comment
- Add 70-test manual testing sheet for wallet & zapping features
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Send, Receive, and Connect are now AlertDialogs instead of full-screen
sub-pages. The wallet home content is centered with widthIn(max=360.dp)
for a polished desktop look. Dialogs are the native desktop pattern
(consistent with zap dialog, bookmark dialog, tor settings dialog).
- ConnectWalletDialog: NWC URI input with paste + validation
- SendDialog: BOLT11 invoice input with paste + progress
- ReceiveDialog: amount/description input, transitions to show
generated invoice with copy button
- Home content: centered balance card + action buttons + connection info
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getBalance and makeInvoice were publishing the request event before
subscribing for the response. Fast wallet responses (like get_balance)
would arrive before the subscription was active, causing timeouts.
Fix: subscribe first via onSubscribed callback in waitForGenericResponse,
then publish. The original payInvoice wasn't affected because Lightning
routing takes long enough for the subscription to be ready.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wire AccountManager.setNwcConnection() and clearNwcConnection() into
wallet column for persistent connect/disconnect
- Implement NwcPaymentHandler.getBalance() via NIP-47 get_balance RPC
- Implement NwcPaymentHandler.makeInvoice() via NIP-47 make_invoice RPC
- Add generic waitForGenericResponse() helper for NWC RPC operations
- Auto-fetch balance on wallet column load via LaunchedEffect
- Wire receive screen to generate real invoices via NWC
- All wallet column features now functional (no blocking TODOs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>