1
0
forked from GRIN/grim
Commit Graph

1110 Commits

Author SHA1 Message Date
2ro 2e2d96016e goblin: switch, catch-up and shared-balance receive across identities
The active identity drives the single live gift-wrap subscription and every
send/display, and every identity redeems into the ONE shared grin balance.
Switching reuses the rotate/import stop-wait-start machinery: it verifies the
wallet password by unlocking the target BEFORE any teardown (a wrong password
never strands the wallet with no running identity), gates on no in-flight
send, tears down the service and stands a fresh one on the target key against
the SAME shared store (so processed-dedup carries across and nothing double
redeems), and moves only the active pointer (identity.json is never
overwritten). The catch-up now looks back from when THIS identity last
listened, so a payment that arrived while it was dormant is fetched and
redeemed on switch-in; the service surfaces a syncing state and a count of
payments redeemed during that catch-up for a "you were paid while away" cue.

TxNostrMeta gains recipient_pubkey (serde-default empty = primary) to tag
which front door a payment used, for per-identity activity and a later
accounting split. Wallet gains nostr_identities / add_nostr_identity /
switch_nostr_identity, init_nostr adopts the held-identity index with legacy
migration, and a wallet-password change now re-encrypts every held identity
through the same NIP-49 path. Only the active nsec is ever decrypted in
memory; the rest stay encrypted at rest.
2026-07-05 16:11:18 -04:00
2ro 26da66710e goblin: held nostr identity set (storage + migration)
One wallet, one grin seed / one balance, but a SET of nostr identities
(nsecs), exactly one active at a time. New identities module owns the
held-identity index (identities.json): which identities the wallet holds,
their order, and which is active. It stores no secrets; each held identity
is its own NIP-49 ncryptsec on disk, exactly like the single identity today
(identity.rs create/unlock/backup reused unchanged, plus save_at/load_at for
the per-identity files and pubkey_hex for the index key).

Migration is fund-safe and needs no key regen: a pre-feature wallet has only
identity.json, which the index adopts in place as the single active identity
number one; the legacy file is never overwritten, so an older build still
opens the wallet on it (clean rollback). The store gains a per-identity
last_active_at so a switch back to a dormant identity can catch up from when
it last listened, not merely from the wallet-wide last connection.

Unit tests cover migration, add/switch/cap/dedupe, active resolution across a
reload, corrupt-index fallback, reencrypt-all, and the catch-up-since rule.
2026-07-05 16:11:02 -04:00
2ro 90c294bf26 goblin: point changelog third link to docs.goblin.st and rebrand startup log
Repoint the changelog modal's third button to https://docs.goblin.st
(the docs/reference destination; GitHub button already covers source)
and swap its code-brackets glyph for the BOOK icon so it reads as a
docs link. Rebrand the startup version-announcement log line from
"Grim" to "Goblin".
2026-07-05 14:07:55 -04:00
2ro 3a69b72d9d goblin: fix update changelog modal branding and links
Rebrand the inherited GRIM update/changelog modal for Goblin. The
modal title now reads "Goblin <version>" instead of "Grim <version>",
and the three footer links point to Goblin destinations: GitHub
(2ro/goblin), the Telegram family channel, and goblin.st in place of
the old code.gri.mw source mirror.
2026-07-05 14:03:51 -04:00
2ro bdedcba498 goblin: appearance settings additions for Build 140
Three settings-surface additions:

- Hide amounts: a toggle (same switch widget as the incoming-requests
  toggle) under the amount-privacy rows, persisted in the app config
  (serde-default off so old configs load). When on, the received-payment
  notification masks the numeric grin with dots.

- Language: a picker under Appearance beside the theme row. Lists the six
  shipped locales in their own names; tapping one switches and persists the
  active locale.

- Update available: a badge pinned to the right of the settings profile
  panel, shown only when the release check has found a newer build (reuses
  the existing AppConfig::app_update state); tapping it opens the release
  download page.

New user-facing strings added to all six locales.
2026-07-05 13:11:23 -04:00
2ro 037e727756 goblin: bolder frameless home QR matching the pay header
Drop the tacky filled circle behind the home scan-to-pay QR. Render the
glyph larger and in the theme's foreground ink (white on the dark home
background), vertically centered next to the profile avatar, mirroring the
size and treatment the Pay header already uses. The scan action and target
are unchanged.
2026-07-05 13:11:08 -04:00
2ro ce024443ac goblin: refine the activity row and drop list seconds
Rebuild the activity row so the message pins left and truncates while the
signed amount pins right with the date/time right-aligned directly beneath
it. List timestamps lose the seconds (HH:MM); the tap-in detail view keeps
the full second-precision time. A row with no timestamp (a canceled tx)
draws no right-side time slot at all and folds its status word into the
left message instead. Shared note/time derivation lives in one helper.
2026-07-05 13:10:43 -04:00
2ro 1141f97b22 goblin: date the home news panel (ISO 8601) and guard title overflow
Show the article date first in the Home news card, formatted YYYY-MM-DD in
UTC (never a US M/D/Y, day only, no time). The date is sourced from the
NIP-23 published_at tag when present, else the 30023 event created_at; a new
optional published_at field on NewsItem threads it through the store and the
news handler.

Guard the title against clipping two ways: a hard NEWS_TITLE_MAX_CHARS cap
(48) that ellipsizes an over-long title predictably, plus measured
shrink-to-fit that steps the semibold font from 16pt down to a 12pt floor so
the title stays on one line within the card width on a 390px phone, backed by
truncate for the pathological narrow case.
2026-07-05 06:29:21 -04:00
2ro e036a9692a goblin: fix activity row amount overlap and truncation target
The recent-activity rows had two layout faults visible on the 390-wide
home surface.

The transaction amount collided with the subtitle text. Root cause was
layout, not position: the title/subtitle column used truncate() against
the full available width, which included the space the right-aligned
amount needed, so the text stretched under the amount and the amount was
pushed on top of it. Reserve the amount as its own right-hand column
first, then bound the text column to the width that is left. Centered
against the two-line stack, the amount now lands between the title and
subtitle lines with clear space above and below. Row height goes 60 to
64 for breathing room.

The wrong subtitle field was being clipped. The subtitle was one
"note · date/time" string truncated at the end, so the date and time
(the part the row exists to show) lost its seconds first. Split the
subtitle: the date/time tail is pinned right and never clipped, and the
note takes the remaining width and gets the ellipsis.

No new user-facing strings. Picker rows reuse the same widget with an
empty tail, so their npub/tag subtitle still truncates as before.
2026-07-05 06:20:12 -04:00
2ro ef58f260e8 harden(nostr): cap relay ws frames and zeroize conversation keys
Two self-contained hardening measures:

- WebSocket frame cap: the Tor relay transport dialed every relay with
  tungstenite's default 64 MiB message / 16 MiB frame ceiling. Pass a
  WebSocketConfig capping both to 4 MiB via client_async_tls_with_config, so
  a hostile or buggy relay can't stream a giant frame into wallet memory. The
  pool only requires max_message_length >= 128 KiB and the wallet's own events
  are far smaller, so 4 MiB keeps ample headroom.

- Secret zeroization: the raw NIP-44 v3 ECDH conversation keys in wrapv3.rs
  are now Zeroizing<[u8; 32]> so they are scrubbed from memory on drop instead
  of lingering. zeroize (already a transitive dep) is pulled in directly.

Adds a ws-config test asserting the caps sit below the tungstenite defaults
and above the pool minimum.
2026-07-05 06:08:25 -04:00
2ro 210c4ab662 fix(send): show the proof-notify target on the review screen
With proof mode on, a notify= npub on the scanned URI makes the wallet
gift-wrap the full signed payment proof (buyer sender address + kernel) to
that key at finalize, but the review screen never showed the target, a
hidden watcher the payer never saw or consented to.

The proof row now renders a second line, Proof shared with: npub1abc...xyz,
whenever a notify target is present, so the recipient is seen and chosen: if
you scanned it, you asked for it. Display only; nothing about what gets sent
changes. Adds the goblin.send.row_proof_shared key in all six locales.
2026-07-05 06:08:25 -04:00
2ro c0b622f694 fix(payuri): reject non-ASCII and oversized scanned amounts
A crafted pay-URI amount with a multibyte UTF-8 char (e.g. amount=0.EEEE)
reached grin_core's amount_from_hr_string, which slices the fractional tail
at a fixed byte index and panics on a non-char-boundary. The scan/deep-link
thread has no catch_unwind, so a single scanned QR or opened link could crash
the wallet. validate_amount now rejects any non-ASCII amount up front (Grin
amounts are only [0-9.]), degrading to manual entry.

Also cap the whole-GRIN part at 1e9 before the grin parse, below the u64
overflow point of grins * GRIN_BASE. Without this a giant amount wraps in
release to a small atomic value that is what actually gets dispatched while
the review screen still shows the giant figure.

Adds regression tests for both (multibyte to None no panic; over-cap to None).
2026-07-05 06:08:25 -04:00
2ro 42d70e1a5e Goblin: generic browser User-Agent for Tor HTTP, drop duplicate UA
Replace the "goblin-wallet" User-Agent on every Tor-carried request with a
common desktop-browser string so the wallet's traffic is not trivially
classifiable as Goblin at the destination. The default lives in one const in
the Tor client and is applied once per request; price.rs no longer passes its
own User-Agent (which sent the header twice). Also fix a stale nip05 doc
comment ("Nym mixnet" -> "Tor"). No transport change: the update check stays
on its existing clearnet path for GRIM parity.
2026-07-05 05:55:32 -04:00
2ro e9f0c3f0e2 Goblin: publish the "payment sent" receipt at dispatch, not finalize
Closes the buyer's double-send window. When a Nostr-rail send carries
order context, the plain kind-17 status=sent receipt used to publish at
finalize alongside the encrypted proof. With an offline merchant,
finalize can be hours away, so the buyer stared at a scannable QR for a
payment already made: maximum double-send temptation.

The receipt now publishes at S1 dispatch, the same moment the payment
envelope is accepted by a relay and the wallet UI flips to "sent",
through the same crash-safe reconcile queue. The event shape is
unchanged (same tags, kind, content); only the timing moves earlier, so
magick's status=sent matcher is unaffected.

The encrypted PROOF delivery stays at finalize (the proof does not exist
before then). Idempotency: a new receipt_sent flag on TxNostrMeta gates
the receipt to exactly one publish per tx, so finalize never duplicates
it. deliver_proof no longer touches the receipt; the reconcile pass gets
an independent receipt-retry path for the crash/offline case.

Decision logic is factored into pure predicates (receipt_due_at_dispatch,
receipt_retry_due, proof_delivery_due) used by the live paths and covered
by three tests: dispatch-publish decision, no-duplicate-at-finalize, and
proof-still-at-finalize. Full lib + i18n drift green.
2026-07-05 05:53:00 -04:00
2ro a437aad2f8 README: language-aware news, open-to-pay links, proofs on request
Build 138 surface: the Home news panel now renders in the wallet's language
with an English fallback; goblin:/nostr: pay links and scanned checkout QRs open
straight to a prefilled review screen; and payments can include a native Grin
payment proof when the request asks for one, off by default and shown on review.
2026-07-05 05:31:43 -04:00
2ro d5d1212a44 Goblin: fetch fiat rate live on view, kill the 48h price cache
The fiat subline now fetches the CURRENT rate over Tor only while the
balance is actually on screen, and never paints a stale number as if it
were live. No disk cache, no background timer: an idle or payment-
listening wallet never polls (battery).

- price.rs: in-session rate held for a 3-minute freshness window; viewing
  a stale/missing rate kicks a live refetch. New RateState (Fresh/Loading/
  Unavailable) replaces the Option<f64>; failures are tracked so the line
  says "unavailable" instead of spinning or showing an old value. Removed
  the disk seed and the persisted-rate write.
- config.rs: dropped the dead last_rate/last_rate_vs/last_rate_at fields
  and their read/write paths. Old configs still load (serde ignores the
  unknown keys); the fields are simply not written back.
- lib.rs: dropped the cold-start seed_from_disk call.
- GUI: balance hero fiat line renders a subtle "≈ …" while loading and a
  localized "rate unavailable" on failure, requesting a bounded repaint
  while a fetch is pending so the rate pops in on-screen. Pay/send amount
  preview shows nothing until a fresh rate lands.
- New goblin.home.fiat_unavailable string across all six locales.
- Tests: freshness-window bounds, is_fresh boundary, classify state
  machine (fresh/loading/unavailable), and config compat for the removed
  fields.
2026-07-05 04:38:24 -04:00
2ro ab2ac7c3ac Goblin: fix macOS build, import objc sel_impl for the deep-link bridge
objc 0.2's sel!/msg_send! macros expand to sel_impl!, which has to be in
scope. The cfg(target_os = macos) apple-events module never compiles on
Linux, so local cargo check missed it and only macOS CI caught it.
2026-07-05 04:01:28 -04:00
2ro 3db6375459 Goblin: apply node selection to the running wallet, no silent zero
Selecting a node in Settings > Wallet > Node moved the checkmark and
persisted the choice, but the live session never switched: the node
client is baked into the wallet instance at open time and was only
rebuilt on the next unlock, so a running wallet kept polling the old
(often dead) node. Owners onboarded on an older build still carry the
retired grinnode.live default in connections.toml, which now 502s, so
the wallet showed a stuck zero balance with no way to recover short of
a force-restart.

GRIM's own connection-settings UI reconnects on change (close + reopen);
the Goblin node picker only called update_connection, so this was a
Goblin regression, not inherited upstream.

- Wallet::reconnect_node swaps the live node client's URL + secret in
  place via w2n_client (no close/reopen, so no password re-entry),
  updates the runtime connection so the UI reflects the switch at once,
  clears the stale sync error, and wakes the sync thread to refresh
  against the new node. Wired into all three picker actions.
- node_url_secret factored out of create_node_client so open and live
  reconnect always resolve the same node for a given config.
- balance_hero no longer renders a bare 0 during a node outage: a pure
  balance_subline state machine picks updating / can't-reach-node /
  last-known-balance, so an unreachable node reads honestly instead of
  looking like an emptied wallet. New goblin.home.balance_stale string
  in all six locales (drift green).
- Unit tests for the balance state machine.
2026-07-05 03:38:08 -04:00
2ro d5ae136cf1 Goblin: route macOS goblin: link clicks to the send-review screen
The goblin: scheme was registered everywhere last night, but macOS is the one
platform that delivers a scheme click as a Carbon/Apple Event (kAEGetURL), not on
argv and not through any path winit or eframe surface, so on a Mac the click went
nowhere.

Install a kAEGetURL handler straight on the shared NSAppleEventManager at startup
(macOS only), pull the URL string out of the event, and feed it to on_data, the
exact same entry the desktop argv path uses. The per-frame Goblin router then lands
the pay URI on the prefilled review screen, identical to a scanned checkout QR or a
Linux argv link. This covers both a cold launch (event queued at start-up) and a
warm click (app already running).

The bridge is entirely cfg(target_os = "macos"): zero bytes on Linux, Windows and
Android (Linux binary size byte-for-byte unchanged). It talks to the Objective-C
runtime through the classic objc crate, which is already in the macOS build graph
via nokhwa/cocoa/metal/wgpu, so it adds no new dependency tree, only its own small
handler class. It registers its own Apple Event handler rather than touching the
NSApplicationDelegate winit owns, so nothing winit does is clobbered.
2026-07-05 03:14:45 -04:00
2ro a7c2443f3b Goblin: proof-on-request payments (wallet side of the frozen contract)
Implements the Goblin-wallet half of the proof-on-request architecture
(magick payment infra spec section 4/5): the buyer's wallet threads a
native Grin payment proof per transaction, off by default and on only
when the scanned pay URI asks for it, and auto-delivers the proof to the
market and watcher on finalize. Person-to-person sends are unchanged.

W1 pay-URI parser (payuri.rs): PayUri gains proof/order/notify, each
fail-closed (a bad value drops to None and never blocks the payment).
proof is a grin1/tgrin1 slatepack-address shape check, order is an opaque
control-stripped routing key capped at 64 bytes, notify is an npub shape
check. Unknown-param forward-compat and the magick interop tests still
hold. 12 new parser tests.

W2 proof threading: WalletTask::NostrSend carries proof/order/notify; the
handler re-parses proof authoritatively into a SlatepackAddress and calls
w.send(a, Some(addr)) instead of the hard-coded None, setting
payment_proof_recipient_address. The order handle, watcher npub, and
amount persist in TxNostrMeta (new serde-default fields) before dispatch,
so a crash between send and finalize loses nothing.

W3 review indicator: the send-review screen shows a small "proof included"
row when proof mode is active. New t!() keys row_proof / row_proof_val in
all six locales (drift test green).

W4 proof delivery on finalize: when a finalized SEND was sent in proof
mode, the wallet publishes the two contract events: a plain unencrypted
kind-17 "payment sent" receipt to the app relays (payment-request tag =
order handle; proof and kernel deliberately omitted to avoid leaking the
buyer's sender address; flips the page to "detected", never "paid"), and
a NIP-59 gift-wrapped kind-17 rumor to the notify npub carrying the full
proof JSON plus a kernel-excess tag. Both are enrolled in the existing
crash-safe reconcile pass and retried until a relay accepts them.
wrapv3 gains wrap_kind() to gift-wrap an arbitrary rumor kind.

Matching is by the payment-request (invoice number) tag alone; the wallet
never learns magick's internal orderId. No merchant-side auto-receipt is
built: the rewritten spec has the buyer publish on finalize instead.
2026-07-05 03:01:04 -04:00
2ro ae0f36d287 Goblin: goblin: payment deep link opens straight to send review
Web "Open in Goblin" pay buttons fired generic nostr: URIs, which the
OS routed to whatever social client claims nostr:. Register a dedicated
goblin: scheme and route it (plus nostr: pay URIs) to the prefilled
send-review screen, the same destination as scanning a checkout QR.

Parser (payuri): accept goblin: alongside nostr: (same payload, either
scheme); add is_pay_uri() to tell a payment link from a slatepack.

Runtime: an argv/intent/socket payload that is a pay URI is stashed and
opened by the Goblin surface via SendFlow::from_deeplink, which shares
the exact apply_scan path the camera uses. Desktop cold + warm start
(argv + single-instance socket) and Android cold + warm (intent-filter)
covered.

Registration: Linux .desktop x-scheme-handler/goblin, Android goblin
intent-filter, macOS CFBundleURLTypes, Windows WiX HKCR URL protocol.
2026-07-05 01:53:18 -04:00
2ro 9f019edfeb Goblin: fix desktop clipboard (copy/paste lost the selection)
On desktop the Paste button in the wallet-restore seed entry did nothing, and
the mirror copy on the new-wallet creation screen was equally broken: a copied
recovery phrase never made it to the clipboard.

Root cause (inherited from GRIM, not a Goblin regression — the two clipboard
impls are byte-identical): copy_string_to_buffer / get_string_from_buffer each
created a fresh `arboard::Clipboard` and dropped it the instant the function
returned. On Linux (X11 AND Wayland) the clipboard selection is owned by the
live Clipboard instance, so dropping it immediately released ownership and the
copied text vanished before it could be pasted. Reproduced with arboard 3.6.1
on a Wayland session: fresh-instance-per-call read back empty; one persistent
instance across set+get read back correctly.

Fix: keep ONE long-lived `arboard::Clipboard` on the Desktop platform (behind a
Mutex, since the trait methods take &self and arboard's take &mut self), so our
process stays the durable selection owner. Also stop unwrap()-panicking on a
clipboard backend error — a failed copy/paste now logs instead of crashing.

Every clipboard button in the app routes through this one platform pair
(onboarding paste/copy, GRIM creation paste/copy, proof, slatepack message
entry, tx/receipt copy, receive copy, npub/nsec/backup copy, the shared
TextEdit paste), so this single change repairs the whole class at once.

Verified end-to-end: an #[ignore]d clipboard_roundtrip test drives the real
Desktop impl on the live session clipboard and asserts a 24-word phrase
survives copy -> paste (run with --ignored). It passes on Wayland now and would
fail under the old pattern.
2026-07-05 00:41:33 -04:00
2ro 093c5014ef Goblin: GoblinPay QR scans straight to review
When a scanned QR is recognized as a full GoblinPay payment payload (a
recipient key that decodes locally to a concrete pubkey AND a validated
amount), skip the recipient discovery/search step and open the review screen
prefilled with recipient, amount and memo. The merchant checkout QR already
carries everything needed to pay, so re-walking the recipient search was
redundant. The review screen still confirms the send unchanged (the user
holds-to-send); nothing auto-sends.

recognize_scan classifies the payload:
- Review: nprofile/npub/64-hex recipient + amount -> straight to review. A
  cheap LOCAL contact lookup supplies a friendly name; otherwise the short
  npub (no network resolution on this path). nprofile relay hints carry
  through as delivery targets.
- Search: a name/@handle (needs NIP-05 resolution), an amount-less key, or
  non-nostr text -> the pre-existing search-box path, with any amount/memo
  still prefilled.

This covers every scanner entry into the send flow (search-field scan icon
and the header scan-to-pay screen), so it works from anywhere the payment
scanner is reachable.

Unit tests on the pure recognize_scan / decode_recipient_key:
GoblinPay-uri-with-amount -> Review; key-without-amount -> Search;
name+amount -> Search; non-nostr -> Search; key decode variants.
2026-07-05 00:34:59 -04:00
2ro 65d8d0f7bd Goblin: language-aware news panel
Show the newest news article whose language matches the wallet's active
locale, falling back to the newest English article when none exists.

Language detection per article, in priority order:
1. An event language tag: NIP-32-style ["l","<code>","ISO-639-1"] or the
   bare ["l","<code>"] / ["lang","<code>"] (code is the tag's 2nd element),
   extracted in handle_news and persisted on NewsItem.lang.
2. A trailing "[xx]" marker on the title (case-insensitive, xx = ISO 639-1
   two letters), e.g. "2026-07-05 Welcome to Goblin [de]".
3. No marker anywhere = English.

Selection happens at the data layer (news_latest): the store stays
language-agnostic (still newest-per-d-tag, cap 8, edit-in-place). Among all
cached articles, filter by detected language, take the newest by created_at;
the "[xx]" marker is stripped from the displayed title. The app locale is
folded to its ISO 639-1 primary for matching (zh-CN -> zh).

The GOBLIN_FAKE_NEWS debug hook now injects an English + German sample so the
panel selection can be screenshotted per locale.

Unit tests: tag detection (both shapes), title-suffix detect + strip,
no-marker-means-English, zh-CN folding, locale match + English fallback,
newest-within-language-slice.
2026-07-05 00:34:47 -04:00
2ro d4dcbb115f i18n: translate lagging fallback strings and localize settings username hint 2026-07-04 23:13:41 -04:00
2ro 0bf87a6cff README: drop stale onion claim (Tor-exit only since 134), style sweep
Every relay, money path included, is reached over a Tor exit to its
clearnet host; the pinned-onion wording was Build <134. Em dashes
normalized per house style.
2026-07-04 22:21:33 -04:00
2ro a57121959a README: Build 136 news panel on Home
Latest article from the official Goblin news key (kind 30023) between
Send/Receive and Recent Activity; hidden when empty; latest-only with
edits-in-place.
2026-07-04 22:20:21 -04:00
2ro ba8e81ef5f Goblin Build 136 - home news panel, faster send confirmation
Home now shows the latest post from the Goblin news key (kind 30023) in
a card with tappable links; the panel stays hidden until a post is seen.
Desktop Home widens to use the available space. Send confirms faster.
2026-07-04 20:07:13 -04:00
butler e492703a0c docs: mixnet references updated to Tor (build 134) 2026-07-04 19:36:03 -04:00
2ro 89791793ed Goblin: dedupe balance sync indicator
Remove the Build 135 balance-hero "Updating…" line. During a routine
sync it duplicated the header node card's "Syncing" status, so a funded
wallet showed two updating indicators at once. Keep the established
"Balance updating…" line (zero-balance / first-sync case) and the header
status as the single sync indicators. Drop the now-orphan
goblin.home.updating key from all six locales (i18n drift green).
2026-07-04 12:10:52 -04:00
2ro 278a946980 chore: gitignore local release artifacts 2026-07-04 11:39:29 -04:00
2ro e8d71afc7e Goblin Build 135 - Tor credit, balance updating indicator, Nym sweep
- Third-party credits: replace the "Nym mixnet" row with "Tor (arti)" (0.43,
  linking the arti repo), matching the other credit rows' hardcoded style. No
  locale strings existed for it (credit labels are hardcoded, not localized).
- Balance: show a quiet "Updating…" line under the balance while the node is
  still warming, reusing the existing wallet.syncing() signal and the
  balance-updating line's muted style; it clears once the node is synced. New
  t!() key goblin.home.updating added to all six locales (i18n drift green).
- Nym sweep: Cargo.toml package description "Nym mixnet" -> "Tor". The only
  other user-facing Nym reference was the credit above. Remaining references
  are the deliberately-dormant `nym` feature/module and internal code comments,
  left as-is; README and all locale values were already Tor.
2026-07-04 06:15:44 -04:00
2ro 1f36631777 Goblin: retire Nym from the public toolchains + drop internal test harnesses
- CI (release.yml/build.yml): remove fetch-nym + AWS_LC_SYS_PREBUILT_NASM;
  the default cargo build --release is Tor/arti only now (nip44 fetch kept).
- Cargo.toml: nym-sdk is optional behind the `nym` feature so the default
  build never pulls it.
- Delete the dead Nym-era probes (xrelay_smoke, connect_timing,
  tunnel_measure) that referenced the removed Nym API and broke `cargo test`.
- Untrack the internal E2E harnesses (e2e.rs, nostr_e2e.rs) via .gitignore;
  gate `mod e2e` behind the `e2e-internal` feature so clones still compile.
- Refresh stale Nym comments in build.rs and the build scripts.
2026-07-04 05:59:56 -04:00
2ro 1e55ef5dfb Goblin Build 134 - Tor delivery fixes (drop the flapping onion)
build133 shipped the Tor transport but two delivery bugs remained; this
fixes both so payments resolve through confirmations again.

- Drop the pinned relay .onion. It was a single fragile hidden-service hop
  shared by every wallet; under load it flapped (WebSocket 1006), and when
  it dropped mid-handshake a payment stalled after the first gift-wrap --
  you'd get the incoming alert but the money never confirmed. Every relay is
  now reached over the Tor exit to its clearnet host: your IP still stays
  hidden from the relay, with no fragile onion hop to wedge. The has-onion
  gate that rejected an onion-less relay list is removed, and the pinned
  candidate pool carries no onion/exit fields.

- Tor-friendly relay set. Defaults/fallbacks had included relays that refuse
  Tor-exit connections (damus, nos.lol); wallets that fell back to them
  couldn't send at all. Defaults and the pinned pool are now
  relay.floonet.dev, relay.0xchat.com, offchain.pub.

- confirm-before-sent no longer false-fails across a relay reconnect (a
  confirmed-received wrap is treated as sent-pending instead of re-dispatched,
  which had duplicated wraps and hung the spinner); a v3 gift-wrap unwrap
  failure no longer silently drops the message.

Validated over Tor with two funded wallets on two independent mainnet nodes:
connect in seconds, send to finalize in 8s.
2026-07-04 05:33:10 -04:00
2ro 03c1770892 README: update transport description from Nym mixnet to Tor
The README still described the old Nym-mixnet transport, including a
reference to src/nym/sidecar.rs which no longer exists post-switch.
Update the intro, feature list, payment-travel diagram, and build
instructions to describe the current embedded-Tor (arti) transport:
in-process, dialed at the relay's pinned .onion, no sidecar, no sibling
checkout needed for the default build.
2026-07-04 03:51:21 -04:00
2ro 3c32474f75 docs(internal): Tor transport redesign plan + payment-feel report + forum post 2026-07-04 03:35:33 -04:00
2ro 30c0ed9a12 Goblin Build 133 - Tor transport (replaces the Nym mixnet)
The wallet's private transport moves from the Nym mixnet to embedded Tor
(arti, copied from GRIM's engine): it dials the relay's pinned .onion, so
the relay never learns your IP, while the relay + NIP-59 gift-wrap hide the
rest - content, sender, and (via a relay-side randomized release) timing.
The Grin node stays on the clear internet as before.

Why leave the mixnet: the Nym free-tier bandwidth this depended on is being
removed upstream (the grant expires at UTC midnight; the paid path requires
holding NYM tokens), so a payments wallet can't stand on it. Tor is
unmetered, embedded in-process on mobile, faster where users wait, and
lighter on the battery.

Preserved intact: the confirm-before-sent guard, relay-gated readiness, and
the lazy warm-on-activity node polling. src/nym/ is feature-gated off (arti
and nym-sdk can't share one binary); full removal is a follow-up.
2026-07-04 03:35:29 -04:00
2ro 22bf3359f5 nostr: revert the money-path split — all nostr on the mixnet (as prior builds), keep confirm-before-sent 2026-07-03 22:50:01 -04:00
2ro 53e18f06c7 nostr: money-path split — slatepacks over the mixnet exit, everything else clearnet
The wallet routed its ENTIRE relay.floonet.dev session (own identity, recipient
lookups, profile, catch-up, subscribe, publish) through the scoped Nym exit,
saturating its metered free-tier bandwidth so a payment gift-wrap arrived
minutes late or dropped — while nostr-sdk falsely reported "sent" (it returns on
the local mixnet-stream write, not a relay OK). Root cause was contention: ~97%
of the exit's relay bytes were non-payment overhead.

Split the nostr service into two clients:
- money client (Nym scoped exit): kind-1059 slatepacks + gift-wrap
  subscribe/catch-up + recipient resolution — NIP-05 name lookup (already
  mixnet via the HTTP tunnel) and the kind-10050 DM-relay lookup (moved here;
  it's private payment-target resolution, and cheap). Relays kept, not dropped.
- general client (clearnet): own identity (0/10002/10050), discovery, the fat
  kind-0 profile/avatar, general subs, and catch-up of non-1059.

Plus confirm-before-sent: a payment publish is not reported "sent" until a real
relay OK read-back confirms it — a slow/failed exit now surfaces as a retryable
error instead of silent money loss.

Runtime-verified: a normal session puts 0 bytes on the scoped exit (all
clearnet); a kind-1059 slatepack rides the exit and lands on relay.floonet.dev
(exit read-back + independent clearnet oracle). Exit non-payment overhead
dropped from ~50 KiB in / 164 KiB out per session to ~0.

Adds E2E test harnesses (wallet::e2e::funded_e2e_pay,
examples/nostr_split_measure, an exit publish repro in streamexit tests).
2026-07-03 22:30:25 -04:00
2ro c78d7b0e60 nym: fast fresh-gate probe budget — dead exits condemned in 10s, not ~32s
Measured (30 cold trials at the gateway-race commit): the worst cold-start
outliers (51s, 72s) were dominated by DEAD-EXIT condemnation cost, not the
gateway. A fresh tunnel on a blackholing exit burned probe_fresh's doubled
patient budget (2 calls x 2 rounds x 8s ~= 32s) before reselecting.

Split the liveness probe into two budgets over a shared probe_with_budget:
- probe() (established tunnels: watchdog keepalive + condemnation checks):
  UNCHANGED - 8s x 2 rounds, worst 16s, same patience as before.
- probe_fresh() (gating a just-built tunnel before publish): 5s x 2 rounds
  x 1 call = 10s worst. 5s is 4.2x the measured worst healthy probe
  (successful probes: min 465ms / median 774ms / max 1197ms across 15
  trials), so the build130 false-condemn regression stays far away.

DEAD-EXIT declaration drops from ~36s measured to ~13-17s total; the warn
now logs the isolated probe_ms. Also adds the gateway-race observability the
verification flagged (race START / WON-by-draw + ms / loser reaped-connected
vs dropped-pending / survivor-after-error), so the race is visible in
[timing] logs instead of inferred statistically.

Verify on the next emulator pass: DEAD EXIT probe_ms <= ~10s, zero false
DEAD EXIT across ~15 cold trials, established keepalive/condemn cadence
unchanged.
2026-07-03 15:55:37 -04:00
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 e6e262009e nym: race two gateway connects on the cold-start path (bound the lottery tail)
The read-tunnel cold/auto path drew ONE random entry gateway; a dead draw blocked connect_to_mixnet() until run_tunnel's 10s cap and consecutive dead draws stacked into a 15-35s tail (measured 6/15 cold starts). With no warm gateway hint, build_tunnel now races TWO ephemeral gateway connects (identical anonymity cfg, ephemeral keys) via connect_gateway_racing and takes the first up; the loser is reaped (disconnected if connected, dropped if pending) so no gateway session leaks. Only the gateway handshake is doubled - the exit/IPR is still built ONCE on the winner. Warm reconnects (cached gateway hint) stay single-build. Money path (streamexit.rs) untouched.

Verify before release: emulator cold-start timing (~15 trials) shows the tail bounded, no session/task leak across many cold starts, warm path unaffected.
2026-07-03 13:52:26 -04:00
2ro 2478a7c08a ui: update banner shows the build number, not GRIM's frozen 0.3.6
The 'Update is available' fallback row rendered crate::VERSION (CARGO_PKG_VERSION = GRIM's 0.3.6) as the current version, e.g. '0.3.6 > build131' — a mismatched, GRIM-leaking string. Show the Goblin build number: 'build130 > build131'.
2026-07-03 13:40:22 -04:00
2ro d95efef896 linux: build_release.sh produces a correct arm AppImage (per-arch runtime)
The arm path hardcoded ARCH=x86_64 and the x86_64 type2 runtime, so a
`build_release.sh arm` run produced a broken AppImage (x86_64 runtime wrapping
an aarch64 binary). Derive ARCH and the runtime file from the target arch, and
have toolchain.sh fetch runtime-aarch64 alongside runtime-x86_64. This restores
GRIM's linux-arm AppImage release type; verified the output is an ARM aarch64
AppImage.
2026-07-03 13:22:47 -04:00
2ro 698806421f deps: nip44 -> crates.io 0.3.0 (drop the ../nip44 sibling requirement)
nip44 is now published, so depend on the registry crate instead of the local
path checkout. A fresh checkout (the Mac included) no longer needs ../nip44
cloned next to goblin - that missing sibling was the reported build failure.
Byte-identical source (0.3.0 was published from this tree); cargo check --lib
green. nym stays a path dep (private fork, not on crates.io).
2026-07-03 11:56:40 -04:00
2ro 61545b767d android: version tracks GOBLIN_BUILD, not GRIM's frozen 0.3.6
versionCode/versionName were inherited from GRIM (5 / "0.3.6") at the fork and
never rewired to Goblin's build number, so every build shipped as versionCode 5
- Android could not tell builds apart and Settings showed 0.3.6 for build 130.
Gradle now derives both from the GOBLIN_BUILD env (the same number the Rust side
stamps into crate::BUILD), with a fallback to the old values for a keyless local
gradle build.
2026-07-03 11:47:38 -04:00
2ro 87f82eb61e nym: race the exit-liveness probe across stable targets + rounds
The single-target, single-shot probe(1.1.1.1:443) was the read tunnel's
liveness gate AND its keepalive. On a slow/unlucky exit->1.1.1.1 path it
false-declared healthy tunnels DEAD, which rejected freshly-built tunnels and
reselected forever ("Connecting to Nym" for minutes or never) and condemned
working tunnels via keepalive. Now it races two anycast targets (1.1.1.1,
9.9.9.9) over two rounds, mirroring the DoT/DoH resolver racing above it: a
tunnel is DEAD only if it reaches NEITHER across both rounds. Verified over 15
cold starts: 15/15 connected, zero DEAD-EXIT, zero hangs (was the sole cause of
the never-resolves).
2026-07-03 11:15:54 -04:00
2ro b3165c3964 relays: default DM relay -> relay.floonet.dev (relay.goblin.st retiring); refresh Cargo.lock for nip44 0.3.0 2026-07-03 03:48:31 -04:00
2ro 4d1db9cb28 chore(relays): drop retiring relay.goblin.st from the pinned pool
relay.goblin.st + nrelay.us-ea.st retire 2026-07-04 04:44 UTC. The live
gist pool already dropped relay.goblin.st; this removes it from the
compiled-in PINNED_POOL fallback so first-run/offline no longer ships a
dead relay. relay.floonet.dev remains THE primary dm+discovery+exit
(money-path) relay.

- pool.rs: remove the relay.goblin.st entry (12 relays now) and repoint
  the pinned-pool tests onto relay.floonet.dev (counts 13->12, dm 11->10,
  discovery 4->3; the dm-membership and exit assertions). Synthetic
  parsing-logic fixtures that name relay.goblin.st are left untouched.
- wallet/e2e.rs: refresh the money-path E2E harness doc comment onto
  relay.floonet.dev (the RELAY_A/RELAY_B consts already point there) so
  the ignored on-chain test documents a live relay after retirement.

Validated: cargo test --lib pool (5 passed), cargo build ok.
2026-07-03 03:44:03 -04:00
2ro 138785cf67 test(payuri): cover the magick.market checkout pay-URI contract
Add round-trip tests asserting a realistic magick.market checkout URI
(nostr:<nprofile>?amount=<decimal GRIN>&memo=MM-<hex>) parses to the exact
recipient / amount / memo. This guards the magick.market <-> Goblin pay-URI
contract: magick converts its internal integer nanogrin to the decimal-GRIN
`amount` string the wallet's amount_from_hr_string accepts, and carries the
opaque invoice number as the memo. Test-only; the parser is unchanged.
2026-07-03 03:08:38 -04:00