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.
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.
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.
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.
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.
- 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.
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.
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.
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.
Adds confirm-before-sent: a payment is not shown as "sent" until the relay
actually confirms receipt. If the path is slow or a send doesn't land, you now
see a clear "not confirmed — retry" instead of a false "sent" that quietly loses
the payment.
Routing is unchanged from prior builds: all wallet nostr traffic — slatepacks,
payment requests, identity, discovery, goblin.st name + relay lookups — rides
the mixnet; the grin node and price/fiat lookups stay direct.
Connect reliability, measured before release:
- Cold start: the wallet races two mixnet entry-gateway connects and takes the
first up, so a dead random draw no longer costs the full timeout (dead-round
rate drops from p to p^2; verified across 30 cold trials).
- Dead exits are condemned in ~10s instead of ~32s (fresh-tunnel probe budget
5s x 2 rounds, 4.2x margin over the worst measured healthy probe), removing
the 50-70s worst-case connects. Established-tunnel keepalive unchanged.
- "Connecting relays..." clears in ~2-4s after saving the relay list (the
Connected flag now tracks the real relay-up signal instead of waiting on a
30s catch-up fetch); identity publishes are time-boxed; the onboarding
Claim button un-gates equally sooner.
- Gateway-race and probe timings are now visible in [timing] logs.
Also: the update banner shows Goblin build numbers (not the inherited 0.3.6),
and the nip44 crate is consumed from crates.io (v0.3.0) - no sibling checkout
needed to build.
Bugfix: the mixnet exit-liveness probe now races two stable targets over two
rounds instead of a single one-shot 1.1.1.1 connect, so a healthy tunnel is no
longer false-condemned and reselected forever. Fixes the intermittent
Connecting to Nym hangs that could take minutes or never resolve (verified:
15/15 cold starts connected, zero hangs).
Also: the Android versionCode/versionName now track the Goblin build number
(were frozen at GRIM's 5 / "0.3.6"), so the OS detects upgrades correctly.
Bugfix: the default and fallback messaging relay moved from relay.goblin.st
(being retired) to relay.floonet.dev, so new installs and reconnects use the
live relay. Built-in relay fallback list updated to match.
Floonet scoped Nym exit on the money-path relay (dial a relay operator's
co-located exit over a MixnetStream, TLS end to end, no public DNS; anchor +
fallback, never pin-only), NIP-44 v3 gift wraps (G4), mix-DNS, localization,
GUI polish. Verified end to end: two wallets complete a real gift-wrapped Grin
payment over relay.goblin.st, finalized + posted on mainnet.
The Settings third-party 'GRIM (upstream wallet)' link opened github.com/ardocrat/grim
— a stale personal fork ~106 commits behind. Point it at github.com/GetGrin/grim, the
live GitHub mirror of canonical code.gri.mw/GUI/grim.