1
0
forked from GRIN/grim

1171 Commits

Author SHA1 Message Date
2ro 51de695d4e ja: align with removed rotate/import keys + learn_more build156 2026-07-07 01:49:58 -04:00
2ro f39785096d goblin: revert Pay header to original goblin mark
Restore the Pay tab header top-left rendering to the original goblin
mark (goblin-logo2.svg tinted to the header ink at 40px), reverting the
GoblinPay badge/wordmark theme-split lockup introduced by f768907 (UI
pack 153, merged via build154). Matches the owner reference: the plain
black goblin girlhead silhouette on the yellow header, no badge, no
wordmark.

The two badge assets added by that commit are no longer referenced
anywhere after the revert, so remove them:
  img/goblinpay-badge-black.svg
  img/goblinpay-wordmark.svg

Scope is strictly the Pay header hunk; the Home wordmark sizing from the
same UI pack is untouched.
2026-07-07 01:49:48 -04:00
2ro 705b954fe9 es: fix handle-loanword and verb-form inconsistencies
Full read-through of the just-landed es and ko locales against en.yml.
ko needed no changes. es had three spots that drifted from its own
established style:

- share_handle / n05_blurb translated "handle" as "identificador(es)"
  while every other occurrence in the file (search_hint, no_contacts,
  scan_not_recipient, enter_recipient) keeps it as the loanword
  "handle" — switched these two to match, consistent with the
  fr/de/tr precedent of treating "handle" as an untranslated loanword.
- scan_to_pay_me used the imperative "Escanea" while every sibling
  label (scan_to_pay, scan_to_request, tab_scan, scan_qr) uses the
  infinitive "Escanear" — aligned it to match.
2026-07-07 01:49:48 -04:00
2ro 78c5667a18 Add Japanese (ja) as ninth Goblin wallet locale
Full translation of all 918 goblin.* leaf keys from en.yml, byte-matching
key order and %{...} placeholders, standard polite desu/masu register.
House terms follow the es/ko precedent: Goblin/GoblinPay/GRIM/Tor/Nostr/
slatepack/npub/nsec stay untranslated; wallet = ウォレット, recovery
phrase = リカバリーフレーズ, identity = アイデンティティ, relay = リレー,
handle stays as a katakana loanword (ハンドル) throughout.

Font: verified glyph-by-glyph against fonts/noto_sc_reg.otf (already
wired into every font family stack) that it covers every character used
in ja.yml, including hiragana/katakana and kokuji kanji not used in
Chinese, so no new Japanese font file is needed.

Keyboard block keeps literal QWERTY romaji like en/es/fr/de: the custom
on-screen keyboard widget is dead code in this fork (no_soft_keyboard is
always true; native OS keyboards handle all text entry), so there is no
functional need for a kana layout, per zh-CN precedent review.

Registers ja in tests/i18n_keys.rs OTHER_LOCALES and refreshes locale
count/list comments in src/lib.rs, src/gui/views/goblin/mod.rs and
src/gui/views/goblin/data.rs.
2026-07-07 01:49:48 -04:00
2ro befff36133 Fix Sign in with Goblin trust announce over Tor
The session-open announce that tells the site a trust session exists was
confirmed on the wrong signal and never retried, so the live flow either
toasted announce_failed or returned to the browser without logging in.

Root causes:
- Confirm-on-any-relay: announce_new_sessions marked the grant confirmed
  when ANY wallet relay accepted the session-open (out.success non-empty).
  The site (magick) subscribes ONLY on its r= hint relay, so an accept on
  another wallet default let the wallet "succeed" and return-to-caller
  while the site never saw the session. Now confirm requires a relay the
  site actually listens on (the hint) via announce_confirmed().
- Announced-set-unconditionally: s.announced was set true even when the
  publish failed, and nothing re-armed sessions_dirty, so a failed announce
  was never retried and announced_ok stayed empty forever. announced now
  flips true only on a real hint-relay confirm; the loop re-arms and
  re-publishes each tick until confirmed or ANNOUNCE_MAX_ATTEMPTS, tracked
  by a new Session::announce_attempts counter.
- Relay dedup/normalization: channel_relays deduped hints by raw string, so
  a trailing-slash or case difference (wss://host/ vs wss://host) forked a
  phantom cold relay and broke confirm matching. canonical_relay() now folds
  both the union (dedup_relay_union) and the confirm comparison.
- Deadline too short for a cold Tor circuit: TRUST_ANNOUNCE_TIMEOUT_SECS
  raised 15s -> 30s to cover reaching a relay we were not already dialed to.

Tests: canonical_relay/dedup, hint-relay confirm semantics, retry cap.
cargo test --lib 246 passed (242 baseline + 4), i18n_keys green, fmt clean.
2026-07-07 01:49:48 -04:00
2ro e3b4545dc4 goblin: remove Settings Import identity (replace-current) flow
Owner correction: importing an existing identity belongs at first-run
(onboarding, restored in the previous commit), not in Settings. The
Settings > Identity "Import identity" row was the destructive
replace-the-current-identity action, redundant now that add-identity and
the identity switcher cover bringing in keys non-destructively. Removes
the settings row, the inline ImportState/import_nsec_ui flow, and the
five now-unused settings keys (import_identity, import_identity_title,
import_blurb, identity_replaced, now_using) across all eight locales.

Wallet::import_nostr_identity stays: the onboarding import panel calls
it. The shared settings.* strings the onboarding panel reuses
(importing, import_failed, import_nsec_hint, backup_read_failed,
choose_backup_file, backup_password_hint, import_btn, cancel, close)
also stay. Drift + call-site tests green.
2026-07-07 01:49:48 -04:00
2ro 26d3d41790 Revert "goblin: remove the Import identity option from onboarding"
This reverts commit b1baa14b85.
2026-07-07 01:49:48 -04:00
2ro 811eff825c goblin: remove the rotate-nostr-identity feature
Rotation just does Keys::generate (IdentitySource::Random) — the old
seed-derived path was dropped in Build 8 — so it is functionally a random
add-identity-and-switch, and it does NOT carry the username or profile
over (it RELEASES the old name and hands the new key a nameless identity).
It therefore adds nothing over the identity switcher, so remove it: the
Settings entry row, the inline rotate flow (RotateState + rotate_ui) and
the now-unreachable Wallet::rotate_nostr_identity. Shared helpers
(unlock_all_identities, create_random, nip05::unregister) are untouched.
Drops the fifteen unused goblin.settings.rotate_* / key_rotated /
new_npub / backup_new_key / copy_new_nsec / rotation_failed keys from all
eight locales; drift + call-site tests green.
2026-07-07 01:49:48 -04:00
2ro 44f9363ecf goblin: remove the Import identity option from onboarding
Onboarding import replaces the just-created identity and is risky, so the
owner ruled it out of the first-run flow; returning users import via the
normal identity-management flow in Settings instead. Removes both entry
points added earlier (the wallet-step import chip and the identity-step
centered button) and the now-unreachable onboarding import sub-flow
(OnbImport state + import_ui), leaving the Settings import path fully
intact. Drops the unused goblin.onboarding.wallet.import_identity{,_hint}
and goblin.onboarding.identity.import_{existing,title,blurb} keys from all
eight locales; drift + call-site tests green.
2026-07-07 01:49:48 -04:00
2ro 3b80650972 goblin: shorten Username name-authority copy + add Learn more link
Replace the long name-authority blurb on the Username page with one brief
line, plus a "Learn more" link that opens the name-authority docs chapter
(https://docs.goblin.st/features/name-authority.html) via the same
open_url idiom used elsewhere in Settings. Authority list, free-type and
save behaviour are unchanged. New goblin.username.learn_more key and the
shortened authority_blurb across all eight locales; drift + call-site
tests green.
2026-07-07 01:49:48 -04:00
2ro bb1023e51b Anonymous mode: censored avatars + Recent strip
While anonymous mode is on, replace every counterparty avatar on the
wallet home and activity panels with one uniform censored tile: a solid
Goblin-yellow (#FED60E) circle with the Goblin mark inked dark on top
(reusing the img/goblin-logo2 asset that widgets_logo draws). Identical
for every identity, so no picture, gradient, or initial leaks.

The Recent strip is now anonymized like the rest of the surface: the
censored tile for every avatar and dotted names. Tap-to-reveal is intact
(the row/tile still senses clicks and opens the full detail).

Shared CENSOR_NAME_DOTS backs the activity title and the Recent names.
Adds a unit test that the censored name is dots-only.
2026-07-07 01:49:48 -04:00
2ro c3bd0c00d0 android: sign release build type from keystore.properties when present
The release build type now applies signingConfigs.release when
android/keystore.properties exists, and stays on the debug-signed
fallback when it is absent, so existing local and CI builds are
unchanged. Also ignore *.jks so signing keys can never be committed.
2026-07-07 01:49:48 -04:00
2ro 773589a2f1 Add Spanish and Korean locales (es, ko)
Translates every en.yml key into es.yml and ko.yml (918 keys each,
placeholder-parity verified), wires both into the drift/call-site test
(tests/i18n_keys.rs), and bundles a Hangul font subset (noto_kr_reg.otf,
~1.9MB, Noto Sans CJK KR Regular subset to Hangul + Jamo + Latin/CJK
punctuation) as a proportional/monospace fallback so Korean UI text
actually renders instead of showing tofu boxes. The Chinese font already
covers Han but not Hangul, which is a disjoint Unicode block.

Language registration itself needed no code changes: rust_i18n::i18n!
auto-discovers yml files in locales/, the locale-detection fallback in
setup_i18n already matches on bare subtags generically, and the language
picker iterates rust_i18n::available_locales!() and renders each
locale's own lang_name key (Espanol, and Hangul for Korean).
build155
2026-07-06 23:09:45 -04:00
2ro 94951e8ca9 Username page: name authority on top, claim below
Swap the two panels on the Username settings page so the flow reads
top-to-bottom: pick your name authority first (known list + custom
save, default goblin.st), then claim or release your name on it.

Pure reorder of username_ui; no behavior or copy changes.
2026-07-06 23:09:45 -04:00
2ro f72a9718a3 goblin: move Trusted Sites into the nostr settings group + fix raw-key leaks
Trusted Sites is nostr-identity signing, and name authority now lives on the
Username page, so the nostr cluster is its logical home. Pure placement move:
the row now renders inside the identity card just below Nostr Relays; label,
live session-count value, and open handler are unchanged.

Also carries the two raw-key leak fixes (goblin.receipt.copied ->
goblin.receive.copied, wallets.canceled -> wallets.tx_canceled) and the
every_t_call_site_key_exists_in_en guard, so this branch is clean on its own.

Translations for the new settings/username/advprivacy keys were reviewed
across all six locales and left as-is (consistent with house register).
build154
2026-07-06 22:11:00 -04:00
2ro 7223e65a7a goblin: promote "Import identity" to first-class onboarding buttons
The returning-user identity-import path (a .backup file or a bare nsec,
existing plumbing since Build 92) is now surfaced as proper centered
buttons instead of a small left-aligned text link.

- Wallet step: a first-class "Import identity" choice stands alongside
  Create new / Restore from seed. It composes with either seed choice; the
  import panel itself opens on the identity step once the wallet exists.
- Identity step: the left-aligned "Already have an identity? Import it"
  link is replaced by a centered secondary button labelled "Import
  identity", consistent with the other onboarding buttons. The panel behind
  it clearly offers BOTH a .backup file picker and an nsec field.

New keys goblin.onboarding.wallet.import_identity{,_hint} in all six
locales; drift test green.
2026-07-06 22:11:00 -04:00
2ro 4a91998a44 goblin: settings polish (wrap fix, 5-dot censor, nav reset)
Follow-ups on the Settings redesign, all in the goblin view:

- Truncation sweep: settings_row_toggle now reserves room for the switch
  and bounds the text column, so a long label/subtitle WRAPS to another
  line instead of running under the switch and clipping. Fixes the
  Advanced Privacy notification descriptions (worst in longer locales);
  applies uniformly to every toggle row. The Username and Advanced Privacy
  blurbs are full-width labels that already wrap. No description on the new
  surfaces can truncate: wrapping is width-bounded, not length-tuned.

- Anonymous-mode censor is a FIXED count of dots (five), computed by a pure
  censored_amount_dots() that ignores the real amount, so the balance
  magnitude never leaks. Used for both the home balance hero and censored
  activity amounts. Unit-tested to never vary with size.

- Settings navigation resets to the root whenever the user is off the
  Settings (Me) tab, so leaving and re-entering always lands on the
  top-level page instead of the last sub-page. One reset at the leave/enter
  boundary via a pure settings_page_after(); deep links that open a
  sub-page set the tab to Me in the same frame, so they survive. Unit-tested.
2026-07-06 22:11:00 -04:00
2ro 6af8fd1c26 goblin: Settings redesign (Username + Advanced Privacy pages)
Two new Settings pages, each the single home for its feature.

Username (SettingsPage::Username) consolidates everything name-related
off the main Settings and out of the identity card: claim a name if you
have none, release the one you own, and choose the name authority from a
known list OR a free-typed custom server. Reuses the existing claim_ui and
claim/release worker flow; the old inline name-authority editor and the
main-settings claim card are removed (single home).

Advanced Privacy (SettingsPage::AdvancedPrivacy) replaces the lone
"hide amounts" toggle. Notifications section: hide amounts (existing),
hide names, or hide all details (a generic localized private alert that
leaks no name or amount; an empty amount collapses the Android
notification template to just the private line). Anonymous mode: one
toggle that censors STRICTLY the home balance (dots, tap to reveal; the
fiat rate fetch stays off until revealed) and the activity list (dots for
names and amounts, tap a row to reveal and open). Presentation only, with
no money-path or storage change.

Migration: the existing hide_amounts config keeps its exact meaning
(notification amount hiding); the three new fields default off, so no
surprise on upgrade. Covered by a new config test (227 lib tests).

All new strings via t!() in all six locales; drift test green.
2026-07-06 22:11:00 -04:00
2ro f7178254e0 goblin: complete the flow BEFORE return-to-caller + terse trust prompt
Root cause of the broken build 153 QR trust login, confirmed in code:
cb.return_to_caller() fired in the sign handler right after spawning the
callback POST worker. The app backgrounded immediately, the GUI frame pump
stopped, and the rest of the grant lives on that pump: the outcome poll that
consumes the POST result, the session creation, and the session-open channel
publish (which the service loop only sends on its next tick, after
add_session marks the set dirty). Backgrounded, none of that ran, so the
site never received the session and the browser poll never logged in.

The ordering fix, uniform across login, authorize, and trust:
- the sign handlers no longer return-to-caller; the app stays foreground
  (frames pumping) until the flow is fully complete
- login/authorize: the return decision happens in the outcome poll, only on
  a successful POST; a failed POST keeps the user in the wallet with the
  honest toast
- trust: the decision waits further, for the session-open announce to be
  CONFIRMED handed to a relay. announce_new_sessions now records per-channel
  delivery (at least one relay accepted) in NostrService::session_announced;
  the new TrustWait GUI state polls it with a 15s deadline and an honest
  "announce unconfirmed" toast on timeout (goblin.trust.announce_failed x6)
- a freshness bound on all three: a result consumed on a late resume (the
  user backgrounded the wallet themselves mid-flow) shows the toast but
  never bounces them back out
- the decision itself is the pure authuri::should_return_to_caller(allowed,
  post_ok, publish_confirmed), unit-tested: pending, failed, or unconfirmed
  work never returns; rt=0 never returns

Trust prompt copy, owner directive: the gist in one or two lines, detail
tucked away. The modal now leads with a single plain line (act-for-you plus
the money rule) and folds categories, money line, and duration behind the
existing show-full disclosure idiom; the red caution lines stay visible.
goblin.trust.* keys tightened in all six locales, drift test green.
2026-07-06 22:11:00 -04:00
2ro 1f224107a0 goblin: gate return-to-caller on an optional rt flag (QR scans stay in-wallet)
The Build 151 return-to-caller is right for same-device deep-links but wrong
for a QR scanned from another screen: there is no caller, so the wallet bounced
the user into whatever app was previous. The login/authorize/trust URIs now
carry an optional rt flag: rt=0 disables the return (QR case), rt=1 or an absent
flag keeps the return (backward compatible). Parsing is byte-exact and fails
closed to the return-enabled default on empty, garbage, or duplicate values
(first occurrence wins, like every other param). Security is unchanged: the flag
is a single boolean gate on the existing best-effort app-switch and derives no
target, so it can never introduce a redirect the URI could not already perform;
cb-host==domain binding and every other check are untouched.

- shared parse_return_flag helper in authuri, reused by login and trust
- return_to_caller field on LoginUri, AuthorizeUri, TrustUri (default true)
- all three GUI flows (login, authorize, trust) honour the flag uniformly
- parser unit tests: absent/explicit/garbage/duplicate + the no-return decision
2026-07-06 22:11:00 -04:00
2ro d24a09074a goblin: polish FR stale label + fix raw-key leaks on this branch
FR wallets.tx_stale was 'En attente trop longtemps' (noun + adverb, not
grammatical); now 'En attente trop longue', adjective agreeing with attente.
Other five locales for tx_stale / receipt.stale_note read natural, left as-is.

Also carries the two raw-key leak fixes (goblin.receipt.copied ->
goblin.receive.copied, wallets.canceled -> wallets.tx_canceled) and the
every_t_call_site_key_exists_in_en guard so this branch is clean standalone.
2026-07-06 22:09:05 -04:00
2ro 062cf2f2ab goblin: always offer a manual Cancel on stuck pendings + soft stale nudge
Some pending transactions showed no way to cancel or clear them, stranding
users with stuck pendings (fee-bug leftovers, and payments orphaned after an
identity switch whose nostr meta lives in another identity's store).

Root causes:
- Goblin receipt: both cancel affordances required the payment's nostr tx_meta,
  which is per-identity. A tx orphaned by an identity switch (or left by an old
  build) has no meta in the active store, so neither Cancel button rendered.
- GRIM tx list + tx detail: the Cancel button was gated behind
  wallet.synced_from_node(), so an unreachable/unsynced node hid it entirely.

Fixes (cancellation is unchanged: the plain libwallet cancel_tx that unlocks the
reserved inputs, GRIM parity, no new money behavior):
- Receipt: universal fallback Cancel for any still-cancellable pending the
  nostr-aware paths miss, running WalletTask::Cancel. ReceiptDetail carries
  can_cancel/stale.
- GRIM list/detail: ungate the local Cancel from node sync (only repost/repeat
  still needs a live height).
- Soft "stale" nudge on pendings older than 24h (WalletTx::stale/STALE_AGE_SECS)
  with the Cancel right there. It only colours a label; nothing auto-cancels on
  a timer (a slow interactive send may still complete).
- New tx_stale / receipt.stale_note strings across all six locales.
- Unit tests for can_cancel state coverage and the stale threshold.
2026-07-06 22:09:05 -04:00
2ro 2790d9a592 goblin: UI pack 153 (portrait lock, home logo, GoblinPay badge, plain QR)
1. Lock Android to portrait (screenOrientation="portrait" on MainActivity).
   Phone-first GRIM-style wallet; no cheap phone/tablet split so the whole
   Android app is locked, matching the single phone surface.
2. Enlarge the mobile Home header wordmark: mark 24 to 36 (+50%), "goblin"
   text 18 to 26, gap 8 to 9, so the top-left lockup carries the same
   visual weight as the 40 to 44px right cluster (owner asked for a
   noticeably bigger bump than the first +25% pass).
3. Swap the Pay-screen inline mark for the official GoblinPay lockup: the
   black Apple-Pay-style badge on light surfaces, the white wordmark on
   dark (theme dark_base split). New img assets from the GoblinPay repo.
4. Plain GoblinPay/receive QR: drop the yellow center-mark overlay from
   w::qr_code (mirrors the prior "plain QR default" decision). Test
   goblin_receive_qr_decodes updated to the plain render.
2026-07-06 22:09:05 -04:00
2ro c6dbec5109 goblin: in-flight request card becomes a single centered spinner
After approve + hold-to-accept, the pending request card no longer keeps
the Decline button and amount alongside a half-width spinner. While the
payment is in flight the whole card is replaced by one centered spinner
labelled with the action, sitting exactly where the card was, and it
vanishes once the send completes and the request clears.
build153
2026-07-06 19:46:59 -04:00
2ro fdbcbf0bdd goblin: audit fixes (money replay guard, honest encrypt prompt) + hardening
P2-1: a money-tier request id is reserved the moment its prompt is raised
(money_pending_ids); a replayed envelope (drain overlapping the live
subscription, or deliberate) returns AlreadyPending / the cached result and
can never raise a second prompt or re-sign. Applies to sign and escalated
encrypt. Regression test covers both replay-while-pending and replay-after-
completion.

P2-3: the money modal for a pay-committing encrypt now shows the inspected
plaintext (escaped + truncated exactly like the sign path), so the user sees
what they are agreeing to pay, not a blind label.

Hardening:
- payment marker set widened (amount, total, price, msat), TODO(audit) kept.
- identity dropped mid-session now answers identity_mismatch on the channel
  (refusal_json per op type) instead of silently timing out the site.
- session-end payload carries reason: revoked (wallet end) / expired (TTL or
  idle sweep, newly published by the tick) / logout (site's own).
- decrypt gets its own soft-cap window (30/min) and an honest 'reading your
  messages' notice, distinct from the signing-volume toast (new locale key
  in all six languages).
- money answers route by the session's site channel key, never the display
  domain.
- committed the auditor's env-gated NIP-44 v2 interop test against
  nostr-tools (GOBLIN_CT1 harness).
2026-07-06 19:46:59 -04:00
2ro 2f52df509e goblin: honour the trust relay hint on the channel + clippy clean
- The session channel now subscribes and publishes on the wallet's relays
  UNION each session's relay hint (channel_relays), dialing any hinted relay
  via the idempotent connect_relays, so wallet and site meet even without a
  shared default relay.
- Clean up clippy findings in the new code (abs_diff, collapsible ifs via
  let-chains, char-array find).
2026-07-06 19:46:59 -04:00
2ro 5da92d0147 goblin: Authorize Sessions GUI (trust grant, money prompt, Trusted Sites) + 6 locales
- Trust-grant modal: password-gated hold-to-confirm 'Trust <domain>?' showing
  identity + truncated npub, the granted low-tier categories (human labels,
  caution lines for unknown/login-excluded kinds), and the fixed money-asks
  line. Folds login in; creates the session on login-POST success.
- Money-tier per-action modal: the v1-authorize-shaped password + hold-to-confirm
  prompt raised over the channel for a sign or a pay-committing encrypt; declines
  on cancel/timeout.
- Trusted Sites settings page: active sessions, what each signs silently, time
  left, pause/resume, and one-tap end (revocation). Row + count in Settings.
- Dispatch + per-frame router for trust/money; PENDING_TRUST wired.
- goblin.trust.*, goblin.money.*, goblin.trusted_sites.*, settings.trusted_sites
  in all six locales; drift test green.
2026-07-06 19:46:59 -04:00
2ro c178f84bf0 goblin: fold cross-worker seam rulings (encrypt/decrypt ops, session-open, 30402 money)
- Add channel encrypt/decrypt ops (identity-key NIP-44) for magick's order-DM
  seal path: EncryptRequest/DecryptRequest, serve_encrypt/serve_decrypt,
  ChannelOp, generalized complete_money. Both low tier + rate-limited; an
  encrypt whose plaintext commits payment escalates to the money prompt;
  decrypt (the DM-read risk) is rate-limited and flagged for the security pass.
- session-open payload aligned to {type,id,identity_pubkey}; sent alongside
  the unchanged 22242 server callback.
- Error codes aligned to the shared set (Refused -> kind_not_in_session).
- Owner ruling: kind 30402 (listing) is money tier, stripped from the silent
  set like 17/22242; spec tier table + category map + tests updated.
2026-07-06 19:46:59 -04:00
2ro fbc6599ab0 goblin: Authorize Sessions transport + runtime (encrypted relay channel, session store)
- session.rs: channel-event builders (session-open / session-end / wrapped
  sign_result), decrypt, SessionSummary + PendingMoney, and the serve/
  complete_money runtime orchestration (with unit tests).
- client.rs: in-memory session store on NostrService (add/end/resume,
  summaries, money-prompt queue + answers, high-volume notice); channel
  subscription over the sessions' wallet channel keys, handle_channel event
  serving, session-open announce, drain-on-resume via the frame heartbeat,
  and money-answer draining wired into the service loop.
- lib.rs: PENDING_TRUST crate-root stash (set/take).
- dispatch: goblin:trust routed after authorize, before pay, in both the
  deep-link and camera-scan dispatchers.
2026-07-06 19:46:59 -04:00
2ro 939d1b18d1 goblin: Authorize Sessions pure core (trust URI parser, tier classifier, session enforcement)
Adds the testable protocol core for the two-tier session signer:
- trusturi.rs: goblin:trust URI parser, fail-closed like login/authorize,
  with sk (site channel key), r (relay hint), k (kind set) fields.
- session.rs: wallet-owned tier classifier (kind table + content
  escalation for flagged conversation kinds), kind-set sanitation, the
  kind-to-category display mapping, NIP-44 channel envelope shapes,
  client-pinned created_at signer with skew guard, and per-session
  enforcement (identity binding, replay dedup, size caps, rate limiting,
  TTL/idle lifetime).

30 new unit tests; full lib suite green.
2026-07-06 19:46:59 -04:00
2ro 4d45a330dc goblin: reserve the network fee in the send flow (GRIM parity)
The live Goblin send UI guarded only on amount > spendable, so a send of
the whole balance was offered as valid with no room for the network fee.
The wallet then built a slate needing amount + fee but holding only amount,
so the send failed with NotEnoughFunds or sat unconfirmed.

GRIM's send modal reserves the fee: it refuses amount + fee > max and caps
its max at spendable - fee. Mirror that here. Both the amount screen and the
review page now price the exact amount (one CalculateFee per amount, as GRIM
does) and refuse the send unless amount + fee fits the spendable balance;
hold-to-send stays disabled until the fee is priced, so a full-balance
payment is never dispatched with no room for its fee.

Adds amount_fits / max_sendable helpers with unit tests covering the
send-max boundary.
build152
2026-07-06 16:14:19 -04:00
2ro b51aac1e46 goblin: mark deferred audit items (M5, M6, L2, L3, L4) with TODO tags
Comments only, no behavior change: tag the five deferred audit findings at the
code they refer to so they are findable later. M5 (widen display escaping to
category-based) and M6 (template echo vs re-serialization) in authuri; L2
(redact secrets from Debug) at NostrIdentity; L3 (carry secrets as ZeroingString
end-to-end) at get_nostr_nsec; L4 (reset AdvancedState on back) in the goblin
on_back handler.
2026-07-06 08:09:40 -04:00
2ro ef4a7a5d19 goblin: auto-clear a revealed nsec from the clipboard after a delay
Copying a revealed nsec (or a post-rotation new key) left the secret sitting in
the clipboard indefinitely. Add copy_secret_to_buffer: it copies now and, after
a short delay, clears the clipboard only if it still holds exactly that secret
(compare-then-clear), so whatever the user copied since is never clobbered. Both
nsec copy sites now use it. Desktop clears through the long-lived clipboard owner
on a background thread; Android does the same over its JNI clipboard via the
established non-GUI app-handle pattern; other platforms fall back to a plain copy.
2026-07-06 08:09:40 -04:00
2ro 20e21d0364 goblin: make password-change identity re-encryption all-or-nothing and rebind the live service
reencrypt_all was best-effort per file: a partial failure left some identities
on the old password and some on the new, a split the user cannot open with one
password. Make it all-or-nothing: stage every re-encryption to a sibling temp
file and only commit (rename into place) once every identity re-encrypts
cleanly; any phase-1 failure deletes the temps and leaves every identity on the
old password.

Surface the failure: change_password now returns a hard error when the
re-encryption fails instead of logging and reporting success.

Keep the running NostrService in sync after a successful change by refreshing
its in-memory identity blobs to the new password, so same-session gated ops
(which re-unlock with the new password) work without a wallet reopen. The
decrypted keys are unchanged by a password change, so listening and sending are
untouched. Adds an all-or-nothing test.
2026-07-06 08:09:15 -04:00
2ro ab029b2647 goblin: bind the login callback host to the approved domain
The login URI parser checked the callback scheme but not that its host belongs
to the displayed domain, so a site could show d=magick.market while pointing cb
at its own host and harvest a signed login event for a domain it does not
control. Apply the same domain binding the authorize URI already enforces:
share authuri's domain_bound as pub(crate) and reject a login URI whose callback
host is neither the domain nor a label-boundary subdomain of it. The
http://localhost dev callback stays exempt. Adds binding tests.
2026-07-06 08:08:39 -04:00
2ro 436674e08a goblin: forward-compatible parsing for proof-address and held-identity registries
Add serde field defaults to every field of ProofAddrRegistry, HeldIdentities,
and HeldEntry so a file written by a different build parses instead of failing:
an unknown extra field is ignored and a dropped non-essential field falls back
to its default. The counter and version defaults are supplied by helper
functions so next defaults to 1 (never 0, which is the app's default address)
and ver to 1. This is parse-side only; the on-disk write path is unchanged.
Adds forward-compat tests for both registries.
2026-07-06 08:08:39 -04:00
2ro d804d2e126 goblin: serialize proof-address allocation, atomic persist, refuse corrupt registry
The per-sale proof-address allocator did an unlocked read-modify-write of the
persisted counter, so two concurrent mints could read the same next index and
hand out the same derivation index, an address reuse that mis-addresses a
payment proof. Serialize the whole load-bump-persist behind a process-wide
lock so every index is unique.

Persist the counter atomically: write a sibling temp file in the same
directory, flush, then rename over the target, so a crash mid-write cannot
leave a torn file.

Refuse a corrupt registry: a file that exists but fails to read or parse now
returns an error instead of silently resetting the counter to 1 (which would
re-hand-out already-minted indices and reuse an address). Only a missing file
defaults to the fresh-wallet counter. Adds a concurrency test asserting unique
indices across racing threads, plus missing-vs-corrupt coverage.
2026-07-06 08:08:39 -04:00
2ro d4cc7dbe98 goblin: Authorize, one-shot site-requested event signing with per-event approval (Build 151)
goblin:authorize?e=<base64url template>&d=<domain>&cb=<https callback>&c=<64-hex nonce>
(and the nostr: QR twin) asks the wallet to sign exactly one Nostr event.
Fail-closed parser beside loginuri: strict three-key template (kind, content,
tags; any pubkey/created_at/id/sig injection rejects), kind allowlist 1/6/7/30023
(22242 only via login), 4096/2048 size caps, unpadded base64url only, delegation
tags rejected, and the new cb-host-must-belong-to-d binding. Approval modal
mirrors the 150 login machinery: Authorize <domain>? verb, plain-language kind
rendering, 240-char escaped preview with truncation marker and show-full view,
identity picker, wallet password gate, single pending request, 120 s expiry,
single-use. Wallet alone sets pubkey/created_at/id/sig, NIP-01 canonical only;
delivery POSTs {c, d, event} to cb with the shared HttpClient and 15 s timeout,
consumed regardless of outcome, quiet toasts. Dispatchers check authorize before
pay on both the deep-link and QR paths. goblin.authorize.* strings in all six
locales; 29 new authuri tests.
2026-07-06 07:34:03 -04:00
2ro 4b0071fa91 goblin: add-identity sheet gets real Generate/Import buttons, Cancel beneath (Build 150) 2026-07-06 03:27:56 -04:00
2ro b7adae89c7 goblin: sign in with Goblin, one-time login approval + 22242 signer (Build 150) 2026-07-06 03:24:09 -04:00
2ro 3c09baf25a goblin: batch invoice URI with one approval, per-sale addresses (Build 149)
URI schema: pay/invoice URIs gain an optional `count=N` parameter
(goblin:<npub|nprofile>?amount=X&count=N[&memo=...]) asking the wallet to
issue N payment requests in one go. Default 1 when absent (single flow
unchanged), capped at MAX_BATCH_COUNT = 20, fail-closed to 1 on zero/garbage,
first occurrence wins — all payuri conventions, unit-tested.

A count>=2 deep link whose recipient is a direct key (npub/nprofile) with an
amount opens ONE batch-approval modal — "Approve N invoices", with the payer,
per-invoice amount, total, and memo — instead of the send flow. On approve,
the wallet mints N fresh per-sale proof addresses (Wallet::mint_proof_address;
a mint failure stops the batch, already-issued requests stand) and fires the
same NostrRequest task the single flow uses, once per invoice; the requests
appear in activity as they dispatch. Anything else (count on a name that
needs discovery, or no amount) degrades to the single flow with count
ignored. The minted address is recorded on each request's metadata
(TxNostrMeta::proof_address, serde-default None) so the per-sale machinery
can read it; WalletTask::NostrRequest gains the optional address and the
single flow passes None. New goblin.batch.* strings in all six locales.
2026-07-06 02:03:00 -04:00
2ro ea07fd2081 goblin: per-sale proof-address allocator + mint API (Build 149)
App side of the index-0 unpinning. Wallet::mint_proof_address allocates the
next fresh derivation index from a persisted, never-reusing counter
(src/wallet/proof_addrs.rs; proof_addrs.json in the wallet dir — an index
carries no secret, the keys live in the wallet seed) and derives its
slatepack/proof address through the wallet's existing indexed derivation
(address_from_derivation_path), exactly as the app's default index-0 address
is derived. Index 0 stays the default app address; nothing changes for normal
receives. Allocation starts at 1, is monotonic and persisted before use (a
crash burns an index, never reuses one), and is capped at
MAX_PROOF_ADDRESS_INDEX = 1023 — the receive-side scan bound of the matching
grin-wallet submodule patch (kept as a local constant so this crate also
builds against the unpatched upstream submodule; the wallet-side patch, which
detects the addressed index on receive and signs the payment proof with the
matching key, lives in the wallet submodule on a local branch pending the
owner's decision on where that patch is pinned).

Tests: allocation monotonic + persistent from 1, refuses past the scan bound,
corrupt counter degrades to address reuse (the old single-address world),
never a fund risk.
2026-07-06 01:56:06 -04:00
2ro c0cdeb6d6b goblin: back-hint polish — switcher wording, quiet-toast look (Build 148)
The first-back pill now reads "Press back again to switch wallets"
(re-translated in all six locales) and is styled as a native quiet toast: a
solid soft pill with no border, small regular dim text — no accent colors,
nothing loud. No haptic on the back press (owner's call). Behavior unchanged.
2026-07-06 00:45:54 -04:00
2ro e9f3bd453b goblin: double-back at Home returns to the wallet switcher (Build 148)
At the wallet Home (the top level, where back was silently swallowed), the
first Android back now shows a brief non-blocking pill — "Press back again to
leave this wallet" — and a second back within two seconds DESELECTS the wallet
to the switcher. The wallet stays UNLOCKED (select(None)-style deselection, no
close, no logout), so returning is one tap and its nostr service keeps running
untouched. Quitting the app lives only at the switcher, via GRIM's existing
native exit-confirmation on back (unchanged). In-wallet sub-screens keep
normal back navigation, no hint. The second back never quits directly.

The hint is a bottom-anchored fading pill above the tab bar (goblin view),
armed by the host (WalletsContent::back_exit_at) in the swallowed-no-op
branch; nothing else about back handling changes. New goblin.home.back_again
string in all six locales; drift green.
2026-07-06 00:34:47 -04:00
2ro 9b1cec2063 goblin: min-confirmations setting + integrated node moved to Advanced (148)
MIN CONFIRMATIONS (GRIM parity): the wallet group in Settings gains a
"Minimum confirmations" row just below the Node row, showing the current value
(default 10) and opening a numeric edit modal ported from GRIM's
min_conf_modal_ui (same prompt/error strings, Cancel/Save, CenterTop). The
fork already carried the full backend — WalletConfig::min_confirmations with
MIN_CONFIRMATIONS_DEFAULT=10 and Wallet::update_min_confirmations — and the
value already feeds the spendable/send logic (retrieve_summary_info, send and
receive params) plus the GUI confirmation counts via
data.info.minimum_confirmations, so this only exposes the existing knob.

INTEGRATED NODE -> ADVANCED: the integrated-node tabs (info, metrics, mining
with stratum, node settings) leave main Settings — Goblin is the lighter
client. Their ONE home is now Settings -> Advanced, as a second action on the
"Run your own node" card (live sync status in the label when the node runs),
with back returning to Advanced. The duplicate entry on the node-connection
page is removed too, per the single-home rule; all functionality is kept.
2026-07-06 00:07:21 -04:00
2ro c3d6c31aca goblin: one-toggle add-identity sheet, kill the dead Generate button (Build 148)
The add sheet's "Generate new" was a mode toggle that set import=false — the
mode the sheet already opens in — so it read as a completely dead button, and
the four-button layout (Generate new / Import / Cancel / Add) mixed two silent
mode toggles with two actions.

The sheet now DEFAULTS to generate mode with a one-line note saying a fresh
anonymous key will be created; the standalone "Generate new" button is gone.
One clear toggle sits above the action row and always names the OTHER mode:
"Import an existing identity instead" switches to import mode (revealing the
.backup picker and nsec paste inputs), where it becomes "Create a new key
instead" to switch back. The action row stays exactly Cancel + Add — Add
creates a fresh identity in generate mode and imports in import mode — keeping
the 147 modal-lock, uniform-button and CenterTop conventions. The old
generate/import toggle labels are removed from all six locales and the new
generate_note / import_instead / generate_instead strings added; drift green.
2026-07-05 23:52:59 -04:00
2ro 3237174c2e goblin: modal-lock the identity manage sheet, uniform paired buttons (Build 147)
MANAGE SHEET IS A TRUE MODAL: the pencil now opens the per-identity management
sheet through the GRIM Modal system (like the password modal), which dims and
locks the identity list behind it — switching and row taps are disabled until
it closes, killing the bug where the active identity could be switched while a
manage sheet was open for a different one. The modal is titled with the
identity it manages (display precedence: private tag, else bare name, else
truncated npub). The step-1 delete confirmation moved into a modal too (same
standard: a sheet acting on a specific item locks its background), also titled
with the identity, chaining into the wallet-password modal as before.

UNIFORM PAIRED BUTTONS: every paired action row in the identity sheets now
renders one consistent widget per row. In modals both halves are the GRIM
modal button (Cancel/Save; Cancel/Delete with Delete carrying only the red
color at the same size and shape). The in-page add panel's Cancel/Add row —
which mixed big_action_on_card with the 56px big_action inside 44px halves —
now uses big_action_on_card_ink for both, matching the Generate/Import toggle
(the reference pattern); the disabled Add state dims its ink. Audited the rest
of the new identity UI: the toggle row was already uniform and the remaining
buttons are full-width singles.
2026-07-05 23:07:50 -04:00
2ro 0c7a1404fa goblin: fix Settings scroll extent so the page reaches its foot (Android)
Root cause found by diffing against upstream GRIM, which does not have the
bug: GRIM gives every ScrollArea a distinct id_salt (e.g. "wallet_list_scroll",
"wallet_tab_content_scroll"); our goblin surface created fifteen unsalted
ScrollArea::vertical() — Settings Main and every settings sub-page render
theirs at the same position in the same parent Ui, so they all resolved to the
SAME egui scroll-state id. Scroll offset and remembered content extents
collided across pages, clamping the Settings list's maximum scroll to another
(shorter) page's extent: the page stopped scrolling before its real end and the
bottom items — including the wallet-management foot (Switch wallet / Lock /
Advanced), i.e. the way out — were unreachable on the phone. This one root
cause covers both user reports ("Settings bottom cut off" and "can't exit").

Every goblin ScrollArea now carries a unique id_salt, matching GRIM's
convention, so each page keeps its own scroll state and traverses to its true
bottom. Back-press behavior is deliberately untouched.
2026-07-05 22:07:12 -04:00
2ro d80218a13c goblin: plain-language identities blurb in all six locales
The top-of-page identities copy now says what identities ARE in end-user
terms — the names you get paid under, all paying into this one wallet — and
what you can do (add, switch, remove). No protocol jargon.
2026-07-05 21:59:17 -04:00
2ro 472e2f0915 goblin: private identity tags + per-row management sheet (Build 146)
PRIVATE TAG: a local, app-only name the user gives an identity. Stored as
NostrIdentity.private_tag in its 0600 identity file (serde-default; it also
rides inside the NIP-44-sealed .backup envelope, which serializes the whole
struct, and is restored on backup import). NEVER published — kind-0 metadata is
built only from the claimed NIP-05 name, untouched. Display precedence
everywhere an identity is shown (switcher rows, delete confirm, transaction-
detail attribution): private tag, else claimed name (bare, no @), else
truncated npub (HeldIdentitySummary::display).

ROW UX: identity rows show the label with a checkmark on the active one; TAP =
instant switch (unchanged). A small pencil affordance per row opens a
management sheet: Rename (set/clear the private tag; saves via
Wallet::rename_nostr_identity, which persists the file and updates the running
service in place — no password needed, the ncryptsec is untouched) on top, and
Delete at the bottom, visually separated and destructive-styled, feeding the
existing double gate (danger card, then the wallet-password modal). Delete only
renders while more than one identity is held. New strings in all six locales.
2026-07-05 21:58:44 -04:00