1
0
forked from GRIN/grim

1076 Commits

Author SHA1 Message Date
2ro ba5ddf07ac Build 51: fit the balance, center the send flow, fix the success logo
On-device review surfaced UI overflow/centering issues at phone width:

- Balance hero shrinks to fit: amount_text_centered_ink measures the text and
  scales the font down when it would run off the edge (e.g. 0.47520721ツ).
- Send flow sits closer to the screen edges (smaller side gutters) and is
  properly centered: the Review/Confirm hero now uses a centered, wrapping
  layout instead of a fragile manual offset that clipped a long npub and
  threw off the centering. info_row values truncate instead of overflowing.
- Recipient picker: the suggested row shows the full npub in the grey subtitle
  (truncated to fit) instead of repeating the same shortened npub as the title.
- Success screen uses the goblin head (goblin-logo2), not the mascot mask.

Verified live at 394px across balance, recipient, review, confirm and the
success screen.
2026-06-13 15:55:02 -04:00
2ro c05074faac Build 50: production Nym requester; correct sender amount/fee; Nym copy
- Bake the always-on us-ea.st network requester into NETWORK_REQUESTER so a
  shipped wallet routes through the mixnet out of the box (override with
  GOBLIN_NYM_PROVIDER). Verified e2e: real wallet-to-wallet payment across two
  mainnet nodes (grincoin.org → main.gri.mw), recipient received the exact
  amount, all traffic over Nym.
- Fix the sender's displayed amount: a sent tx debits inputs and credits change,
  so debited-minus-credited is amount PLUS fee. Subtract the fee so the activity
  feed, receipt and history show the value that reached the recipient (0.01),
  not the fee-inclusive net (0.033). The receipt's "Network fee" now reads the
  real kernel fee instead of the change.
- Replace the remaining user-facing "Tor" copy with Nym (send/request review
  delivery line, connection status chips, onboarding, the Privacy settings row).

38 lib tests pass.
2026-06-13 15:15:41 -04:00
2ro c4d26d3b7f Build 49: auto-expire stale transactions; de-dupe receipt counterparty
Pending transactions that never complete now auto-cancel/expire after a
configurable window (NostrConfig::expiry_secs, default 24h; lower it in
nostr.toml to test). A sync-loop sweep (NostrService::expire_stale):

- cancels stale outgoing sends + invoices we paid via GRIM's cancel_tx
  (WalletTask::Cancel), releasing the outputs they had locked;
- annotates incoming payments / invoices we issued as Cancelled only
  (a late on-chain confirmation still wins);
- marks pending incoming requests Expired.

The activity feed and receipt now render "canceled" (this also fixes a
latent bug where a manually-cancelled tx still showed "pending").

Receipt de-duplication: the To/From name rows are shown only when the
counterparty has a real identity (petname or verified NIP-05); a bare
npub now appears once, in the "nostr" row, instead of twice.

38 lib tests pass (2 new for the expiry bucket logic).
2026-06-13 14:31:14 -04:00
2ro 695c3e6d4f Build 48: replace Tor with the Nym mixnet; remove arti
Route every relay and HTTP request (nostr relays, NIP-05, price) through
a local nym-socks5-client sidecar on 127.0.0.1:1080, so all traffic
egresses via the 5-hop Nym mixnet and nothing touches the clear net.

- Add src/nym/: SOCKS5 HTTP client (reqwest socks5h), NymWebSocketTransport
  for the nostr relay pool (tokio-socks dial + TLS/ws handshake over the
  mixnet), and a sidecar launcher that reuses or spawns nym-socks5-client.
- Swap the nostr-sdk transport off ArtiWebSocketTransport; route nip05.rs
  and price.rs off Tor; revert the clearnet username-lookup shortcut.
- Remove the embedded arti Tor client wholesale: the onion-service
  listener and send-to-onion path in the wallet, the legacy transport
  GUI tab, the Tor settings page, src/tor/, the webtunnel pluggable
  transport (Go build + submodule), and all arti crates from Cargo.toml.

The Grin node connection is unchanged (chain data, no payment metadata,
and never used Tor). The network requester the sidecar routes through is
configured via GOBLIN_NYM_PROVIDER / NETWORK_REQUESTER at deploy time.
2026-06-13 13:29:21 -04:00
2ro dda07dee0a Build 47: username lookup over clearnet, off Tor
The read-only NIP-05 username lookups (resolve + check_availability) carry no
secret and need no anonymity — routing them over Tor is what made choosing a
username slow. They now use a fast clearnet HTTPS GET (reqwest + rustls/ring,
so it still cross-compiles to Android), falling back to Tor only if clearnet is
blocked. The anonymity-critical paths (slatepack delivery, NIP-98-signed ops)
are untouched.
2026-06-13 12:40:36 -04:00
2ro 1ac1186319 Docs: link NIPs/kinds on first mention; gitignore screenshots/
Every first mention of a NIP or nostr kind in the README now links to
nips.nostr.com (NIPs) or nostrbook.dev (kinds). Add screenshots/ to .gitignore
(local gallery captures for the site).
2026-06-13 06:35:55 -04:00
2ro 4cad00079e Build 45: receipts, activity grouping, profiles + block
Rich transaction receipt screen (tap an activity row, or the new Receipt button
on a send's success screen): counterparty, time, note, amount, and a
Transaction-details card joining GRIM's local metadata with the nostr
npub/username — status (Complete vs Pending N/min-conf), To/From, network fee,
Mimblewimble, and the slate id. A local archive, like GRIM.

Group the Activity feed into Pending (unconfirmed) + per-day sections.

Contact profile screen (tap a peer or the receipt counterparty): who they are,
the history between you, a Pay shortcut, and Block — a nostr-level mute that
drops their incoming payments/requests (Contact.blocked, checked in ingest).

Refine success/denied copy ("to/from" by direction; "ask them to send you grin
instead"). Make the profile avatar display-only — no custom-picture upload.
2026-06-13 06:29:57 -04:00
2ro 143f4230c9 Build 44: fix recurring macOS release build
release.yml's `files:` upload globs still used ${{ inputs.tag }}, which is empty
on a `release: published` event, so the auto-built macOS universal zip never
matched its filename and didn't attach — the job went green while uploading
nothing. Use the inputs.tag || release.tag_name fallback for the globs too.

Also fix the CI build number: building from the public single-commit squash
makes `git rev-list b51a46b..HEAD` fail (the fork base isn't an ancestor), so
build.rs fell back to "Build dev". build.rs now honours a GOBLIN_BUILD env
override and release.yml passes GOBLIN_BUILD=${TAG#build}, so CI artifacts carry
the real build number.
2026-06-13 05:44:47 -04:00
2ro f714e4498a Build 43: real payment requests + incoming-requests opt-out
Wire GRIM's receiver-initiated invoice (Invoice1) over nostr so the Pay-tab
"Request" button actually asks someone for money: pick a contact, issue an
Invoice1, DM it; they get the existing approve-to-pay card. New
WalletTask::NostrRequest mirrors NostrSend (issue_invoice + RequestedByUs /
AwaitingI2, reusing send_payment_dm); the send flow gains a request mode
(Request from -> Confirm request -> Send request -> Requested) with no balance
guard, since requesting isn't spending.

Incoming requests are now opt-out (Settings -> Requests, on by default): when
off, an incoming Invoice1 is dropped and the preference is advertised in the
kind-0 profile (goblin_accepts_requests) so a requester sees "Could not
request" before sending. Adds a Cash App-style toggle switch widget.

Also enlarge the center Pay puck in the floating nav, and make the macOS
release build recur: release.yml now triggers on release publish (macOS only,
since Linux/Windows/Android/AppImage are built locally).
2026-06-13 05:08:51 -04:00
2ro 2e8829ef83 Build 42: block over-balance sends + roomier search icon
Guard against paying more than the spendable balance at every entry: the Pay
tab, the send-flow amount step, and the Review screen all render the amount in
red with "You don't have enough grin" and refuse to proceed, so a payment that
would only fail later at the node is stopped up front. On Android the phone
gives a short error buzz on the blocked tap — new VIBRATE bridge
(PlatformCallbacks::vibrate_error -> MainActivity.vibrateError); no-op on
desktop. Grim silently clamped the amount and showed nothing.

Also enlarge and vertically center the magnifier in the "Send to" search field
(an 18px muted label became a 22px centered glyph).
2026-06-13 02:51:29 -04:00
2ro 9d74d6fac5 Build 40: roomier Pay keypad spacing (Cash App-style)
The amount keypad rows were bunched tight (56px keys, 4px gaps) high on the
Pay screen, leaving a large empty area at the bottom. Now: taller (58) and
wider (pad 332, 14px column gaps) keys, bigger digits (30), and adaptive
row spacing that spreads the four rows toward the bottom when there's room
(the Pay tab) while staying compact where there isn't (the send flow).
Clamped so it never overflows or stretches absurdly.
2026-06-13 01:06:34 -04:00
2ro 4d5db923ea docs: de-duplicate the nostr_password comment in Wallet::open
Audit cleanup — collapse the garbled double comment to one line. No code change.
2026-06-12 23:10:44 -04:00
2ro f210180de6 Build 39: match Grim's focus handling on text fields (.focus(false))
Grim never lets a field default to constant focus. Its TextEdit defaults
to focus = true, which means an unfocused field calls request_focus()
EVERY frame. Grim's own usage always overrides this — every field is
.focus(false) (focus only on tap), and at most one gets .focus(first_draw)
to grab focus once when a screen opens (see grim wallets/modals/add.rs).

Goblin's converted fields skipped this, so all ~16 fields requested focus
each frame and fought each other on multi-field screens (wallet setup,
import, rotate). Set every field to .focus(false) to match Grim exactly:
fields now focus on tap and bring up the native keyboard, with no thrash.

The native IME path itself is already identical to Grim (verified: edit.rs
differs only by additive display builders; MainActivity onTextInput JNI
plumbing is byte-for-byte Grim; android/winit/eframe/egui/jni deps match).
2026-06-12 22:31:54 -04:00
2ro 1b5352da0d Build 38: use the native Android keyboard like Grim (revert the virtual-keyboard mistake)
Build 37 wrongly forced Grim's on-screen virtual keyboard on Android (.soft_keyboard())
after the emulator's broken IME misled me into thinking the native keyboard was dead.
That brought up a virtual keyboard AND double-typed. Reverted:

- Removed .soft_keyboard() from every field; they now use Grim's TextEdit defaults
  (no_soft_keyboard = is_android()) → the user's native keyboard on Android, exactly
  like Grim, no virtual keyboard, single input.
- Reverted edit.rs IMEAllowed/on_soft_input to Grim's exact behavior; kept only the
  additive display builders (hint_text/text_color/body) the Goblin design needs.
- Manifest: removed windowSoftInputMode and the keyboard/keyboardHidden/navigation
  configChanges flags I'd added (Grim has none) so input matches Grim; kept the
  uiMode/density flags + isFinishing() guard for the dark-mode crash fix.

The app's input path now diffs from Grim only by the additive display builders.
(The Android emulator's IME is aborted regardless — a known emulator quirk — so the
native keyboard is validated on a real device, where Grim works.)
2026-06-12 22:14:49 -04:00
2ro 2cc023b905 Build 37: fix Android — typing, install-alongside-Grim, status bar, edge padding
- Typing (critical): the Goblin UI used plain egui::TextEdit, which never
  received input because Grim's native-keyboard path only feeds Grim's own
  TextEdit widget. The native IME is also aborted by the windowing system on
  this app. Fix: every Goblin field now uses Grim's TextEdit, opted into
  Grim's own on-screen keyboard on Android via a new .soft_keyboard() builder
  (no native-IME dependency). Additive TextEdit options (hint_text/text_color/
  body/soft_keyboard) leave Grim's defaults and existing call sites unchanged.
- Install alongside Grim: FileProvider authority mw.gri.android.fileprovider →
  st.goblin.wallet.fileprovider (Android rejects duplicate authorities).
- Status bar: app draws edge-to-edge, so dark icons vanished on the dark theme.
  New theme-aware JNI call sets light/dark status-bar icons per theme.
- Edge padding: centered_column now keeps an 18dp side gutter on phones instead
  of running content flush to the screen edge.

Validated on the API-36 emulator: typed into fields, padding gutter present.
2026-06-12 21:47:54 -04:00
2ro 993a438e18 Build 36: friendlier ツ on the Pay puck via a Gamja Flower subset
The center Pay puck rendered ツ through the Noto SC fallback (Geist has no
katakana) — a stiff, geometric glyph. Embed a 1.2 KB subset of Gamja Flower
(OFL) containing only ツ and use it solely at that one widget; its ツ is the
cute winking-smiley shape. Every other ツ (balances, title, activity) is
untouched.
2026-06-12 18:15:16 -04:00
2ro b1c3c07dac Build 35: lead Tor bootstrap with the maintained bridge solo
The default webtunnel list bundles wt.gri.mw with public bridges that rot;
bundling them means arti can fixate on a dead/zombie bridge (front up, tunnel
dead) and burn the whole 120s bootstrap timeout — observed live on Android as
'stuck at 15%, had to reset bootstrapping too many times' (48 attempts on a
zombie, 1 on the good bridge). Now the first attempt uses the maintained
default alone; the public bridges stay as fallback in pairs only if it's
unreachable. Verified on the API-36 emulator: Tor reaches 100% over wt.gri.mw,
zero attempts on the dead bridge.
2026-06-12 17:44:04 -04:00
2ro 878f7728eb Build 34: harden the nostr ingest path (audit Medium items)
- N-F1: commit the wrap/rumor/slate dedup markers immediately after the
  durable receive/finalize, before the reply+sync tail, so a crash there
  can't re-trigger the action on catch-up. (grin + decide() already
  backstopped it; this closes the window cleanly.)
- N-F3: prune the processed-dedup store hourly in the heartbeat, not only
  at startup — a long-lived session could otherwise grow it unbounded
  under fresh-keypair spam since the 30-day TTL never re-applied.
- N-F2: kept Created/SendFailed in the finalize allow-set (removing them
  would strand a real send whose S1 reached the peer before we persisted
  AwaitingS2) and documented why it is not a forgery vector; added a test.
- Update replay_check e2e: a same-pubkey second register is now blocked by
  the name-change cooldown (fires before the one-name rule); accept either.

Validated: 35 lib tests + live nip17_slatepack_roundtrip + replay_check green.
2026-06-12 12:48:34 -04:00
2ro 60e4e8b5a9 Build 33: security audit fixes — remove the clearnet bridge probe, cap untrusted responses
- SECURITY (High): drop sort_bridges_by_reachability / bridge_probe_addr. The
  Build 30 probe did clearnet DNS + direct TCP to bridge endpoints outside Tor
  on every startup, deanonymizing bridge users. The consensus-cache keep + 120s
  timeout + pre-warm remain and are the real fix for slow first connect.
- SECURITY (Low): cap HTTP response bodies from the untrusted goblin.st server
  at 2 MiB, streamed so a lying/absent Content-Length can't OOM the wallet.
2026-06-12 12:18:57 -04:00
2ro b9ce88e996 Build 32: federate identity + sharpen identity chrome
- Accept any NIP-05 domain in the send flow (user@domain resolves and pays;
  foreign handles display with their domain and hit the unverified-key gate)
- Share and scan nprofile (npub + relay hints) so a recipient is reachable
  with no registry/indexer lookup; hints ride the DM send path
- Receive QR, copy buttons and settings row now emit nostr ID (nprofile)
- Sidebar identity chip truncates long handles on one line (was wrapping)
- Crisper small goblin mark via a 2x raster at chip sizes
- Map the server's name-change cooldown to friendly copy in claim/release
2026-06-12 11:54:35 -04:00
2ro 60414e9477 Build 31: survive Android configuration changes — the activity-recreate path killed the whole process (crash on open with forced dark mode); widen configChanges, gate process teardown on isFinishing 2026-06-12 11:02:54 -04:00
2ro 7eefb54075 Build 30: Tor up in seconds — probe bridges, keep consensus cache, pre-warm at start; borderless window frame, vector sidebar mark, Wayland app id, v2 icon set 2026-06-12 02:51:00 -04:00
2ro cda1be992f Build 29: standardize the AI credit line in the README 2026-06-12 02:00:01 -04:00
2ro 9257f82dcf Build 28: drop internal dev docs and upstream Forgejo CI from the repo 2026-06-12 01:47:37 -04:00
2ro 7f09598298 Build 27: let the Android app build without upstream's private maven mirror 2026-06-12 01:23:29 -04:00
2ro 03741f7839 Build 26: replace upstream release CI with native-runner workflow for GitHub 2026-06-12 01:23:29 -04:00
Claude 78f629d8d3 Build 25: default to a steadier instant node + add the Goblin node
main.gri.mw has intermittent issues, so the default external ("instant") node
is now grincoin.org — both the onboarding first-connect and the head of the
external-node list. Adds the Goblin-run node https://main.us-ea.st as a default
option. (Relays already default to ours, nrelay.us-ea.st, plus relay.damus.io
and nos.lol — no change needed.)
2026-06-11 23:48:31 -04:00
Claude 68d72fa853 Build 24: equal-size buttons in the release-username confirm gate
"Keep it" used big_action_on_card (56px / 17px) while "Release it" used
big_action_on_card_ink (44px / 15px), so the two confirm buttons were
visibly different sizes. Both now render with big_action_on_card_ink at the
intended 44px (matching the layout's scope), differing only in text color.
2026-06-11 23:41:38 -04:00
Claude 95403516d5 Build 23: README rewrite + banner — describe Goblin, not Grim
Replace the inherited Grim README with one that reflects what Goblin actually
is: a private, Cash App-style GRIN wallet that pays @usernames via NIP-17
gift-wrapped DMs over Tor, with in-app nostr identity and the goblin.st
identity service. Adds the Goblin banner, a payment-flow diagram, correct
build instructions (binary is `goblin`), and keeps the Claude credit.
2026-06-11 23:26:11 -04:00
Claude f2402eb24d Build 22: security hardening follow-ups
From the audit's deferred P2 list:
- Global gift-wrap decrypt ceiling (~120/min across all senders) so
  fresh-keypair spam can't force unbounded NIP-44 decrypts. The per-sender
  limit only applied after the decrypt revealed the sender; this caps total
  decrypt work up front.
- NostrStore reads tolerate a poisoned lock (unwrap_or_else into_inner) so a
  single panic can't cascade into taking down all nostr storage for the session.
- Avatar upload/delete reuse the service's keys directly instead of
  round-tripping the secret through a plaintext nsec String.

34 lib tests green.
2026-06-11 23:23:26 -04:00
Claude 413746dde3 Build 21: harden the client — NIP-05 gate, hostname validation, avatar limits
From a security audit of our own nostr/identity code (no P0/P1 found; these
close the P2 hardening gaps):
- NIP-05: only goblin.st identities skip the "pay an unverified key?" gate.
  A third-party domain's well-known could point at any key, so those now route
  through the same confirm gate as a bare npub.
- NIP-05: validate the domain as a bare hostname before building the
  well-known URL — closes a path/host-smuggling (SSRF-over-Tor) vector.
- Avatars: decode server-fed bytes under explicit image Limits (<=1024 px,
  8 MiB) so a hostile or breached avatar host can't exhaust memory on the
  texture path.

34 lib tests green (incl. new hostname-rejection cases).
2026-06-11 22:55:09 -04:00
Claude c3b23dc1a7 Build 20: README credit, onboarding copy, centered balance hero
- README: credit Claude (Anthropic) for Goblin's development at the bottom,
  in place of per-commit co-authorship.
- Onboarding: replace the contradictory "Pay people, not addresses" card with
  "Send like a message" and reword the body in plain language (no more
  "plumbing"/"ciphertext"). The identity step now says you can rotate your key
  anytime to maintain your privacy.
- Home: center the balance hero so it lines up with the Pay amount and the
  empty-state below it.
2026-06-11 22:55:09 -04:00
Claude 15c19303ff Build 19: open-wallet shortcuts, Pay QR scan, configurable pairing, hide yellow
- Lower-left sidebar cards are now individual shortcuts: tapping the
  identity chip opens identity settings, tapping the node card jumps
  straight to the Node menu (was: both opened generic settings).
- Pay screen gains a scan-to-pay QR puck top-right that opens the camera
  and prefills the recipient while keeping the typed amount (reuses the
  Home scan + SendFlow::request_scan/prefill_amount path).
- Replace the USD-only "fiat preview" with a configurable "Pairing":
  Off / USD / EUR / GBP / JPY / CNY / Bitcoin / Sats (default USD). price.rs
  now fetches GRIN against any vs_currency (sats price vs btc, ×1e8) and
  caches per code; a Settings → Pairing sub-page picks it; the Pay, send,
  and balance previews all route through one pairing_preview() helper.
- Hide the Yellow theme from the picker (cycle is Dark ↔ Light now); the
  ThemeKind::Yellow tokens stay defined — it's beta, not removed.

Verified live on :2 (Default wallet): node card → Node menu; identity chip
→ identity settings; QR puck renders top-right of Pay; Pairing → Sats shows
"≈ 1,912 sats" for 50ツ over a live BTC rate, resets to USD; theme cycles
Dark↔Light, never Yellow. 34 lib tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:22:39 -04:00
Claude 0438d70cae Build 18: move the integrated node off the wallet list into the cog
- Remove the Node status chip from the wallet-list header; the integrated
  node now lives in the cog's settings as a status + Enable/Autorun section,
  with a "Node settings" button into the full stats/mining/tuning panel.
- Stop the desktop two-column rail from auto-docking the node beside the
  settings screen, so the node has a single home in the cog at every width
  (the full panel opens only on demand). Fixes the wide-window double-exposure.
- Prove NIP-17 payments transit the top public relays: parameterize the
  nostr e2e roundtrip and add damus.io + nos.lol proofs (3/3 green).
- Add DEVELOPING.md documenting how we iterate and test (Xwayland :2 recipe,
  Build N cadence, gui-sweep, e2e tests, live infra) — no secrets.

Verified live on :2: chip gone from the list; cog shows the node once
(single column, no left rail); "Node settings" opens the full panel on
demand. 34 lib tests green; nip17 roundtrip green over nrelay + damus + nos.lol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:21:13 -04:00
Claude 86f042facb Build 17: elastic recipient search w/ nostr verification, numpad, copy
Recipient picker (send.rs) is now type-ahead instead of button-driven:
- as you type, instant local-contact matches + a debounced network
  lookup surface as tappable cards (no more "Find recipient" button)
- a name or @handle is treated as a goblin username (nip05 resolve) and
  shown as a tappable card — tap the right identity to select it
- a pasted npub/hex is verified over nostr: fetch its kind-0 profile and
  badge the card "✓ on nostr"; a valid key with no profile shows "no
  profile" and, when tapped, asks "Pay an unverified key?" before
  continuing (owner choice — never hard-blocked)

NostrService::fetch_profile_blocking (client.rs) — one-shot kind-0 fetch
over the connected relay pool (nrelay + damus + nos.lol), so arbitrary
npubs resolve across the public network. data::search_contacts for the
local half.

Numpad (widgets.rs): centered, evenly-columned grid — the trailing
per-key space was drifting it off-center under the amount; hovered keys
now highlight in accent.

Onboarding identity step: added a plain-spoken line that you can swap in
a fresh key any time to unlink from your old identity.

34 lib tests green. Verified live on :2: numpad centers (92/198/300),
fiatjaf npub → "✓ on nostr", an unprofiled key → "no profile" → confirm
gate → Pay anyway → Review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:59:01 -04:00
Claude 6dbd0f8e9d Build 16: fix yellow-theme muted-label contrast (WCAG AA)
The yellow theme's muted on-background tier (text_mute #6B6A63) rendered
the eyebrow labels (WALLET/PRIVACY/BALANCE/ACTIVITY…) and the "Moving
devices? Back up BOTH…" helper at only 3.85:1 on the #FFD60A background —
below the 4.5:1 AA floor (sweep P2). Darkened to #55534A (5.5:1,
measured), still the faintest of the three on-bg tiers so the hierarchy
holds. Dark/light themes unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 14:06:09 -04:00
Claude c72cda3039 Build 15: clean the returning-user wallet list of GRIM node chrome
The wallet-list screen (wallets exist, none open) still wore GRIM's
node-first chrome: a yellow INTEGRATED NODE / WALLETS bar and a forced-
open left node column with ENABLE NODE / Autorun / stat tabs. That
fights the payments-first philosophy the open-wallet surface set — the
node is plumbing, chosen in onboarding and managed in settings, not a
console you stare at to reach your wallets.

Now the list is full-bleed dark like the rest of the goblin surface:
- the node panel no longer force-opens on this screen (content.rs);
  it's demoted to a tappable status chip that opens the SAME panel,
  every Enable/Autorun/mining/stat feature intact — just opt-in
- the yellow title bar is suppressed here (wallet_list_screen()), its
  GEAR (app settings) and node access rehomed into a slim goblin header
  (node chip left, gear right) above the GOBLIN logo + wallet rows + "+"

Verified desktop + mobile + panel open/close; unlock still lands on the
unchanged open-wallet surface. 34 lib tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:26:50 -04:00
Claude d53345ffdd Build 14: hide scrollbars on the goblin surface
The yellow theme exposed the egui scrollbar track as a stray dark line
down the Settings column (sweep P2). The goblin surface is touch-styled,
so its content scroll areas now hide the bar entirely (AlwaysHidden),
matching the rest of the app and removing the artifact on every theme.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:41:19 -04:00
Claude b1b9bd61af Build 13: hosted profile pictures, username release, claim/release UX
Identity overhaul (server changes deployed to goblin.st separately):

Avatars
- profile pictures hosted on goblin.st, tied to the username: tap the
  settings avatar → native image picker → 256px PNG uploaded over Tor
- letter avatars now use (background, ink) color pairs (8) keyed off the
  npub, and render the first ALPHANUMERIC char — never the '@'
- custom pictures shown everywhere self/contacts appear: settings card,
  home header, sidebar chip, peers strip, activity rows, send recipient
- AvatarTextures: disk cache (~/.goblin/cache/avatars) + background Tor
  fetch + egui textures loaded on the UI thread; a network/Tor failure
  is never cached as "no avatar" (would have stuck for 6h)
- nostr/avatar.rs mirrors the server's sniff→limits→orientation→crop→
  256→re-encode-PNG pipeline so uploads are small and previews instant

Username lifecycle
- rotating the key now RELEASES the username (and deletes its avatar
  server-side) instead of transferring; rotation aborts if release fails
- claim panel is one Claim button (checks then registers); registered
  state shows "Registered <name>" + a Release action behind an
  are-you-sure gate ("up for grabs the moment it's free")
- released names are immediately re-claimable (quarantine removed)

Other
- Tor::http_request_bytes: binary bodies + status code, for upload and
  avatar download (string http_request kept as a wrapper)
- settings reordered Identity-first, then Wallet
- sidebar node card is 3 lines: status / block height / host
- profile card shows the full npub when it fits, else head…tail

34 lib tests green. Live-verified on goblin.st: upload→serve (image/png,
nosniff, immutable)→5/day limit (6th 429)→release purges avatar; a real
picture for @fartmuncher22 fetched over Tor and rendered across surfaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 12:22:41 -04:00
Claude a12f894dff Build 12: failed username checks no longer read as 'Taken'
check_availability returns Unknown when the Tor request dies, and the
claim panels collapsed everything but Available into 'Taken' — a free
name looked taken whenever a circuit flaked. The full enum now reaches
both panels: Unknown reads 'Couldn't check — connection hiccup. Try
again.' in neutral gray, and Reserved/Invalid/Quarantined get their own
copy. Tor::http_request also backs off up to ~15s across 5 attempts:
fresh circuits fail while arti refreshes its directory consensus over
the bridge, and 3 tries in 4.5s couldn't ride that out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:03:54 -04:00
Claude 0c60368280 Build 11: node card tells 'can't reach node' apart from syncing
A flaky external node read as 'Syncing…' forever — the card now goes
red with 'Can't reach node' on wallet sync errors (wallet.sync_error())
and recovers on the next good cycle. Validated against a dead node and
back on a live one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:50:20 -04:00
Claude 9d36562bab Build 10: width-gate the Pay numpad, mark the settings scroll edge
The numpad was platform-gated while the mobile shell is width-gated, so
narrow desktop windows showed neither numpad nor a visible input (sweep
P2). Settings rows clipping under the pinned profile card now meet a
hairline instead of slicing glyphs on an invisible edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 04:52:50 -04:00
Claude d8cf06b577 Build 9: QR scan, camera fixes, first-run onboarding, sweep fixes
Camera/QR (validated live with a v4l2loopback virtual camera):
- decode non-MJPEG frames on Linux (raw YUYV was undecodable: eternal
  spinner), enumerate devices off the UI thread without unwrap, open
  cameras by their real device index (list position broke whenever
  /dev/video0 was absent), show "No camera found" after 5s
- scan-to-pay: QR button in the send recipient search row and the
  mobile home header; in-surface camera panel feeding recipient
  resolution; only text payloads accepted (seeds/slatepacks refused)
- receive QR was unscannable: full cells (0.5px gaps fragmented finder
  patterns at 4.5px cells), always ink-on-white plate (inverted codes
  fail many scanners), center mark 26% -> 19% (zbar chokes above);
  rqrr+zbar both decode the live card now; unit tests cover the exact
  widget geometry

First-run onboarding (replaces the stock empty state only; the wallet
list, add-wallet modal and GRIM creation flow stay for later wallets):
- intro -> node choice (Private integrated / Instant external with
  URL) -> create or restore (wraps MnemonicSetup; word grid, paste,
  SeedQR scan) -> optional @username claim with prominent skip
- fonts bind at creation context so frame one can use Geist families

Sweep fixes (22 confirmed findings, the simple ones):
- yellow theme: active sidebar nav was dark-on-dark (P1)
- window frame ring painted with theme bg: transparent clear color
  rendered as a black band without a compositor (P2)
- import/rotate identity inputs get visible field wells (P2)
- receive buttons: verb labels + transient "Copied" feedback
- review hero card fills the column; fee row copy; pay-tab stale and
  duplicated hints; unlock modal copy; node card subtitle truncation
- build number stays current (.git/logs/HEAD rerun trigger)

Backup restore: import accepts the export-time password and re-encrypts
under the current wallet password; cross-device restores work.

31 lib tests green. Mainnet-validated: both flows restored real
wallets; 0.1 grin sent B->A through NIP-17 over Tor with async
open/close ping-pong; tx 71fbfce4f591 posted on-chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 04:07:53 -04:00
Claude 908df117e6 Build 8: stability fixes, Cash App-style shell, settings, key rotation
Stability (found via GUI testing):
- tor: create arti runtime on a clean thread; lazy TOR_STATE init panicked
  inside tokio contexts and poisoned the whole Tor/nostr stack
- store: open rkv SafeMode envs with capacity headroom; reopening at
  exactly DEFAULT_MAX_DBS crashed every wallet restart (DbsFull)
- goblin ui: centered_column hands children a full-height rect; ScrollAreas
  inside clipped everything below the first widget
- build: webtunnel client was silently embedded as 0 bytes without Go;
  warn at build, extract with create_dir_all + exec bit at runtime
- price: CoinGecko requires a User-Agent (403 otherwise); add retry-once
  backoff and a parse-failure diagnostic
- tor: retry http_request up to 3x on fresh isolated circuits

UX overhaul (per owner direction + Cash App references):
- floating icon-only 3-tab pill: Wallet / center accent ツ Pay puck /
  Activity (requests badge kept); Me opens via header avatar
- Pay tab: amount-first surface (numpad on mobile, typed on desktop) with
  Request and Pay; Pay carries the amount straight to Review
- Request flips Receive into "Requesting Nツ" state with a clear chip
- full-bleed goblin surface: GRIM title panel and network column hidden
  while a wallet is open; node status card lives in the sidebar above the
  profile chip; Lock wallet row added (Settings -> Wallet)
- goblin branding: titlebar, wallet-list logo + GOBLIN, new mark assets
  (white master, theme-tinted) in wordmark, QR center, wallet list
- build-based versioning: Build N = commits since the GRIM fork base,
  emitted by build.rs; About leads with it, Third party credits GRIM,
  grin node, nostr-sdk, arti, egui and the implemented NIPs

Accessibility & settings:
- surface_text{,_dim,_mute} tokens: yellow theme has dark cards on a
  bright bg; all on-surface text now readable in every theme (incl. QR)
- settings rows clickable across the whole row; profile card fills width;
  density option removed (comfy fixed)
- editable Node connections (integrated/external, add/remove) and Relays
  (add/remove + live service restart); NIPs explainer page with goblin.st
  context; third-party rows link to upstream projects
- standardized npub truncation (head 12 ... tail 6) shown in profile

Identity (owner decision: drop NIP-06 seed binding):
- random standalone nsec (Keys::generate); seed proves nothing about the
  identity and cannot resurrect it; legacy Derived identities still unlock
- key rotation: double warning (pending payments disrupted), typed RESET +
  wallet password, fresh random key, username moved atomically via the
  name server transfer endpoint; aborts cleanly if the move fails
- encrypted identity backup export (NIP-49 ncryptsec JSON, includes
  username + history) and import accepting nsec or backup JSON
- nip05d: POST /api/v1/transfer (NIP-98 by current owner, atomic
  owner-guarded swap, one-name-per-pubkey enforced) + SQL invariant tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 23:04:40 -04:00
Claude 906fee9c71 P8: desktop sidebar (shell B), theme + density pickers
- Wide desktop (≥720px) now renders a left sidebar nav (Wallet/Activity/Send/
  Receive/Settings) with a profile card instead of the bottom tab bar; narrow
  screens keep the bottom tabs. Both drive the same Tab state and screens.
- Settings gains an Appearance group: tap to cycle theme (Dark/Light/Yellow,
  re-applies egui visuals live) and density (Comfy/Regular/Compact).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 02:33:32 -04:00
Claude 32696438d3 Make Privacy settings interactive (accept policy + fiat toggle)
Tap to cycle the incoming-payment accept policy (Anyone/Contacts/Ask) and to
toggle the fiat USD preview, completing the user-control story for settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 02:30:17 -04:00
Claude aa9847bb41 Fix functional bugs found in UI audit
Send flow:
- "Sent" success now gates on the real dispatch result (NostrService send
  phase Working/Sent/Failed) instead of send_creating, which cleared before
  the DM left. Added a Failed stage with Try again / Close so a failed send
  is no longer shown as success.
- NIP-05 handle resolution moved off the UI thread (was .join()-blocking the
  render thread for the Tor timeout); now a worker thread writes into a polled
  slot with an inline "Looking up…" spinner.
- Desktop amount entry no longer steals keystrokes from the Note field (guards
  on the note TextEdit focus).
- Review screen now shows a network-fee line.

Requests / identity / archive:
- Approve is now double-tap safe (per-session approving set + disabled button),
  closing a double-pay window.
- "Backup nostr key" copied the public npub; split into "Copy npub (public)"
  and "Back up secret key (nsec)" which copies the real secret.
- Added inline username claim (availability check + NIP-98 register over Tor,
  off-thread) for anonymous users; persists nip05 + republishes identity.
- Added archive Export / Wipe history controls.
- Receive "Slatepack" button now copies the grin1 address; "Scan" tab relabeled
  "Receive" (no scanner was wired).
- Goblin view resets on wallet switch so a half-filled send can't leak across.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 02:26:54 -04:00
Claude ce1c071f3c Security hardening from adversarial audit
Wallet:
- identity.json (NIP-49 ncryptsec) now written 0600 in a 0700 dir so a local
  user can't grind the wallet password offline (+ regression test).
- Wallet password held as a ZeroingString through init_nostr so it's scrubbed
  on drop instead of lingering in a plain String for the session.
- Replaced 4 .unwrap() on re-read tx_meta with graceful guards (archive wipe
  mid-send could otherwise panic the nostr/task thread).
- Tor::http_request/post: bind the client once via let-else and propagate TLS
  builder errors, fixing a TOCTOU unwrap panic on concurrent Tor restart.

goblin-nip05d server (redeployed to goblin.st, verified live):
- One-name-per-pubkey now enforced by a partial UNIQUE index (closes the
  check-then-insert race); INSERT rows-affected==0 returns 409 not a false 201.
- NIP-98 replay protection: one-time auth event-id enforcement within the
  freshness window; tightened forward skew to +5s.
- Rate-limited the unauthenticated GET endpoints; SQLite in WAL mode.
- Verified live: replay rejected, second name for a pubkey blocked.

Audit verdict: fund-safety invariants (never auto-pay Invoice1; S2/I2
finalization bound to counterparty npub) and Tor-from-day-one all hold.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 02:13:15 -04:00
Claude 87efc8bb2d Add live e2e tests for the nostr relay and NIP-05 server
tests/nostr_e2e.rs (run with --ignored):
- nip17_slatepack_roundtrip: Alice gift-wraps a slatepack payment DM with a
  subject to Bob over wss://nrelay.us-ea.st; Bob unwraps, the seal-author ==
  sender invariant holds, the slatepack and subject extract intact.
- nip05_registration_roundtrip: registers a fresh name on goblin.st with a
  real NIP-98 signature, resolves it back to the right pubkey, releases it.

Both pass against the live deployed infrastructure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 01:54:37 -04:00
Claude 8a6d442544 Goblin UI: Cash App-style wallet surface (P4-P5)
New src/gui/views/goblin/ module rendered as the primary surface for an open
wallet: bottom tab bar (Wallet/Activity/Scan/Me), balance hero in ツ, Send/
Receive, recent peers, activity feed (wallet txs joined with nostr metadata
by slate id), pending payment requests with approve/decline, receive screen
with nostr-handle QR, settings/Me tab. Full send flow (recipient resolve via
npub/NIP-05 over Tor -> numpad amount -> review -> hold-to-send -> success)
dispatching WalletTask::NostrSend. Widgets library, fiat preview over Tor.
Fixed a font-binding panic: weight families are referenced only at widget
call sites, not in default text styles (set_fonts applies a pass later).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 01:50:52 -04:00