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.
Pay: stop reddening the amount while typing — requesting more than you hold is
a valid request, so the digits stay black. Pressing Pay without enough funds
now shakes and briefly flashes the amount red and buzzes the phone, then
settles back. Bigger scan-to-pay puck.
Settings: the username "@" moves inside the field as the "@yourname"
placeholder (a leading "@" the user types is stripped). Mixnet routing is
shortened to "All traffic" and flagged in the privacy color. The Build row
links to GitHub releases. Network reads "MW + Nym mixnet + nostr". The Nym
third-party row shows the linked SDK version instead of "socks5".
Wallet: expose GRIM's native by-hand slatepack flow as an advanced
"Slatepacks" page — paste to receive/pay/finalize, or create a payment
slatepack to hand over. The fallback for when a payment can't ride a @username.
CI: fix the red GitHub builds. nym-sdk is a path dep on ../nym, which the
runners didn't have. A composite action materializes the pinned upstream nym
commit plus Goblin's small Android webpki patch before each build; aws-lc-sys
uses prebuilt NASM on native Windows.
Builds: strip release binaries, dropping ~17 MB of debug symbols from the
desktop build.
Goblin now links nym-sdk directly and runs its SOCKS5 client on an
internal tokio runtime exposing 127.0.0.1:1080 — the same loopback seam
the transport already dials. There is no sidecar subprocess and no
bundled/embedded/sideloaded helper binary; the goblin process itself owns
:1080. This mirrors how GRIM links arti/Tor in-process. Verified live: the
mixnet comes up in ~1.4-2s (gateway persisted in ~/.goblin/nym, reused
across launches) and a relay connects in ~2s over it, with no separate
process.
- Cargo.toml: add nym-sdk (path dep on the local nym checkout, which carries
the Android webpki-roots patch) + rustls with the ring feature.
- src/lib.rs: install rustls' ring CryptoProvider at startup. Linking nym-sdk
pulls aws-lc-rs alongside our ring; with two providers present rustls 0.23
won't auto-pick a default and tokio-tungstenite/reqwest panic on the first
TLS handshake. nym uses its own explicit provider, so this only steers our
relay/HTTP TLS.
- src/nym/sidecar.rs: replace the subprocess machinery with an in-process
Socks5MixnetClient (persistent storage; ephemeral fallback) kept alive for
the process lifetime on a dedicated runtime. Drops the binary lookup, embed
extraction, init/launch, and child management.
- build.rs: drop the GOBLIN_NYM_*_BIN embed block (nothing to embed).
- src/nym/{mod,transport}.rs, src/nostr/{mod,avatar}.rs: docs now describe the
in-process client; clear stray "old Tor/arti" wording (no Tor transport code
remains — only grin-core's slatepack OnionV3Address, which is unrelated).
Also: named users now get the pubkey-seeded gradient background with their
initial composited on top (instead of the Grin mark) — gui identicon.rs gains
gradient_bg_svg and widgets.rs gains gradient_letter_avatar; avatar_any routes
named keys to it. Verified live with @nymgoblin.
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.
Build 60 moved the scan-to-pay screen behind a new `scan_open` flag (for the
Scan | My Code toggle), but the recipient picker (Send to / Request from) still
opened the scanner by setting only `self.scan` — which no longer gates the
screen. So tapping the QR icon in the search field silently started the camera
and showed nothing. Set `scan_open` + the Scan tab there too (keeping `scan` so
the Scan branch does not double-start the camera).
Anonymous users (no @handle, no kind-0 picture) had a flat colored tile with an
N - meaningless, since their identity IS the key. Replace it with a pubkey-seeded
two-tone gradient + the Grin mark (avatar = f(pubkey)): same key -> identical
avatar on every surface, nothing to upload/store/sync. Ported the shared
reference (identicon.rs, f64 math, SHA-256 of the lowercase hex seed) and rendered
via egui SVG loader (cached per pubkey). avatar_any/activity_row now take the
npub/hex and use the gradient when the display name is an npub, the lettered tile
otherwise.
Cash App-style scan screen: a segmented Scan | My Code control (new w::segmented)
over either the camera or your own payment QR. My Code shows the @handle above a
big nprofile QR with the Goblin mark nested in a YELLOW center (was white — same
19% footprint, yellow reads as light to a scanner so the High-ECC recovery is
unchanged), and a native Share button. Added share_text to PlatformCallbacks
(Android ACTION_SEND text/plain via a new shareText JNI method; desktop falls
back to clipboard) to share the npub + nprofile link.
Owner disliked the Gamja Flower ツ style on the center Pay puck. Replace it with
Noto Sans JP's cleaner, more geometric katakana tsu — subset to ONLY that one
glyph (~1.7 KB) the same way the old one was, loaded as the "noto-tsu" family and
drawn at the puck (mod.rs). Drop the now-unused Gamja Flower font + its license.
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.
Ship the bundled nym-socks5-client on Windows and Android, not just Linux:
- sidecar.rs resolves the binary per platform — nym-socks5-client.exe on
Windows; on Android the sidecar rides in the APK jniLibs as
libnym_socks5_client.so and is launched from the native-library dir (the
one exec-allowed path), located via NATIVE_LIBS_DIR.
- Restore a vendored, statically-linked OpenSSL. Upstream Grim got this from
arti's static feature; dropping arti for Nym took it with it, which broke
Android/cross builds (no system OpenSSL for the target) and left desktop
dynamically linked to libssl. Inert on Windows/macOS (SChannel/Security.fw).
- android.sh bundles the sidecar into jniLibs per ABI; scripts/nym-android.sh
cross-builds it. Onboarding copy: Tor -> the Nym mixnet.
Verified end to end on an x86_64 emulator: the sidecar extracts, launches,
initialises, and opens the mixnet SOCKS5 proxy on 127.0.0.1:1080.
- Pay tab keypad sits in the lower third (thumb reach) instead of floating in
the middle with a big empty gap below it (pay_ui computes a drop spacer on
phone layouts).
- Review/Confirm "Delivery" reads "NIP-44 encrypted, over Nym" (naming the
actual gift-wrap encryption), and "Privacy" reads "Mimblewimble + Nym" on
the review and the receipt — surfacing both the chain and network layers.
Verified live at 394px across the Pay tab, review, confirm and receipt.
On-device review surfaced UI overflow/centering issues at phone width:
- Balance hero shrinks to fit: amount_text_centered_ink measures the text and
scales the font down when it would run off the edge (e.g. 0.47520721ツ).
- Send flow sits closer to the screen edges (smaller side gutters) and is
properly centered: the Review/Confirm hero now uses a centered, wrapping
layout instead of a fragile manual offset that clipped a long npub and
threw off the centering. info_row values truncate instead of overflowing.
- Recipient picker: the suggested row shows the full npub in the grey subtitle
(truncated to fit) instead of repeating the same shortened npub as the title.
- Success screen uses the goblin head (goblin-logo2), not the mascot mask.
Verified live at 394px across balance, recipient, review, confirm and the
success screen.
- Bake the always-on us-ea.st network requester into NETWORK_REQUESTER so a
shipped wallet routes through the mixnet out of the box (override with
GOBLIN_NYM_PROVIDER). Verified e2e: real wallet-to-wallet payment across two
mainnet nodes (grincoin.org → main.gri.mw), recipient received the exact
amount, all traffic over Nym.
- Fix the sender's displayed amount: a sent tx debits inputs and credits change,
so debited-minus-credited is amount PLUS fee. Subtract the fee so the activity
feed, receipt and history show the value that reached the recipient (0.01),
not the fee-inclusive net (0.033). The receipt's "Network fee" now reads the
real kernel fee instead of the change.
- Replace the remaining user-facing "Tor" copy with Nym (send/request review
delivery line, connection status chips, onboarding, the Privacy settings row).
38 lib tests pass.
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).
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.
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.
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).
Guard against paying more than the spendable balance at every entry: the Pay
tab, the send-flow amount step, and the Review screen all render the amount in
red with "You don't have enough grin" and refuse to proceed, so a payment that
would only fail later at the node is stopped up front. On Android the phone
gives a short error buzz on the blocked tap — new VIBRATE bridge
(PlatformCallbacks::vibrate_error -> MainActivity.vibrateError); no-op on
desktop. Grim silently clamped the amount and showed nothing.
Also enlarge and vertically center the magnifier in the "Send to" search field
(an 18px muted label became a 22px centered glyph).
The amount keypad rows were bunched tight (56px keys, 4px gaps) high on the
Pay screen, leaving a large empty area at the bottom. Now: taller (58) and
wider (pad 332, 14px column gaps) keys, bigger digits (30), and adaptive
row spacing that spreads the four rows toward the bottom when there's room
(the Pay tab) while staying compact where there isn't (the send flow).
Clamped so it never overflows or stretches absurdly.
Grim never lets a field default to constant focus. Its TextEdit defaults
to focus = true, which means an unfocused field calls request_focus()
EVERY frame. Grim's own usage always overrides this — every field is
.focus(false) (focus only on tap), and at most one gets .focus(first_draw)
to grab focus once when a screen opens (see grim wallets/modals/add.rs).
Goblin's converted fields skipped this, so all ~16 fields requested focus
each frame and fought each other on multi-field screens (wallet setup,
import, rotate). Set every field to .focus(false) to match Grim exactly:
fields now focus on tap and bring up the native keyboard, with no thrash.
The native IME path itself is already identical to Grim (verified: edit.rs
differs only by additive display builders; MainActivity onTextInput JNI
plumbing is byte-for-byte Grim; android/winit/eframe/egui/jni deps match).
Build 37 wrongly forced Grim's on-screen virtual keyboard on Android (.soft_keyboard())
after the emulator's broken IME misled me into thinking the native keyboard was dead.
That brought up a virtual keyboard AND double-typed. Reverted:
- Removed .soft_keyboard() from every field; they now use Grim's TextEdit defaults
(no_soft_keyboard = is_android()) → the user's native keyboard on Android, exactly
like Grim, no virtual keyboard, single input.
- Reverted edit.rs IMEAllowed/on_soft_input to Grim's exact behavior; kept only the
additive display builders (hint_text/text_color/body) the Goblin design needs.
- Manifest: removed windowSoftInputMode and the keyboard/keyboardHidden/navigation
configChanges flags I'd added (Grim has none) so input matches Grim; kept the
uiMode/density flags + isFinishing() guard for the dark-mode crash fix.
The app's input path now diffs from Grim only by the additive display builders.
(The Android emulator's IME is aborted regardless — a known emulator quirk — so the
native keyboard is validated on a real device, where Grim works.)
- Typing (critical): the Goblin UI used plain egui::TextEdit, which never
received input because Grim's native-keyboard path only feeds Grim's own
TextEdit widget. The native IME is also aborted by the windowing system on
this app. Fix: every Goblin field now uses Grim's TextEdit, opted into
Grim's own on-screen keyboard on Android via a new .soft_keyboard() builder
(no native-IME dependency). Additive TextEdit options (hint_text/text_color/
body/soft_keyboard) leave Grim's defaults and existing call sites unchanged.
- Install alongside Grim: FileProvider authority mw.gri.android.fileprovider →
st.goblin.wallet.fileprovider (Android rejects duplicate authorities).
- Status bar: app draws edge-to-edge, so dark icons vanished on the dark theme.
New theme-aware JNI call sets light/dark status-bar icons per theme.
- Edge padding: centered_column now keeps an 18dp side gutter on phones instead
of running content flush to the screen edge.
Validated on the API-36 emulator: typed into fields, padding gutter present.
The center Pay puck rendered ツ through the Noto SC fallback (Geist has no
katakana) — a stiff, geometric glyph. Embed a 1.2 KB subset of Gamja Flower
(OFL) containing only ツ and use it solely at that one widget; its ツ is the
cute winking-smiley shape. Every other ツ (balances, title, activity) is
untouched.
- 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
main.gri.mw has intermittent issues, so the default external ("instant") node
is now grincoin.org — both the onboarding first-connect and the head of the
external-node list. Adds the Goblin-run node https://main.us-ea.st as a default
option. (Relays already default to ours, nrelay.us-ea.st, plus relay.damus.io
and nos.lol — no change needed.)
"Keep it" used big_action_on_card (56px / 17px) while "Release it" used
big_action_on_card_ink (44px / 15px), so the two confirm buttons were
visibly different sizes. Both now render with big_action_on_card_ink at the
intended 44px (matching the layout's scope), differing only in text color.
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.
From a security audit of our own nostr/identity code (no P0/P1 found; these
close the P2 hardening gaps):
- NIP-05: only goblin.st identities skip the "pay an unverified key?" gate.
A third-party domain's well-known could point at any key, so those now route
through the same confirm gate as a bare npub.
- NIP-05: validate the domain as a bare hostname before building the
well-known URL — closes a path/host-smuggling (SSRF-over-Tor) vector.
- Avatars: decode server-fed bytes under explicit image Limits (<=1024 px,
8 MiB) so a hostile or breached avatar host can't exhaust memory on the
texture path.
34 lib tests green (incl. new hostname-rejection cases).
- README: credit Claude (Anthropic) for Goblin's development at the bottom,
in place of per-commit co-authorship.
- Onboarding: replace the contradictory "Pay people, not addresses" card with
"Send like a message" and reword the body in plain language (no more
"plumbing"/"ciphertext"). The identity step now says you can rotate your key
anytime to maintain your privacy.
- Home: center the balance hero so it lines up with the Pay amount and the
empty-state below it.
- Lower-left sidebar cards are now individual shortcuts: tapping the
identity chip opens identity settings, tapping the node card jumps
straight to the Node menu (was: both opened generic settings).
- Pay screen gains a scan-to-pay QR puck top-right that opens the camera
and prefills the recipient while keeping the typed amount (reuses the
Home scan + SendFlow::request_scan/prefill_amount path).
- Replace the USD-only "fiat preview" with a configurable "Pairing":
Off / USD / EUR / GBP / JPY / CNY / Bitcoin / Sats (default USD). price.rs
now fetches GRIN against any vs_currency (sats price vs btc, ×1e8) and
caches per code; a Settings → Pairing sub-page picks it; the Pay, send,
and balance previews all route through one pairing_preview() helper.
- Hide the Yellow theme from the picker (cycle is Dark ↔ Light now); the
ThemeKind::Yellow tokens stay defined — it's beta, not removed.
Verified live on :2 (Default wallet): node card → Node menu; identity chip
→ identity settings; QR puck renders top-right of Pay; Pairing → Sats shows
"≈ 1,912 sats" for 50ツ over a live BTC rate, resets to USD; theme cycles
Dark↔Light, never Yellow. 34 lib tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove the Node status chip from the wallet-list header; the integrated
node now lives in the cog's settings as a status + Enable/Autorun section,
with a "Node settings" button into the full stats/mining/tuning panel.
- Stop the desktop two-column rail from auto-docking the node beside the
settings screen, so the node has a single home in the cog at every width
(the full panel opens only on demand). Fixes the wide-window double-exposure.
- Prove NIP-17 payments transit the top public relays: parameterize the
nostr e2e roundtrip and add damus.io + nos.lol proofs (3/3 green).
- Add DEVELOPING.md documenting how we iterate and test (Xwayland :2 recipe,
Build N cadence, gui-sweep, e2e tests, live infra) — no secrets.
Verified live on :2: chip gone from the list; cog shows the node once
(single column, no left rail); "Node settings" opens the full panel on
demand. 34 lib tests green; nip17 roundtrip green over nrelay + damus + nos.lol.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The yellow theme's muted on-background tier (text_mute #6B6A63) rendered
the eyebrow labels (WALLET/PRIVACY/BALANCE/ACTIVITY…) and the "Moving
devices? Back up BOTH…" helper at only 3.85:1 on the #FFD60A background —
below the 4.5:1 AA floor (sweep P2). Darkened to #55534A (5.5:1,
measured), still the faintest of the three on-bg tiers so the hierarchy
holds. Dark/light themes unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wallet-list screen (wallets exist, none open) still wore GRIM's
node-first chrome: a yellow INTEGRATED NODE / WALLETS bar and a forced-
open left node column with ENABLE NODE / Autorun / stat tabs. That
fights the payments-first philosophy the open-wallet surface set — the
node is plumbing, chosen in onboarding and managed in settings, not a
console you stare at to reach your wallets.
Now the list is full-bleed dark like the rest of the goblin surface:
- the node panel no longer force-opens on this screen (content.rs);
it's demoted to a tappable status chip that opens the SAME panel,
every Enable/Autorun/mining/stat feature intact — just opt-in
- the yellow title bar is suppressed here (wallet_list_screen()), its
GEAR (app settings) and node access rehomed into a slim goblin header
(node chip left, gear right) above the GOBLIN logo + wallet rows + "+"
Verified desktop + mobile + panel open/close; unlock still lands on the
unchanged open-wallet surface. 34 lib tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The yellow theme exposed the egui scrollbar track as a stray dark line
down the Settings column (sweep P2). The goblin surface is touch-styled,
so its content scroll areas now hide the bar entirely (AlwaysHidden),
matching the rest of the app and removing the artifact on every theme.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>