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.
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.
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.
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.
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.
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.
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.
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.
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).
- 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.
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.
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'.
Names never affect the avatar anymore: claimed and anonymous identities
render identically (picture or pubkey-seeded gradient). Drops
gradient_avatar_ringed/name_ring/is_named and the ringed rows in the
avatar_ring example sheet.
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).
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.
The username-seeded conic-gradient ring is replaced by a single thin,
light-yellow outline that hugs the avatar circle (image or grinmark
orb). It only appears for a claimed name (never a bare npub) and simply
signifies 'this identity has a name' - no per-name color, no gap, no
gradient. Drops the now-dead ring_params seed.
The recipient QR gains optional amount/memo query params:
nostr:<nprofile>?amount=<decimal GRIN>&memo=<percent-encoded>
Scanning a GoblinPay checkout QR now prefills the amount (and the send
note) instead of only the recipient. Bare nostr:<nprofile> is unchanged.
The parser (src/nostr/payuri.rs) is pure and fail-closed over untrusted
scan input: amount is accepted only if the wallet's own
amount_from_hr_string parses it strictly positive; memo is percent-
decoded, control-stripped and 256-byte capped; only the nostr: scheme
unlocks params; 4096-byte cap; embedded NUL rejected; any problem
degrades to recipient-only. PREFILL ONLY - the picker resolver and the
amount/review confirm still gate every send; nothing auto-advances.
Advanced gains a password-gated Nostr key card: reveal the wallet's nsec,
Copy it, or show it as a QR. Scanning that QR (or pasting the copied
nsec) into a nostr app's private-key login - e.g. magick.market - signs
you in with the same identity the wallet uses. The nsec is derived on
demand behind the wallet password and never persisted; wrong password
cannot leak it. Six advanced.* strings added across all six locales.
Fix: a relay-pool cache written by an older build parses fine but lacks
the scoped-exit addresses, which silently disabled the fast money path
for up to 7 days after an update - relay connects rode the slow public
path for minutes. The cache is now ignored and replaced when it lacks
exits, and the pinned pool takes over immediately.
Fix: the scoped-exit mixnet client now prewarms at cold start, so the
sequencer's head start is real (it previously waited on a client that
nothing had started until the first relay dial).
Build: wallet submodule repinned to the upstream grim branch tip
(c2db754) - the previous pin was deleted upstream, breaking CI checkout
and fresh clones. Policy: submodules stay pinned to GRIM's sources.
Lean: drop verified-dead code (avatar upload pipeline, legacy UDP DNS
path, legacy watchdog, duplicate TLS config, one-use error type, dead
store helpers).
The recipient field had a QR-scan button but no paste, so on Android there
was no reliable way to paste an npub. Add the paste button (same one the
slatepack field uses).
The username ring overlapped the gradient orb's edge. Inset the orb and
add a real gap so the ring sits around it without touching; thin the ring
(size*0.045) since it no longer needs to overlap.
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.
Tapping Approve on an incoming request no longer pays immediately — it opens a
full-surface review (who's asking, amount, note, live network fee, privacy,
delivery) with a hold-to-accept gesture, mirroring the send review. Paying a
request is a spend, so it should confirm like one. The NostrPayRequest is
dispatched only when the hold completes; decline is unchanged, and an
over-balance request disables the accept. New goblin.request.review_title /
hold_to_accept / hold_accept_hint across all six locales (drift green).
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.
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.
- 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.
Rephrase the README as 'pay-by-username' and scrub the Cash App name from
code comments. Also drop the leftover 'hosted avatar'/'avatars' wording —
identities use generated identicons, not uploaded pictures.
- Republish kind 0 right after claiming a username (was invisible until restart).
- Request card shows a 'Paying…' spinner instead of a dead greyed button.
- Receipt: count confirmations 1/10…10/10 (was stuck at 0/10, jumped to done
at one block); hide the network-fee row on received payments.
- Settings: one 'Back up to a file' flow (GOBLIN-*.backup) replacing copy-nsec
/ copy-JSON; import accepts a .backup file via the native picker.
- Advanced: 'Run your own node' opens the node-connection page (incl. an
integrated-node option); Repair confirms in accent; Restore warns in red.
- Send: drop the 1/10/100/Max chips; Note becomes an Add-note editor.
- Remove the dead profile-picture upload UI and scrub picture wording.
- Localize all new strings across 6 locales; drift test green.
In dark theme the wallet list, app settings, wallet creation and onboarding
leave the bright accent-yellow title_panel_bg showing under the status bar,
but status_bar_white_icons() returned the dark-theme default (white) — white
icons are illegible on yellow. Force dark icons on every non-Goblin-surface
screen (the Goblin surface covers the inset and sets its own per-tab flag).
- Onboarding goes Intro -> wallet setup directly (connected to a public node
instantly); the node-choice step is retired.
- External node settings stay in Settings; 'run your own node' (integrated) now
lives in the Advanced submenu.
- Wallet-list: title reads GOBLIN (was WALLETS), dropped the redundant GOBLIN
wordmark under the mark, build number kept but smaller.
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).
The right button copied the grin1 slatepack address and the left copied the
nprofile — both wrong for 'how people pay me'. Now: left = Share a friendly
'Pay me on Goblin (goblin.st) — npub1…' message, right = Copy the bare npub.
New receive.copy_npub / receive.share_message keys across all six locales.
The nip05d authority now enforces a 3..=20 length; align the wallet's claim
validation (onboarding + settings) and the 'Names are 3-20 chars' hint across
all six locales so the wallet never offers a name the server will reject.
- 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.
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.
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.
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).
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.
- The profile card drops the duplicate npub line and shows two statuses: the
mixnet ("Connected over Nym", ~instant) and under it the nostr relay
("Connected to nostr" / "Connecting to relays…"). The relay connection is the
slower step, not the mixnet.
- New cheap cached nym::is_ready() flag backs the mixnet status (no per-frame
TCP probe), set when the SOCKS5 proxy comes up.
- Sidebar identity chip: smaller handle so the npub stays on one line, with a
matching Nym/relay status.
- Pairing defaults to Off on first run: no conversion is shown anywhere and no
price request leaves the device until the user opts into a pairing. Off is a
picker option and the choice persists.
- The rate fetch goes back over the Nym mixnet (reverting Build 71's clearnet
route) — it's opt-in and infrequent, so it stays private and doesn't get in
the way of slatepacks. CoinGecko's free-tier rate limit (shared exit IP) rules
out live/1s polling, so it keeps the slow cache.
- Small note on the Pairing page that rates fetch over the mixnet.
On the Pay screen the scan QR drops its dark puck (now a bigger black icon
directly on the yellow), the user's profile picture sits to its right as a link
to the settings/profile page, and the goblin mark is sized to match — all three
controls about 40px.
On the Pay surface the 3-item bar drops its floating pill + shadow — Wallet,
the center ツ, and Activity sit dark directly on the yellow, Cash App-style.
Other surfaces keep the floating pill.
Correct the Build 71 misread of the design notes:
- Amount keeps the ツ unit mark (revert the goblin mark next to the number).
- The goblin mark replaces the "Pay" title at the top-left (tinted dark to read
on the yellow surface).
- The bottom tab-bar strip turns yellow on the Pay surface, like the body.
- (Dark status-bar icons on the yellow surface kept from Build 71.)
Following upstream Grim's posture for non-sensitive metadata: the fiat price
preview and the update check now go direct over HTTPS instead of the mixnet, so
the price shows promptly without waiting on the mixnet bootstrap. Payments,
relays and identity stay mixnet-only.
- price.rs: fetch the rate over clearnet (plain reqwest), no proxy.
- update check: on by default like Grim, repointed at Goblin's own GitHub
releases, build-number aware (is_update compares buildNN), Goblin asset names,
GitHub User-Agent header.
- Pay screen: status-bar icons go dark on the bright yellow surface
(status_bar_white_icons honours a per-frame yellow-surface flag); the hero
amount shows the goblin mark as its unit in place of the ツ glyph.
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.