1
0
forked from GRIN/grim
Commit Graph

33 Commits

Author SHA1 Message Date
2ro ce23214d98 nostr: Connected flag tracks the fast relay-live probe, not the 30s catch-up fetch
Symptom: after 'Save & reconnect' of the relay list, the home/onboarding UI sat on 'Connecting relays…' for ~30s even though the relays had physically reconnected over the exit in ~2-4s.

Cause: on restart the service clears connected=false, then the UI flag was only restored AFTER publish_identity (serial, untimed per-event sends) AND a catch-up fetch_events_from bounded by FETCH_TIMEOUT=30s. One relay slow to EOSE pinned is_connected() false for the whole window while the connection was already usable. A separate FAST probe task already detects first-relay-Connected at 250ms poll (~2-4s) and reports relay-live to nymproc, but it did not touch the UI flag.

Fix: in that fast probe, when relays first report Connected (same point that calls report_relay_live), also set svc.connected=true. The indicator now tracks the real ~2-4s relay-up signal; publish_identity + the catch-up fetch continue in the background. Tradeoff (documented in code): a relay drop between the probe store(true) and the 2s status loop taking over wouldn't flip the flag for up to ~30s until the post-catch-up re-check re-syncs to reality — the same-order staleness as the old pessimistic gap, just optimistic; the transport watchdog still tracks real exit health independently.

Hardening: publish_identity's per-event send_event_to was untimed, so a stalled relay delayed the catch-up fetch and the kind:1059 subscription that follow it (real incoming-message latency). Each publish is now wrapped in tokio::time::timeout(SEND_TIMEOUT), mirroring dispatch_dm; on timeout it warns and continues to the next event, never aborting the sequence.

Audit: all readers of is_connected() were reviewed for the relaxed invariant (flag can now be true before the giftwrap subscription is established). gui/goblin/mod.rs and gui/goblin/onboarding.rs use it for display + repaint scheduling and to enable the claim-username button — claiming needs relays connected (which the flag now genuinely means), not the incoming kind:1059 subscription. wallet/e2e.rs uses it as a test precondition with downstream waits of 900s/2400s and relays replay stored gift wraps on subscribe, so it still converges. No reader treats is_connected() as 'safe to receive now', so no separate ui_connected flag is needed.
2026-07-03 15:39:02 -04:00
2ro 54344bd1d3 android: one-shot payment-requested notification (id=3)
Fire a separate high-importance system notification when an incoming
payment request (Invoice1 -> SurfaceRequest) is ingested over nostr,
mirroring the existing received-payment notification (id=2). Fail-open on
a missing JNI handle; fires once per not-yet-seen slate. No-op off Android.

Also add examples/tunnel_measure.rs, a dev harness for measuring the Nym
read tunnel (cold connect + warm per-fetch latency over the real transport).
2026-07-02 22:19:23 -04:00
2ro 23bb845689 nym: major transport speedup (throughput + reuse), money path unchanged
The in-process smolmix tunnel was far slower than the old SOCKS5 model.
Fixes:
- smolmix TCP buffers 8KB -> 256KB and burst 1 -> 64: bulk throughput
  ceiling rises ~32x (was capping relay backfill and JSON reads at a few
  KB/s).
- Read tunnel runs a high-traffic mixnet profile (cover traffic off, higher
  send rate, fewer reply SURBs) for lower latency; per-hop mix delays kept.
  The scoped-exit money-path client is separate and unchanged.
- HTTP over the mixnet now reuses connections (keep-alive pool) instead of a
  fresh TCP+TLS+HTTP handshake per request - fixes slow price and username
  reads.
- DNS prewarm no longer skips on cold start and serves stale-while-revalidate
  for known hosts, so a dial never blocks on DoT/DoH.

Money path (streamexit.rs, transport.rs) byte-for-byte unchanged.
2026-07-02 19:57:35 -04:00
2ro aa39737d3b nym: fast interactive reads + faster reconnect (no money-path change)
Profile/username/NIP-05 reads were ~10s and the tunnel stalled 20s on a
dead gateway. Fixes:
- Profile/accepts/dm-relay fetches stream scoped to their dial set and
  return on the first matching event instead of waiting for every relay
  (or the full timeout) - the ~10s nprofile search.
- HTTP over the mixnet is tunnel-first, scoped-exit only as fallback when
  the tunnel is not up (NIP-11/price/name lookups are public data).
- Name re-verify interval 78s -> 6h (was a debug leftover churning reads).
- Discovery relay NIP-11 probes run in parallel, not sequentially.
- Tunnel build timeout split from the exit dial cap: build 20s -> 10s
  (env GOBLIN_NYM_BUILD_TIMEOUT) so a dead gateway is abandoned fast; the
  exit money-path dial stays 20s.
- Cold start brings the tunnel up first, then prewarms the exit once after
  publish (grant sequencing preserved).
- NIP-05 search bounded to 15s instead of hanging up to ~90s.

Money path (transport.rs, streamexit.rs) byte-for-byte unchanged.
2026-07-02 18:57:52 -04:00
2ro c701f0f480 Floonet scoped Nym exit + NIP-44 v3 + wallet polish
Money path:
- Scoped, unbonded Nym exit for the money-path relay: the wallet dials a
  relay operator's co-located exit over a MixnetStream (src/nym/streamexit.rs)
  which pipes to its one relay; hostname-validated TLS end to end, no public
  DNS. Anchor + fallback (never pin-only): any exit failure degrades to the
  smolmix tunnel. relay.goblin.st's exit address is pinned in the relay pool
  (src/nostr/pool.rs) and the maintainer gist so it bootstraps offline.
- STREAM_SETTLE bridges the open-before-accept gap so the first TLS byte is
  not dropped into a stalled handshake.
- Verified end to end: two wallets complete a real gift-wrapped Grin payment
  through relay.goblin.st over the exit, finalized + posted on mainnet
  (src/wallet/e2e.rs, ignored live test).

Encryption:
- Adopt NIP-44 v3 for the NIP-17 gift-wrap path (G4): src/nostr/wrapv3.rs,
  nip44 path dep; v3<->v3 and v3->v2 interop.

Also: mix-DNS (src/nym/dns.rs), full localization pass, GUI polish,
avatar-ring example, Android icon/script updates, GRIM deviation notes,
xrelay + connect-timing tests.
2026-07-02 04:17:59 -04:00
2ro 991670d863 nostr: pause the @name re-verify sweep while the app is backgrounded
The 78s sweep fired regardless of whether anyone was looking, spending mixnet
round-trips in the background. Gate it on a frame heartbeat: the GUI stamps
crate::mark_frame() each draw (with a light ~2s repaint cadence so it stays
fresh while visible); when the app is backgrounded eframe stops drawing and the
stamp goes stale, so crate::app_foreground() reads false and the sweep skips.

The skip deliberately does NOT advance last_name_sweep, so the first tick after
the app returns to the foreground runs the sweep immediately — catching up on
resume rather than waiting out another full interval. Heartbeat lives at the
crate root so nostr reads it without depending on the gui module.
2026-06-16 22:58:07 -04:00
2ro 55b78b78ef macOS app icon = Goblin gradient; keep a contact's name across a decline
- The macOS bundle still shipped the old GRIM paw AppIcon.icns. Regenerated it
  from Goblin-Logo-Gradient-SMALLER.png (the yellow-gradient goblin mark) as a
  proper multi-resolution PNG-icns (16–512), so Finder/Dock show the Goblin icon.

- Declining (or cancelling) a request never re-resolved the counterparty, so
  their @name could drop to a bare npub just because the request didn't go
  through. handle_wrap now re-resolves the counterparty after a void — cheap,
  authoritative via the by-pubkey reverse lookup, and a no-op for anonymous keys.
2026-06-16 22:15:43 -04:00
2ro 919cfcb71e nostr: resolve a contact's @name via authority reverse lookup first
The requester still saw a bare npub for the payer because resolving a name
depended on fetching the peer's kind-0 off a relay (a nostr REQ with tight
timeouts over the private transport) before it could verify the NIP-05. When
that fetch doesn't land, resolution never starts — even though the name server
knows the name perfectly well.

resolve_contact_identity now asks the home authority directly:
GET /api/v1/by-pubkey/{hex} → the active @name for that key, in one HTTP
round-trip, with no profile fetch. That answer is authoritative, so the name is
set verified immediately. The kind-0 + verify path stays as a fallback for
FOREIGN authorities (which the home server can't speak for) and is still what
CLEARS a released/reassigned name. New nip05::name_by_pubkey helper.

Pairs with the goblin-nip05d by-pubkey endpoint. Verified by me only as far as
compile + unit/i18n tests; the live two-party resolution is the owner's call.
2026-06-16 20:01:20 -04:00
2ro d60e71d1e0 onboarding + nostr: healthy default node, claim feedback, identity import, resolve payer name
Four field-reported issues from a fresh install + a friend payment:

- Default node was grincoin.org, whose foreign API was returning "rpc call
  failed" — onboarding sync died with an un-retryable error. Lead the node
  list with the verified-healthy api.grin.money (external.rs) and use it as the
  onboarding default (was grincoin.org); grincoin.org stays in the list.

- Claiming a username gave no feedback and the identity card kept showing the
  npub. The card now shows the @name + a seal check once claimed, and a clear
  "name is yours" success card replaces the claim form before Open wallet.

- A returning user who restores a seed gets a fresh random nostr key, so their
  old @name couldn't come back. Offer "Import it" in the identity step: paste an
  nsec or pick a .backup file (reuses the wallet password just set) to keep the
  existing key + username.

- The requester side of a request never resolved the payer's @username — the
  FinalizePost ingest arm skipped ensure_contact/resolve_contact_identity, so a
  completed request showed a bare npub for the payer. Resolve on finalize like
  every other ingest path.

i18n: claimed_title/claimed_blurb + import_existing/import_title/import_blurb
across all six locales; drift test green.
2026-06-16 18:50:19 -04:00
2ro 11033b93fe Federation, note modal, save-to-device, onboarding fixes + UI polish
- Configurable name authority (Settings → Identity → Name authority): bare
  names resolve there, own-domain names show bare, foreign verified names show
  'name · domain' with a check — no '@' anywhere. Lets bob@otherdomain pay
  alice@goblin.st. Home domain derived from the configured server.
- Note entry is now a modal that floats above the soft keyboard (dimmed
  backdrop) instead of an inline editor the keyboard covered.
- Backup export SAVES to a chosen location (Android CREATE_DOCUMENT / desktop
  save dialog) instead of opening the share sheet.
- Onboarding status-bar icons are legible again (white on the dark surface,
  not black); identity step is less wordy and drops the '@' prefix; claiming a
  name during onboarding now republishes kind 0 so it's visible immediately.
- App-open name re-verify sweep (persisted, runs if >78s since last).
- Advanced 'Manage node connection' opens GRIM's native Connections UI.
- Manual slatepack paste: removed the QR icon. Pay screen: bolder, bigger ツ.
- Localized new strings across 6 locales.
2026-06-16 03:22:08 -04:00
2ro dfbd85c7b3 nostr: keep contact @usernames fresh; clear released/reassigned names
Cached names were verified once and never re-checked, so a contact who
released or changed their username kept showing the stale name forever.
Re-validate names on a 78s sweep (capped per tick to bound mixnet lookups):

- nip05::check — tri-state Verified/Mismatch/Unreachable, so we only clear on
  a definitive server answer (released, or reassigned to a different key),
  never on a network blip.
- resolve_contact_identity now re-checks names older than the freshness window
  and clears nip05 + nip05_verified_at on Mismatch (a user petname is kept);
  display falls back to the npub automatically.
- A periodic sweep in run_service re-verifies the stalest due contacts.

Tests for the tri-state parsing and the clear-keeps-petname logic.
2026-06-16 01:39:54 -04:00
2ro 6ea94989bf nostr: fix profile loading over the relay
Profiles never loaded when scanning a bare npub (username/avatar stayed
blank) even though the relay stores and serves the kind-0 fine. Two causes:
fetch_profile_blocking ran on a throwaway current-thread runtime that can't
drive the relay connections (which live on the service runtime, behind the
custom Nym mixnet transport), and it only dialed nprofile hints, never the
user's own default relays. Run the fetch on the service runtime via a stored
Handle, and always dial the default relay set (incl relay.goblin.st).
2026-06-16 00:31:27 -04:00
2ro 9768de2fbd Add manual 'Cancel payment' to reclaim a stuck outgoing send
A Goblin payment locks the sender's outputs until the recipient replies (S2)
and we finalize+post. If the recipient never connects to nostr, the funds stay
locked until the 24h auto-expiry. This adds a manual Cancel that reclaims them
on demand (after a 10-min grace, or immediately if the send never reached a
relay), marks the payment Cancelled, and best-effort voids it to the recipient.

- WalletTask::NostrCancelSend: authoritative tx lookup; refuses if already
  finalized/confirmed (race); marks meta Cancelled BEFORE cancelling the grin
  tx; serialized with nostr_finalize_post via a per-service lock so a cancel and
  a concurrent S2 finalize can't both commit.
- nostr_finalize_post returns Ok(false) (skip, no retry/re-post) when the tx is
  cancelled or the meta is Cancelled — covers the tx-list cancel path too.
- decide() already drops a late S2 on a Cancelled meta (new unit tests assert
  it); recipient-side void marks a received payment Cancelled for display WITHOUT
  deleting the output (a malicious sender could void-then-post otherwise).
- Void-before-S1 ordering handled via a (slate,sender)-bound marker.
- Receipt: tap-twice 'Cancel payment' with caveat + outcome notice; honest
  'Waiting for X to receive…' label; first-class Cancelled status. 6 locales.
- cancel_grace_secs config (default 600).
2026-06-15 19:34:44 -04:00
2ro 5005d7cb2d Build 82: names without @, wallet management + Advanced, list header, sturdier receive
- Drop the displayed @ prefix everywhere (identity picker, slatepacks page, all copy); @ stays internal for lookups/avatars.
- Settings: move wallet management to the foot — Switch wallet (deselect, stays unlocked), Lock wallet, and a new Advanced page mirroring GRIM's recovery tools (repair, restore-from-seed, reveal recovery phrase, delete).
- Restore the wallet-list title header (reachable settings gear, Pay-screen accent yellow).
- Transport: a transient receive/finalize failure no longer marks the gift wrap processed, so an incoming payment is retried on catch-up instead of being silently lost; finalize-post is now retry-safe (re-posts an already-finalized slate).
- Guard a latent panic on a short sender key in ensure_contact.
- Add more healthy public grin nodes (mainnet + testnet) for redundancy.
- Default first-run onboarding to the Instant connection (public node), shown first.
- Tidy leftover Tor remnants left from the fork.
2026-06-15 02:17:45 -04:00
2ro 96daed3c84 Build 81: show names everywhere, no @, tidier pay entry
Resolve a counterparty's @username on every interaction, not just incoming
requests — receives, sends and requests all kick off a verify-and-cache, plus a
one-time backfill on wallet open — so activity and the recent strip show names,
not bare npubs. Usernames now render WITHOUT the @ (kept internally for avatar
lookup); the recent row ellipsizes past 8 chars and centers the name under the
avatar, while activity shows the full name.

Pay screen: the numpad now sits above the note so the soft keyboard can't cover
it (and tapping the pad dismisses the note's keyboard), and a no-op key — a
second dot, a 0 on a leading zero, the 9-decimal cap — fires a short error
haptic instead of doing nothing silently.
2026-06-15 01:14:36 -04:00
2ro f715149302 Build 80: show the real balance and explain failed sends
The balance hero only showed amount_currently_spendable, so a wallet whose
funds were still confirming read 0 while GRIM showed the real total — the source
of the "Goblin says 0" confusion. It now shows the TOTAL (matching GRIM) with an
"X available - Y confirming" breakdown when some isn't spendable yet.

A send or approve that hits NotEnoughFunds (coins from a recent payment still
confirming, ~10 min) now says exactly that instead of a blank "Couldn't send",
and the Approve button no longer stays greyed forever — it un-greys on failure
with the reason shown, so the user can retry once funds clear. The relay-dial
cap for cross-relay delivery drops 12s to 6s so the first send isn't sluggish.
2026-06-15 00:21:10 -04:00
2ro af30af48e4 Build 79: deliver payments and profiles across different relays
A send/request to a counterparty on relays we don't already hold failed with
"relay not found": NIP-17 publishes to the RECIPIENT's relays, but send_*_to
rejects any relay not in the pool. connect_relays() now adds and dials the
target's relays (from their kind-10050 or an nprofile/QR hint) before sending,
so the gift wrap actually reaches their inbox. Same fix lets profile/@username
lookup find a kind-0 that lives only on the target's own relays — fetch_profile
takes relay hints and dials them first. Sidebar handle font now scales with
length so short @names are legible (was a fixed, too-small 11px).
2026-06-14 22:47:02 -04:00
2ro f0b854171c Build 78: honest transport labels, request decline/cancel, NIP-05 on requests, full localization
Settings now says "Manual transaction" and the privacy row reads "Messages &
lookups" opening a new Network privacy page that tells the truth: messages,
names, price and avatars ride the Nym mixnet; the grin node connects directly.
README and lander updated to match.

Requests are messages, payments are final: declining a request now sends the
requester a void control message (NIP-17), a requester can cancel a request they
sent (cancels the local invoice and notifies the payer), and incoming requests
resolve the sender's verified @username instead of a bare npub. The Requested
amount on the success screen is centered. New NostrDecline/NostrCancel tasks and
a goblin-action control message carry it, bound to the stored counterparty.

Localization: every Goblin-screen string moved to t!() keys (370 keys) and
translated into de/fr/ru/tr/zh-CN, guarded by a key/placeholder drift test.
System-locale auto-detect now matches region locales like zh-CN.
2026-06-14 21:44:24 -04:00
2ro 851ae1c565 Build 70: refresh README + clean stale comments for the public release
README: drop the removed nym-socks5-client sidecar story (the SDK is linked
in-process now), add the in-process build steps + the manual-slatepack feature.

Comments: replace stale "sidecar" wording with the in-process SOCKS5 proxy,
drop leftover Tor references (Goblin routes over Nym), and trim the chattiest
working-notes to terse rationale.
2026-06-14 13:02:03 -04:00
2ro bfed0a1cb9 Build 63: connect over Nym in seconds, yellow Pay page, gradient-avatar fixes
Nym "Connecting..." for ~1 minute — root cause fixed. open() spawned the
wallet sync thread BEFORE init_nostr() created the nostr service, so the
sync loop's first pass at service.start() found no service and skipped
it; the service only started on the next sync cycle, a full SYNC_DELAY
(60s) later. Measured on the dev box: the gap between "identity ready"
and "starting service" was 62s, while the relay itself connects over the
mixnet in ~2s. Initialising nostr before start_sync collapses that gap
to ~1ms (verified 62s -> 1ms). The mixnet sidecar already warms up at app
launch, so the relay connect was the only thing waiting — and it waited
on loop timing, not on Nym. Added a "first relay Connected ~Nms" log line
so the timing is observable from logs going forward.

Pay tab is now painted in the yellow theme (Cash App-style brand surface)
regardless of the active theme, via a scoped theme override held across
the central panel so the fill and every widget pick up the yellow tokens
together.

Gradient avatar now renders for anonymous npubs on the onboarding
identity card and the mobile home header — both hardcoded a flat "N"
letter tile while Settings already showed the gradient. The header falls
back to the short npub so avatar_any takes the gradient branch.
2026-06-14 01:58:19 -04:00
2ro de7007269f Build 56: wait for the mixnet before dialing relays; no on-screen keyboard
Two fixes from live testing:

1) Stuck on "Connecting…": the nostr service dialed relays without waiting for
   the bundled Nym sidecar to be up. On a cold start a fast wallet-open beat the
   mixnet bootstrap, every relay failed, and nostr-sdk backed off — so the wallet
   sat on "Connecting…" long after the mixnet was ready. Now we wait for the
   sidecar SOCKS5 port before connecting (instant once it is warm). Verified:
   service start -> relay catch-up in ~5s; the flag (Build 55) reflects it within
   2s.

2) On-screen keyboard on desktop: upstream Grim pops its own virtual keyboard on
   desktop (no_soft_keyboard = is_android()), which looked wrong in the wallet
   flows and competed with physical typing (intermittent dropped/duplicated
   chars). Goblin now uses native input on every platform — Android IME via JNI,
   physical keyboard on desktop — by defaulting no_soft_keyboard to true. Verified
   on desktop: no on-screen keyboard, reliable typing.
2026-06-13 21:39:17 -04:00
2ro 6dff408766 Build 55: surface the Nym/relay connection promptly (fix stuck "Connecting…")
The nostr service only refreshed its connected flag inside the select!s
sleep(30s) branch, which restarted on every incoming notification — so the
flag could lag the real relay state by 30s+ (or, under steady event flow,
never update), leaving the UI stuck on "Connecting…" even though a relay
handshake over the Nym mixnet completes in ~2s (measured). Set the flag right
after the startup connect, and poll it on an independent 2s interval; the 30s
heartbeat work (persist last-seen, TTL prune) stays on its own cadence.
2026-06-13 21:23:09 -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 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 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 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 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
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 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 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 1848d0c796 Goblin P0-P3 backend: brand reskin, theme tokens, nostr payment subsystem
Infrastructure (P0): deployed nostr-rs-relay (wss://nrelay.us-ea.st) and the
goblin-nip05d NIP-05 service (goblin.st) on us-ea.st with TLS + DNS.

Brand & theme (P1): Goblin name/icon/data-dir (.goblin); three-theme token
system (light/dark/yellow) in gui/theme.rs with colors.rs remapped as a shim;
Geist + Geist Mono fonts; AppConfig theme/density/last_wallet_id.

Nostr subsystem (P2-P3): src/nostr/ with NIP-06 identity (seed-derived,
NIP-49 encrypted), per-wallet rkv archive, guarded ingest policy (never
auto-pays Invoice1; binds replies to the stored counterparty npub), NIP-17
send/receive pipeline, NIP-05 client. Relay traffic routed over the embedded
arti Tor client via a custom WebSocketTransport. Wired into Wallet lifecycle
and the task handler. 26 unit tests pass.

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