1
0
forked from GRIN/grim
Commit Graph

1222 Commits

Author SHA1 Message Date
2ro dac3cf3faa build: sync grin node + wallet submodules to GRIM upstream
Adopt GRIM's restructure (upstream 20db758, 2ebc8ba, 3e4a3db): the
separate `node` submodule is gone; the grin node crates now live inside
the wallet submodule at wallet/grin (a nested submodule pinned to
ardocrat/node grim tip). Pins now match upstream GRIM master exactly:

  wallet = 32e132a45470d01a105fcab263e4970bc8eaff40 (ardocrat/wallet grim)
  node   = 7ae52bc6991da00443b40ce96f1afbe675023f6d (nested wallet/grin)

Changes:
- .gitmodules: drop the node submodule; wallet now carries nested grin.
- Cargo.toml: grin_* paths node/* -> wallet/grin/*; uuid 0.8.2 -> 1.23.4
  (the new wallet API exposes uuid 1.x in its public signatures, e.g.
  retrieve_payment_proof/slate ids -- forced bump, matches upstream).
- src/wallet/wallet.rs + src/node/node.rs: adapt to new upstream APIs --
  HTTPNodeClient::new/new_proxy now take a request timeout; Server/ApiServer
  api_chan is a tokio mpsc channel instead of a leaked futures oneshot.

Seed-at-rest is unchanged across the wallet bump: impls/lifecycle/seed.rs
is byte-identical (PBKDF2-HMAC-SHA512, 100 iters) and the node keychain
mnemonic derivation is byte-identical, so existing wallets/funds are safe.

Goblin's additive layers are untouched: money path runs over nostr (no
Tor send/pay/finalize tasks), the custom Tor engine stays on arti 0.43,
and the soft-lock work is preserved.

cargo check clean (1 pre-existing crate::node::Node warning); lib tests
280 pass / 1 ignored, i18n_keys 2 pass -- unchanged from baseline.

(cherry picked from commit eb37e66a990b537ad65db2b3438a467e1100d7ef)
2026-07-15 23:14:26 -04:00
2ro ca6ff965ab stratum: do not panic on error
Cherry-pick of GRIM upstream 3434936 (src/node/stratum.rs identical
to goblin's base, applied cleanly). Replaces panicking unwrap/expect
paths in the stratum server with graceful error handling.

(cherry picked from commit 4d928ce06fb985e814f3188661dfe544dfb1601b)
2026-07-15 23:14:26 -04:00
2ro 16602be964 goblin: bounce an approved pay request that races the wallet lock
WalletTask::NostrPayRequest ran nostr_pay/nostr_receive with no lock check,
so an Approve tapped just before locking could spend or receive seconds
later while the wallet was locked. Guard the task entry: when locked, the
request is left Pending in the store (still approvable after unlock) and
fail_send() flips the send phase to FAILED, which the activity card already
consumes — the spinner clears and the reason line explains the bounce.

Adds goblin.lock.approve_locked to all 10 locales.

(cherry picked from commit ee47343259f12314932976c193739a2e0e96521d)
2026-07-15 23:14:26 -04:00
2ro 6b7aaf5d4c goblin: lock wallet keeps relays alive behind an unlock screen
The Lock wallet button now calls wallet.lock() instead of wallet.close(),
so the wallet stays open and its nostr relays stay connected (payments keep
arriving and queue up) while the money seed is sealed. A full-surface unlock
screen replaces the whole wallet UI whenever is_locked() is set: it verifies
the typed password against the live nostr identity (no re-open) and calls
unlock() on success, or fully closes the wallet as a fallback. Because the
screen is an early return from GoblinWalletView::ui it covers every tab,
overlay, modal and pending deep link, and system/Android back is swallowed
while locked, so no in-app money action is reachable. lock() also stops the
Foreign API receive listener (its /v2/foreign receive_tx is a seed op); the
sync loop restarts it on unlock, guarded by !is_locked().

Adds goblin.lock.{title,subtitle,unlock,close_wallet} to all 10 locales.

(cherry picked from commit ee27be3020592d9c1228df9d0f23451920ca5035)
2026-07-15 23:14:26 -04:00
2ro b7ad5d669c fix(nostr): gate incoming-slate money ops behind a wallet lock
Add a money-path lock to Wallet that keeps the wallet open and the nostr
service's relay connections live (payments keep arriving) while sealing the
money seed from automatic use. When locked, the gift-wrap ingest defers every
seed-touching decision (AutoReceive / FinalizePost): the raw wrap is buffered
on the service and deliberately NOT marked processed, so relay catch-up still
backstops it across a restart. Surface/request/drop still run, so a payment
lands as a visibly pending card. On unlock the service loop drains the buffer,
replaying each wrap through the normal, now-unlocked ingest so payments received
while locked complete normally (receive + S2 reply, or finalize+post); the
replay is idempotent via the existing dedup + decide() guards.

- Wallet: locked flag with is_locked/set_locked/lock/unlock; cleared on
  open() and close().
- ingest: pure defer_while_locked/decision_touches_seed gate + unit test.
- NostrService: bounded, id-deduped deferred_wraps buffer + drain.

(cherry picked from commit 133d0248dc7cfe3f8707ee7fa0a66b748bcd205c)
2026-07-15 23:13:05 -04:00
2ro 4273e1c5a6 goblin: drop loading skeletons and normalize Settings typography (Build 151)
(cherry picked from commit bc27cb312c)
2026-07-15 23:13:05 -04:00
2ro 52ca12d0c0 docs: README truth sweep for builds 149-153 (identities, sign-in, Authorize, Authorize Sessions, merchant invoicing)
(cherry picked from commit 6e969bf5ee)
2026-07-15 23:12:32 -04:00
2ro b7757172b7 feat(privacy): give the VPN nudge a real highlighter-marker background
The 'we recommend using at least a VPN' recommendation on the Network-privacy
screen now sits on a yellow highlighter-marker background instead of just
semibold brighter text. Uses RichText::background_color so each wrapped row is
highlighted tight to the glyphs like a real marker stroke.

Theme-aware, dark ink in every theme for legibility (>=9:1):
  - Light  -> softer warm yellow #FFE45C (marker on near-white page)
  - Dark   -> brand yellow #FFD60A (full presence on near-black page)
  - Yellow -> deeper amber #F0A800 (distinct marker on the lemon page bg)

Shared network_privacy_panels covers both the Settings and onboarding
Network-privacy surfaces. Styling only; no locale strings changed.
2026-07-13 01:50:00 -04:00
2ro 2b4cb9dd73 fix(nostr): neutralize bidi/zero-width spoofing in displayed untrusted text
Untrusted strings rendered in the payment/UI strip stripped only ASCII
control chars, not Unicode bidi overrides/isolates or zero-width format
chars. An attacker could embed a right-to-left override or invisible
codepoint in a name, memo, or subject to visually reorder or hide the
displayed identity/amount (Trojan-Source style spoof). Funds were never
at risk — they always go to the correct npub — but the DISPLAY could lie.

Add one shared classifier, nostr::sanitize::is_display_dangerous, that
flags all Cc controls (C0/C1/DEL via char::is_control) plus the bidi
marks/overrides/isolates (U+200E/200F, U+061C, U+202A-202E, U+2066-2069)
and zero-width/BOM chars (U+200B-200D, U+FEFF). Route every untrusted
display surface through it:

- sanitize_note (DM subject/note): dangerous -> space, was control-only.
- validate_memo (pay-URI memo): now filters by char after decode so the
  multibyte bidi codepoints a byte filter missed are dropped.
- escape_for_display (authorize/login prompts): delegates to the shared
  classifier, completing TODO(audit M5).
- nip05::name_by_pubkey (authority @name): sanitized + capped at 64 chars
  before it becomes a rendered contact handle.

Only invisible FORMAT/CONTROL codepoints are neutralized; legitimate
letters of every script (Latin/accents, Arabic, Hebrew, CJK, Hangul) and
emoji pass through unchanged — proven by tests.
2026-07-10 15:36:29 -04:00
2ro 5466f687ac fix: route login/authorize callback POSTs over Tor, drop goblin-wallet UA
The login (kind 22242) and authorize callback POSTs carry a signed Nostr
event containing the user's identity pubkey. They went out via
crate::http::HttpClient::send (clearnet even when Tor is on) with an
identifying "User-Agent: goblin-wallet" header, so the callback server saw
the user's real IP tied to their Nostr identity — a deanonymization leak.

Reroute both post_login_event and post_authorize_event through
crate::tor::http_request_bytes, which honors route_over_tor(): Tor when the
open wallet routes over Tor, clearnet otherwise. Drop the goblin-wallet UA;
the helper sets a browser-like default User-Agent, so the traffic is not
trivially fingerprintable as Goblin at the destination. Content-Type is
preserved. Behavior with Tor off is unchanged (clearnet POST via the
helper's clearnet branch).
2026-07-10 15:36:29 -04:00
2ro d47a71a03b fix(http): cap collected response body size to prevent OOM DoS
HTTP response bodies were collected with no size limit on both the Tor
path (tor::request_once) and the clearnet path (http::clearnet_once),
each doing into_body().collect().to_vec() unbounded. A compromised or
malicious name-authority, relay-pool gist, NIP-11 relay-info, or price
endpoint could stream a multi-GB body and OOM the wallet. The websocket
path already caps frames at 4 MiB; HTTP did not.

Add a shared collect_body_capped() that rejects an oversized
Content-Length up front and bounds the actual streamed read frame by
frame (catching a lying/absent Content-Length), returning a clean Err
instead of panicking. Cap is 8 MiB: comfortably above every legitimate
response (relay-pool gist, NIP-05/.well-known/by-pubkey JSON, NIP-11
info, price/fee JSON are all a few KiB; the 1 MiB avatar PNG is the
largest) and a touch above the 4 MiB websocket ceiling, so nothing real
is clipped.
2026-07-10 15:36:29 -04:00
2ro 528d088e54 fix(nostr): bound payment-request store + isolate handler panics
F1 (disk-exhaustion DoS): incoming payment-request storage grew unbounded —
an attacker could stream valid Invoice1 requests from fresh ephemeral keys
(bypassing the per-sender limit), each persisting ~30KB of slatepack armor
until the device disk filled. Expiry only flipped status to Expired, never
freeing the row.

- expire_stale now DELETES terminal (Expired/Cancelled/Declined) request rows
  via NostrStore::prune_terminal_requests, actually reclaiming disk.
- New NostrStore::save_incoming_request enforces a REQUEST_CAP (2000): when at
  the cap it evicts terminal rows oldest-first to make room, and only if none
  can be freed does it REFUSE the new request (backpressure) rather than ever
  evict a live PENDING/Approved one. A refused request is not marked processed,
  so a later catch-up re-surfaces it once capacity frees. Ingest path routes
  through it; save_request stays unbounded for backup-restore/status-updates.
- Pure plan_incoming_admission policy + assert tests prove terminal-first
  eviction and that no pending row is ever dropped (unit + full-cap store test).

F2 (payment-service brick): handle_* were awaited inline in the notification
select! loop; a panic killed the service thread before started.store(false),
leaving started stuck true so restart() hung forever, and next-launch catch-up
re-ran the same event and re-panicked (self-reinforcing brick).

- Per-event dispatch is wrapped in futures catch_unwind so a panic in one event
  is logged and skipped, keeping the loop alive; the offending event is marked
  processed so it is not retried forever. Happy path unchanged (same inline,
  in-order execution).
- A drop guard on the service thread resets started/connected on ANY exit
  (normal or unwinding panic) so restart() can always recover. (Android APK is
  panic=abort; there the drop guard is the remaining safeguard.)
2026-07-10 15:36:29 -04:00
2ro c6a4376d7b Bound-check animated UR QR frames to stop crafted-QR crash/OOM
The inherited GRIM ur:bytes animated-QR path trusted the
`<index>-<total>` header from a scanned QR blindly:
- `index - 1` underflowed usize on an index-0 frame,
- `total` was uncapped so a huge value allocated multi-GB (OOM),
- the assembly step used `insert(index, ...)` which panicked out of
  bounds (hard-crash on Android's panic=abort; wedged scanner on
  desktop by leaving `image_processing` stuck true).

Validate the header in parse_qr_code (reject total 0, total over a
1024 cap, and index outside 1..=total; only subtract after index>=1),
bound-check and index-assign into the pre-sized vec instead of insert,
and reset the image_processing flag via a drop guard so a scanner-thread
panic can no longer wedge the scanner. No payment/relay/display/on-disk
behavior changes; legitimate animated QRs still reassemble.
2026-07-10 15:36:29 -04:00
2ro fc8c4aaa45 Copy 24-word seed phrase via auto-clearing clipboard helper
Both seed-copy buttons (wallet creation and onboarding) now use
copy_secret_to_buffer, which compare-then-clears the clipboard after a
delay, matching the nsec copy path, instead of the non-clearing
copy_string_to_buffer.
2026-07-10 15:36:29 -04:00
2ro 4baba58dc3 Write encrypted money-seed file with 0600 permissions on Unix
WalletSeed::init_file now creates wallet.seed owner-only (0600) via
OpenOptionsExt, mirroring the Nostr nsec in src/nostr/identity.rs, so
other local users cannot read it for an offline password grind. Uses
#[cfg(unix)] so non-Unix still compiles. KDF unchanged.
2026-07-10 15:36:29 -04:00
2ro d49cf5c82c Route update check through Tor and drop identifying User-Agent
retrieve_release now uses tor::http_request (honors route_over_tor:
Tor when on, clearnet when off) instead of the clearnet-only
HttpClient::send. Drops the goblin-wallet User-Agent so the check no
longer self-identifies; the Tor helper supplies a browser-like default
UA that also satisfies GitHub's UA requirement.
2026-07-10 15:36:29 -04:00
2ro 78f967e5fc Emphasize the VPN recommendation on the Network privacy screen
The VPN nudge under the Tor switch now renders semibold and one tone
brighter (text_dim instead of text_mute) so the safety recommendation
reads as a deliberate emphasis rather than a muted caption. Styling
only via RichText, so the single localized vpn_note string is untouched
and no retranslation is needed. The shared network_privacy_panels body
means this applies to both the Settings privacy screen and the
onboarding Network-privacy step.
2026-07-10 15:36:29 -04:00
2ro 4fee9f635e Advanced Privacy: lock amounts/names toggles under Hide all details
When the Notifications "Hide all details" switch is on, the finer
"Hide amounts" and "Hide names" switches now render forced visually ON,
dimmed, and non-interactive (a new toggle_locked widget plus a
settings_row_toggle_locked helper) instead of misleadingly showing OFF.

The locked rows only render; they never write the hide_amounts /
hide_names config, so the stored values are preserved. Turning "Hide all
details" back off restores the user's real per-toggle choices and makes
the switches interactive again. No copy/locale changes.
2026-07-10 15:36:29 -04:00
2ro f4a3741e9b chore: ignore local agent tooling and in-flight worktrees 2026-07-10 12:36:43 -04:00
2ro 2a393aca89 goblin: return to the calling app after a successful sign-in or authorize (Build 151) build161 2026-07-10 00:47:15 -04:00
2ro 5e3602d3a1 goblin: seal activity history into the full .backup and restore it
A full .backup carried only the seed, identities and active pointer, so a
restore recovered funds via chain rescan but lost the Goblin activity layer
(counterparty npubs/names, tx notes, request records). Rebuild that layer by
sealing the nostr store's activity metadata into the backup under the same
password and merging it back on import.

- ArchiveSnapshot (types): tx_meta + contacts + requests across all
  identities — the minimal set the activity list and requests panel join on.
  Processed markers, service settings and cached news are excluded (operational
  or relay-reproducible). All fields serde-default for forward/backward compat.
- NostrStore::snapshot_archive / merge_archive: merge is non-destructive —
  tx meta by updated_at, contacts/requests insert-if-absent, so a restore over
  an existing wallet never regresses newer local rows.
- build_full_backup gains an optional history_json, sealed with the existing
  two-layer seal_secret_text (npubs/names/notes never appear in plaintext).
  open_full_backup surfaces it as FullBackup.history.
- Format stays compatible both ways: the envelope is read field-by-field with
  serde_json .get(), so a NEW file still restores on an OLD app (unknown
  history field ignored) and an OLD file still restores on a NEW app (absent
  history -> None). Omitting history reproduces the pre-history bytes.
- create_full_backup snapshots the store; restore_full_backup_identities (the
  single restore chokepoint for settings + onboarding) merges it in.

Tests: history round-trip, legacy-without-history opens clean, new-file keeps
the legacy open-path fields, snapshot->merge into an empty store, and merge
does not clobber newer local rows.
2026-07-10 00:44:00 -04:00
2ro 77c1c0934b goblin: no npub association => anonymous avatar on tx rows
Wiping payment history clears tx_meta (the tx<->npub link), so build_item
already anonymizes a wiped row's title/npub. But the avatar was resolved
from the row's name string independently of the npub, so a leftover name
or cached profile could still render a real profile picture and betray who
a wiped tx was with.

Gate avatar resolution on the presence of an npub association: a tx row
with no npub (wiped metadata, or a non-nostr/plain tx) now renders the
anonymous yellow-goblin tile and never fetches a real avatar, across the
activity feed, receipt and profile-history surfaces. Contacts are kept by
design (the address book) and are documented/asserted as such.

Adds a store-level wipe test (no UI query can rejoin a wiped tx to an npub
or name) and a pure resolution-gate test.
2026-07-10 00:43:46 -04:00
2ro 6cf36402ec goblin: retranslate updated privacy copy across locales 2026-07-10 00:43:46 -04:00
2ro 8e2097da87 tor: restructure network-privacy UI + tighten English copy
On-device build160 feedback (Android, dark + light):

- Route-through-Tor card: stack it centered so it reads as switching
  between Tor and direct. Title on top, the big switch on its own row,
  full-width caption below. The subtitle can no longer clip under the
  switch at 390px or larger font scales.
- Grow toggle_large from 64x36 to 96x54 (roughly 1.5x) for the switch.
- Grin-node status dot is now the brand yellow in every theme (new
  theme::GOBLIN_YELLOW const; theme accent inverts to dark on the yellow
  theme, so it couldn't be used here).
- Drop the onboarding "NETWORK PRIVACY" top-right kicker so the privacy
  screen shows just the back chip + title, matching the settings
  sub-pages (Advanced Privacy, Node). step_header skips an empty kicker.
- Copy (English only, pending owner sign-off): merge the two intro
  blocks into one short line and drop the "How your privacy works"
  heading; "Usernames" capitalized + "to and from name authorities";
  Tor caption shortened to "Hide your IP from relays."; shorter
  Grin-node blurb. Stale non-English values left as-is; key set
  unchanged (how_privacy / how_privacy_blurb / onboarding.privacy.kicker
  kept but unused, so i18n parity holds).
2026-07-10 00:43:46 -04:00
2ro f37788cf26 goblin: floonet relay is a plain removable default, not a pin
Owner correction: relay.floonet.dev ships in the default lists
(DEFAULT_RELAYS / TOR_RELAYS) but gets no special treatment. All three
defaults are ordinary, fully removable entries: the remove (X) control is
back on every row and a user override is used verbatim, so a saved list
without floonet stays without floonet, per wallet per transport.

Drops pin_floonet() and the forced prepend in effective_relays, and
updates the stale "pinned shared floor" style comments to say defaults
are just defaults and user edits win. The per-transport persistence work
stands unchanged.
2026-07-10 00:43:46 -04:00
2ro 6d7a0d6574 goblin: remember relays per wallet, keyed by transport
The "Nostr Relays" editor saved a single transport-agnostic override that
won on both Tor and clearnet, so a list edited on one network silently
replaced the other. Split the persisted override into relays_tor and
relays_clearnet in nostr.toml so each network keeps its own remembered
list across app updates; the legacy `relays` field stays honored as a
fallback for whichever transport has no explicit set (no migration).

If the user has never edited, behavior is unchanged: the pinned TOR_RELAYS
set on Tor, the per-identity random healthy subset on clearnet.

relay.floonet.dev is now pinned first on every resolved override via
pin_floonet() and can no longer be removed in the editor, so any two
wallets always share a rendezvous regardless of per-wallet edits.

Adds round-trip / precedence unit tests for the config and the pin helper.
2026-07-10 00:43:46 -04:00
2ro 905b3ab94d goblin: split nostr/session.rs into session/ submodules
Pure move (no behavior change): convert the ~1,985-line session.rs file
into a session/ directory module along its existing banner-marked seams:

  session/mod.rs      locked constants, the Session object and its
                      enforcement (decide/serve bookkeeping, rate/size/TTL),
                      SessionSummary/PendingMoney, and the full test suite
  session/classify.rs tier classification and the trust/grant taxonomy
                      (kind-to-tier table, content escalation, kind-set
                      sanitation, category display mapping)
  session/sign.rs     request/channel wire types, the client-pinned signer,
                      NIP-44 envelope shapes and the serving orchestration

Public API is unchanged: mod.rs re-exports classify and sign items
(pub use), so every crate::nostr::session path still resolves. The shared
abs_diff clock helper lives in the parent so both descendants reach it; no
other visibility changed (descendants already see parent-private items).
2026-07-10 00:43:46 -04:00
2ro 9e4289041e goblin: split nostr/client.rs into client/ submodules
Pure move (no behavior change): convert the ~3,400-line client.rs file
into a client/ directory module, splitting the NostrService impl and the
background service machinery along existing seams:

  client/mod.rs      NostrProfile/HeldIdentityKeys/NostrService/TransportStatus
                     types + core lifecycle & status accessors
  client/sessions.rs Authorize-Sessions surface (add/announce/resume/end,
                     summaries, money-prompt answering)
  client/identity.rs identity/key surface, contact resolution, NIP-05 recheck
  client/send.rs     DM send path (payment/control DMs, delivery wraps, gates)
  client/service.rs  run_service loop, relay connect/redial, ingest, expiry

Public API is unchanged: methods stay on NostrService and resolve at the
same crate::nostr::client paths. Cross-module private items widened only to
pub(super) where a sibling calls them. Tests move next to their subjects.
2026-07-10 00:43:46 -04:00
2ro de0c8737f3 goblin: split mod.rs into focused sibling submodules
Behavior-preserving structural refactor of the ~8,600-line
src/gui/views/goblin/mod.rs into 17 flat sibling modules following the
existing send.rs/onboarding.rs/widgets.rs pattern. Pure move: no UI,
string, layout, or logic changes.

mod.rs keeps the shell (Tab, GoblinWalletView + state structs,
SettingsPage, Default impl, nav/lifecycle methods, the ui() dispatcher,
and chrome: tab_bar_ui/sidebar_ui/handle_tex/avatar_self) and shrinks
from 8,640 to 1,711 lines.

New siblings (each `use super::*;`, cross-module items pub(super)):
home, pay, receipt, profile, activity, receive, me, settings,
settings_advanced, settings_node, username, identities, prompts, modals,
privacy (network/Tor screen), helpers, format. Unit tests moved next to
their subjects (censor tests -> format::tests; dispatcher test stays).

cargo check --all-targets clean; cargo test --lib 257 passed;
i18n_keys passed. rustfmt applied to touched files.
2026-07-10 00:43:46 -04:00
2ro fc0999e271 nostr: opportunistic NIP-42 auto-auth for hardened DM reads
Enable the SDK's built-in NIP-42 auto-auth explicitly on the wallet's
relay-pool client so it answers a relay's AUTH challenge with a
signer-signed kind-22242 event, then re-issues the pending REQ. It fires
ONLY when a relay challenges (opportunistic): the wallet never forces
auth and never refuses a non-challenging relay, so it is inert against
today's non-challenging relays and only activates once the recipient-only
strfry fork starts challenging kind-1059 reads.

automatic_authentication defaults to true in nostr-sdk 0.44; set it
explicitly so a future default flip cannot silently break DM reads on the
hardened relay. Multi-identity is active-identity-only (single signer per
connection); the builder comment documents the full-multi-identity path
via the pool's forwarded AUTH challenge. Adds an inline test asserting the
opts+signer wiring.
2026-07-10 00:42:05 -04:00
2ro 0b5b072332 tor: drop dead privacy.over_tor/direct_connection i18n keys
The old privacy_ui used these two headers; this branch replaced it with
network_privacy_panels, which does not. Remove the now-unreferenced keys
from all locales (parity preserved, i18n_keys drift guard still green).
2026-07-10 00:42:05 -04:00
2ro 25e349a979 tor: translate per-user-tor strings, zh-Hant/HK auto-detect
Translate the 16 new per-user-Tor keys and the changed transport-neutral
goblin.privacy.intro across all 9 non-English locales (de/es/fr/ja/ko/ru/tr/
zh-CN/zh-TW), replacing the slice-3 English placeholders. Confirm
goblin.receive.copy was already translated everywhere.

Normalize zh-Hant, zh-HK, and zh-Hant-* system locales to zh-TW (and
zh-Hans(-*) to zh-CN) in setup_i18n, so Traditional-script systems that
don't report the exact zh-TW tag still land on Traditional instead of
falling through to the default locale.

Note the zh-TW -> zh news-locale fold in news_locale_code as an
intentional, low-priority tradeoff (Traditional readers see the same
zh news feed as Simplified); no new pipeline added.
2026-07-10 00:42:05 -04:00
2ro 8c8c75d18c tor: share nprofile on receive, Copy label, centered goblin mark on QR
- receive Share/Copy now emit the nprofile (relay hints) so payers learn
  which relays to reach the recipient on; in-app npub still tracks who-is-paid
- rename receive button 'Copy npub' -> 'Copy' (goblin.receive.copy) in all
  10 locales; copies the nprofile the QR already encodes
- composite the black goblin mark centered on the receive/my-code QR
  (widgets.rs, High ECC) and the static qr.rs display (bumped to High ECC)
2026-07-10 00:42:05 -04:00
2ro 346422003f tor: network-privacy UI, blueviolet token, big switch + status wiring
Phase 2 slice 3 of the per-user-tor plan: the interactive UX layer on top of
slices 1-2 (wallet-level Tor setting + transport-aware relay selection).

Colors:
- Add a tor_purple token (CSS blueviolet #8A2BE2) to ThemeTokens in all three
  theme consts, exposed via Colors::tor_purple().

Shared Network-privacy component (onboarding + settings never drift):
- network_privacy_panels(): intro copy (what Goblin sends + how privacy works,
  no crypto version numbers), a Grin-node ALWAYS-DIRECT panel (grey dot, full
  width), a private-traffic panel whose header is TOR OFF (red) / TOR ON (green)
  and whose status dots are goblin-yellow when off / blueviolet when on, a large
  Tor switch (w::toggle_large: dormant gray off, blueviolet on, never yellow),
  and a VPN nudge. Repurposes the old read-only privacy_ui.

Onboarding:
- New Step::Privacy rendered before wallet creation; the create/restore triggers
  route here first. New users default OFF. The choice is stashed on
  OnboardingContent and written as the new wallet's EXPLICIT tor setting
  (set_tor_enabled) right after open() (init_nostr builds the service
  synchronously; start() runs later on the sync thread), so a new wallet writes
  an explicit value while upgraders keep None -> Tor ON.

Settings:
- Tor routing row shows On/Off and opens the Network-privacy screen; the big
  switch there flips the wallet tor setting (set_tor_enabled + set_route_over_tor)
  and restarts the service so the pool rebuilds on the new transport.

Status lines:
- Rewire the 3 Tor-only status call sites (home card, Me identity row,
  onboarding identity) to service.transport_status(), adding Connected/
  Connecting (direct) states so a clearnet wallet no longer reads as forever
  connecting over Tor. Fix the Me-row repaint loop to gate on relay-connected
  rather than the Tor-only transport_ready.
- Re-export TransportStatus from crate::nostr (slice-1 miss).

i18n: 16 new keys added to all 10 locales (English placeholder in the 9
non-English files; a following slice translates them).
2026-07-10 00:42:05 -04:00
2ro d48cad5e6a tor: transport-aware relay-set selection (fixed Tor pin vs per-identity clearnet subset)
On Tor, every identity uses a FIXED pinned set (relay.floonet.dev +
relay.nostr.net + offchain.pub). On clearnet, each identity keeps its
persisted random healthy subset (dm_relays), floonet pinned. Route
NostrService::relays() through a new pure effective_relays() resolver
and gate ensure_advertised_set() to clearnet so Tor never overwrites the
persisted clearnet subset. relay.floonet.dev is pinned first in both
regimes. Adds unit tests for the resolver.
2026-07-10 00:42:05 -04:00
2ro 3c3b680f93 tor: per-wallet Tor setting + reintroduce the clearnet transport/HTTP path
Foundation slice for making Tor a wallet-level user setting (Phase 2, slice 1
of the per-user-tor plan). Transport/config only; no onboarding/settings UI,
relay-set swap, or nprofile in this slice.

Wallet-level tor_enabled (tri-state):
- NostrConfig gains tor_enabled: Option<bool> with the existing Option +
  serde(default) pattern. tor_enabled() resolves None -> ON, so every legacy
  nostr.toml keeps Tor with no migration; new wallets write an explicit value at
  onboarding (later slice). Adds tor_enabled_is_set() + set_tor_enabled() and a
  back-compat/tri-state unit test.

Clearnet path (the big lift, rebuilt after the Nym removal took it out):
- ClearnetWebSocketTransport parallel to TorWebSocketTransport: direct TLS
  websocket, honoring the user's AppConfig SOCKS5 proxy (tokio-socks, already in
  the tree) so a VPN/proxy user can front their traffic. Shares ws_config/
  split_ws with the Tor transport.
- Transport selection moved into run_service: the wallet's resolved tor_enabled()
  picks Tor vs clearnet when the one pool is built; the Tor-bootstrap wait is
  skipped for clearnet wallets. restart() re-enters here, so a settings toggle
  rebuilds the pool on the new transport.
- Process-global route flag (tor::set_route_over_tor/route_over_tor), mirrored
  from the open wallet like set_home_domain, so free-function HTTP callers pick
  the matching transport off-thread.
- tor::http_request_bytes takes a clearnet branch when the wallet is Tor-off
  (no more hard-fail on "Tor not bootstrapped"); new http::clearnet_request_bytes
  reuses the existing HttpClient (proxy-aware) with the same redirect behavior
  and browser-like UA. NIP-05, price, relay-pool gist and NIP-11 probes all ride
  it unchanged.

Status/readiness:
- New TransportStatus enum + NostrService::transport_status()/tor_routing():
  exposes a "Connected (direct)" clearnet state so a Tor-off wallet does not read
  as "connecting over Tor" forever. The three UI status lines are rewired to it
  in a later slice; the state is exposed cleanly now.

Grin node path unchanged (stays clearnet always). cargo check --all-targets
green; targeted config/nostr/tor tests pass.
2026-07-10 00:42:05 -04:00
2ro d7ec692d03 pay: remove top-right tap-to-settings avatar from pay page
Build157 put GoblinView::avatar_self (a tap-through to Settings) top-right
on both the home and pay headers. Per owner request, drop it from the PAY
page only; the home header avatar is unchanged. The scan-QR glyph and the
rest of the pay layout are left in place. Settings remains reachable from
the bottom nav bar (Tab::Me) and the wide-mode sidebar.
build159
2026-07-08 02:22:53 -04:00
2ro 9bdc0e2c60 locales: apply firm zh-CN audit corrections + mirror to zh-TW
Fix 21 keys (23 substitutions) flagged in the zh-CN Goblin-string audit,
and apply the equivalent Traditional corrections to the matching zh-TW keys
so both stay correct and consistent:
- 7 leftover-English strings (you/username/handle/kind) translated
- 3 stray formal 您 -> informal 你 (plus Nostr -> nostr where en is lowercase)
- 7 terminology tweaks (机构->授权方, kind->类型, 活动->动态, 费用->手续费)
- 4 half-width -> full-width commas
Judgment-call rows (review_btn/title, pairing.title) intentionally not applied.
2026-07-08 02:22:53 -04:00
2ro e623271476 locales: add Traditional Chinese (zh-TW)
Full native-quality Traditional Chinese (Taiwan) locale covering all 910
keys. Shared GRIM-inherited keys use jasperli2026's zh-TC values verbatim;
goblin.* namespace translated from the corrected Simplified source into
Taiwan-idiom Traditional (OpenCC s2twp plus a manual Taiwan post-fix and
review pass), keeping Jasper's voice and crypto-wallet register. Cangjie
keyboard labels reused from zh-CN; lang_name set to 繁體中文.

Wired into the i18n drift test (OTHER_LOCALES) and README attribution;
rust_i18n auto-loads it and the language picker labels it via lang_name.
System locale zh-TW auto-selects; news folds zh-TW to zh (shared with
Simplified) as noted in news_locale_code.
2026-07-08 02:22:53 -04:00
2ro 8bbb7853ef locales: adopt corrected Simplified Chinese from jasperli2026 (keep Cangjie keyboard + Goblin title) 2026-07-08 02:22:53 -04:00
2ro f2781fb5a9 locales: fix zh-CN lang_name to native 简体中文
The Chinese locale self-label read 英语 (the word for "English"). Use the
native Simplified Chinese name so the language picker row is correct.
2026-07-08 02:22:53 -04:00
2ro 52e8ff6236 settings: clear Android bottom inset so last button is not clipped
Append ui.add_space(View::get_bottom_inset()) at the end of
SettingsContent::ui. The settings screen supplies no bottom inset (its
tab panel is hidden and the CentralPanel frame uses bottom:0), so the
final FADERS Settings button clipped under 3-button nav / the gesture
pill. get_bottom_inset() is 0 off Android, so desktop is unaffected.
2026-07-08 02:22:53 -04:00
2ro 1521b22cdd nostr/pool: remove inert co-located-exit scaffolding
Drop the PoolRelay.exit field and the exit_for/exit_for_host/has_exit
helpers left over from the retired Nym-era co-located scoped exit. The
deleted src/nym was their only real caller; the Tor build never consumes
them. PoolRelay has no deny_unknown_fields, so a gist that still carries
a stray per-relay "exit" key keeps deserializing and the key is ignored
(covered explicitly in the unknown-fields test).
2026-07-08 02:22:53 -04:00
2ro 35c9ea3bbc Remove dormant Nym/mixnet transport, commit fully to Tor
The wallet pivoted from the Nym mixnet to Tor in build134; the Nym code
had been left on disk behind an optional, always-off "nym" feature and
never ran since. Delete it entirely: no Nym/mixnet fallback and no
Nym/mixnet mentions remain. Tor is the sole transport.

- Delete src/nym/ (mod, transport, streamexit, dns, nymproc) and its
  `#[cfg(feature = "nym")] pub mod nym;` declaration in lib.rs.
- Cargo.toml: drop the `nym` feature and the commented-out Nym-only
  path deps (nym-sdk, smolmix, hickory-proto); rewrite the mixnet-era
  comments on the rustls/tokio-rustls/arti/openssl deps to the current
  Tor reality. No active dependency changed: rustls, tokio-rustls and
  webpki-roots stay (used by the Tor HTTPS client in tor/mod.rs).
- settings/config.rs: remove the persisted nym_entry_gateway /
  nym_last_ipr fields and their getters/setters. No serde
  deny_unknown_fields, so existing on-disk configs that still carry
  those keys keep deserializing (the removed keys are ignored and
  dropped on next save; same path the price-cache-removal test guards).
- Rename the Nym/mixnet-named i18n keys (values already said "Tor")
  across all 9 locales and their t!() call sites: connected_nym ->
  connected_tor, nym_ready -> tor_ready, connecting_nym ->
  connecting_tor, mixnet_routing -> tor_routing, over_mixnet ->
  over_tor. Displayed strings unchanged.
- Rewrite the Nym/mixnet-referencing comments in the active Tor files
  (tor/engine.rs, tor/mod.rs, nostr/client.rs, nostr/pool.rs,
  nostr/mod.rs, lib.rs, wallet/wallet.rs, node/node.rs,
  gui widgets/mod/onboarding, Android BackgroundService) to describe
  Tor; drop the now-broken [crate::nym::*] intra-doc links. No active
  code behavior changed.

The pool's `exit` schema slot and exit_for/exit_for_host/has_exit
helpers are kept (comments de-Nym'd): they are inert under Tor but let
a pool document that carries an exit still parse.
2026-07-08 02:22:53 -04:00
2ro 079fdd4841 docs: README default relay set matches code (Tor-friendly relays, drop damus/nos.lol) build158 2026-07-07 19:21:05 -04:00
2ro 4337580806 goblin: full wallet backup (seed + all identities) with restore
The .backup file now seals the money seed AND every held identity in one
encrypted, versioned envelope, and the caption's promise ("Contains your
wallet and all identities.") is finally true.

Format (nostr/identity.rs): a JSON envelope tagged goblin_backup:2. The
seed phrase is sealed with seal_secret_text() and each identity with the
existing per-identity to_encrypted_backup() (so every element is itself a
valid v1 identity envelope). Both layers reuse the in-tree NIP-49 ncryptsec
+ NIP-44 primitives under one password: no new crypto, no new dependency.
The plaintext seed is assembled and sealed in memory (the zeroizing
recovery string drops at scope end) and never written to disk. Old v1
single-identity backups keep restoring unchanged; is_full_backup() is
checked before the v1 path so the two never collide.

Build (wallet.rs): create_full_backup() gathers the seed via the existing
recovery API (which proves the wallet password) plus every unlocked held
identity. restore_full_backup_identities() reinstates them into a freshly
created wallet, re-encrypting each under the new wallet password, making the
backup's active identity active, and reusing the held-index/service-rebuild
plumbing. The identity-only import paths now reject a full backup with a
clear pointer to wallet creation.

Restore (onboarding.rs): the restore-from-seed step gains a .backup picker;
unlocking it decrypts the seed into the standard 24-word creation grid and
stashes the identities, which a worker reinstates once the wallet opens.

Copy: backup caption, title, blurb, and saved-sub updated across all nine
locales; two new restore strings added in all nine.

Tests: full_backup_roundtrips_seed_identities_and_active,
old_single_identity_backup_is_not_a_full_backup,
seal_secret_text_roundtrips_and_is_opaque; i18n drift + news tests green.
2026-07-07 19:21:05 -04:00
2ro 5ea549abf4 goblin: restructure Advanced page into Advanced Nostr Settings + Danger Zone
Group the Advanced settings page under two grey/red section kickers:

- ADVANCED NOSTR SETTINGS: the Nostr key, and directly below it the
  single always-everything .backup download (one button, no checklist).
  Moved the .backup off the main Settings identity card so it has one
  home (no duplicated exposure). Caption states what the file truly
  holds: the current identity's key and username.
- DANGER ZONE (red kicker): Delete wallet, now password-gated. The old
  tap-twice confirm is replaced by a wallet-password gate
  (get_recovery), with an inline "Download backup" button before the
  password field wired to the same seal action. Copy states the delete
  also removes every identity and urges a backup first.

Add w::kicker_danger; BackupState.anchor_delete routes the seal form to
whichever section opened it. New strings in all nine locales; clear the
backup password from memory on leaving the page.
2026-07-07 19:21:05 -04:00
2ro 5fecba6ff5 goblin: make the self top-right avatar mode-aware (anon-only yellow)
Build 157 hard-coded the flat Goblin-yellow censored tile for the user's
own top-right avatar on the home and pay headers, regardless of anonymous
mode. The owner clarified: anonymous mode is the ONLY thing that turns an
avatar yellow.

Replace the mode-blind w::avatar_self widget with a mode-aware
GoblinView::avatar_self helper: anon ON renders the censored yellow tile
(w::avatar_censored), anon OFF renders this identity's normal gradient/
picture identicon (w::avatar_any), matching every other surface. Remove
the now-unused free avatar_self widget from widgets.rs.

All other avatar_censored call sites (identity switcher, activity rows)
remain correctly gated behind the anonymous-mode flag.
2026-07-07 19:21:05 -04:00
2ro debad91e29 nostr: raise news cap to 18 so English variant survives multi-language batches
A single news post now ships as nine per-`d` language variants and the
publisher emits English first, giving it the oldest `created_at` in the
batch. With the store capped at 8 events across all d-tags, the English
event was always the 9th-newest and got evicted — English readers saw no
news. The catch-up/subscription filter also capped fetches at 4, so fresh
installs only ever received the newest four events.

Raise NEWS_CAP from 8 to 18 (two full nine-language posts) and bump the
news filter limit from 4 to 18 to match. Selection logic is unchanged.
Extend the reconcile_news test with a nine-variant batch that proves the
untagged English event survives under the real cap.
2026-07-07 19:21:05 -04:00
2ro 567c054fe6 goblin: drop the Activity top-right self avatar (owner veto)
Activity returns to a plain title with no top-right avatar; the flat
yellow Grin-mark self avatar stays on the Home and Pay headers only.
build157
2026-07-07 14:22:46 -04:00