- 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.
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).
- 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.)
Identity overhaul (server changes deployed to goblin.st separately):
Avatars
- profile pictures hosted on goblin.st, tied to the username: tap the
settings avatar → native image picker → 256px PNG uploaded over Tor
- letter avatars now use (background, ink) color pairs (8) keyed off the
npub, and render the first ALPHANUMERIC char — never the '@'
- custom pictures shown everywhere self/contacts appear: settings card,
home header, sidebar chip, peers strip, activity rows, send recipient
- AvatarTextures: disk cache (~/.goblin/cache/avatars) + background Tor
fetch + egui textures loaded on the UI thread; a network/Tor failure
is never cached as "no avatar" (would have stuck for 6h)
- nostr/avatar.rs mirrors the server's sniff→limits→orientation→crop→
256→re-encode-PNG pipeline so uploads are small and previews instant
Username lifecycle
- rotating the key now RELEASES the username (and deletes its avatar
server-side) instead of transferring; rotation aborts if release fails
- claim panel is one Claim button (checks then registers); registered
state shows "Registered <name>" + a Release action behind an
are-you-sure gate ("up for grabs the moment it's free")
- released names are immediately re-claimable (quarantine removed)
Other
- Tor::http_request_bytes: binary bodies + status code, for upload and
avatar download (string http_request kept as a wrapper)
- settings reordered Identity-first, then Wallet
- sidebar node card is 3 lines: status / block height / host
- profile card shows the full npub when it fits, else head…tail
34 lib tests green. Live-verified on goblin.st: upload→serve (image/png,
nosniff, immutable)→5/day limit (6th 429)→release purges avatar; a real
picture for @fartmuncher22 fetched over Tor and rendered across surfaces.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Camera/QR (validated live with a v4l2loopback virtual camera):
- decode non-MJPEG frames on Linux (raw YUYV was undecodable: eternal
spinner), enumerate devices off the UI thread without unwrap, open
cameras by their real device index (list position broke whenever
/dev/video0 was absent), show "No camera found" after 5s
- scan-to-pay: QR button in the send recipient search row and the
mobile home header; in-surface camera panel feeding recipient
resolution; only text payloads accepted (seeds/slatepacks refused)
- receive QR was unscannable: full cells (0.5px gaps fragmented finder
patterns at 4.5px cells), always ink-on-white plate (inverted codes
fail many scanners), center mark 26% -> 19% (zbar chokes above);
rqrr+zbar both decode the live card now; unit tests cover the exact
widget geometry
First-run onboarding (replaces the stock empty state only; the wallet
list, add-wallet modal and GRIM creation flow stay for later wallets):
- intro -> node choice (Private integrated / Instant external with
URL) -> create or restore (wraps MnemonicSetup; word grid, paste,
SeedQR scan) -> optional @username claim with prominent skip
- fonts bind at creation context so frame one can use Geist families
Sweep fixes (22 confirmed findings, the simple ones):
- yellow theme: active sidebar nav was dark-on-dark (P1)
- window frame ring painted with theme bg: transparent clear color
rendered as a black band without a compositor (P2)
- import/rotate identity inputs get visible field wells (P2)
- receive buttons: verb labels + transient "Copied" feedback
- review hero card fills the column; fee row copy; pay-tab stale and
duplicated hints; unlock modal copy; node card subtitle truncation
- build number stays current (.git/logs/HEAD rerun trigger)
Backup restore: import accepts the export-time password and re-encrypts
under the current wallet password; cross-device restores work.
31 lib tests green. Mainnet-validated: both flows restored real
wallets; 0.1 grin sent B->A through NIP-17 over Tor with async
open/close ping-pong; tx 71fbfce4f591 posted on-chain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stability (found via GUI testing):
- tor: create arti runtime on a clean thread; lazy TOR_STATE init panicked
inside tokio contexts and poisoned the whole Tor/nostr stack
- store: open rkv SafeMode envs with capacity headroom; reopening at
exactly DEFAULT_MAX_DBS crashed every wallet restart (DbsFull)
- goblin ui: centered_column hands children a full-height rect; ScrollAreas
inside clipped everything below the first widget
- build: webtunnel client was silently embedded as 0 bytes without Go;
warn at build, extract with create_dir_all + exec bit at runtime
- price: CoinGecko requires a User-Agent (403 otherwise); add retry-once
backoff and a parse-failure diagnostic
- tor: retry http_request up to 3x on fresh isolated circuits
UX overhaul (per owner direction + Cash App references):
- floating icon-only 3-tab pill: Wallet / center accent ツ Pay puck /
Activity (requests badge kept); Me opens via header avatar
- Pay tab: amount-first surface (numpad on mobile, typed on desktop) with
Request and Pay; Pay carries the amount straight to Review
- Request flips Receive into "Requesting Nツ" state with a clear chip
- full-bleed goblin surface: GRIM title panel and network column hidden
while a wallet is open; node status card lives in the sidebar above the
profile chip; Lock wallet row added (Settings -> Wallet)
- goblin branding: titlebar, wallet-list logo + GOBLIN, new mark assets
(white master, theme-tinted) in wordmark, QR center, wallet list
- build-based versioning: Build N = commits since the GRIM fork base,
emitted by build.rs; About leads with it, Third party credits GRIM,
grin node, nostr-sdk, arti, egui and the implemented NIPs
Accessibility & settings:
- surface_text{,_dim,_mute} tokens: yellow theme has dark cards on a
bright bg; all on-surface text now readable in every theme (incl. QR)
- settings rows clickable across the whole row; profile card fills width;
density option removed (comfy fixed)
- editable Node connections (integrated/external, add/remove) and Relays
(add/remove + live service restart); NIPs explainer page with goblin.st
context; third-party rows link to upstream projects
- standardized npub truncation (head 12 ... tail 6) shown in profile
Identity (owner decision: drop NIP-06 seed binding):
- random standalone nsec (Keys::generate); seed proves nothing about the
identity and cannot resurrect it; legacy Derived identities still unlock
- key rotation: double warning (pending payments disrupted), typed RESET +
wallet password, fresh random key, username moved atomically via the
name server transfer endpoint; aborts cleanly if the move fails
- encrypted identity backup export (NIP-49 ncryptsec JSON, includes
username + history) and import accepting nsec or backup JSON
- nip05d: POST /api/v1/transfer (NIP-98 by current owner, atomic
owner-guarded swap, one-name-per-pubkey enforced) + SQL invariant tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Send flow:
- "Sent" success now gates on the real dispatch result (NostrService send
phase Working/Sent/Failed) instead of send_creating, which cleared before
the DM left. Added a Failed stage with Try again / Close so a failed send
is no longer shown as success.
- NIP-05 handle resolution moved off the UI thread (was .join()-blocking the
render thread for the Tor timeout); now a worker thread writes into a polled
slot with an inline "Looking up…" spinner.
- Desktop amount entry no longer steals keystrokes from the Note field (guards
on the note TextEdit focus).
- Review screen now shows a network-fee line.
Requests / identity / archive:
- Approve is now double-tap safe (per-session approving set + disabled button),
closing a double-pay window.
- "Backup nostr key" copied the public npub; split into "Copy npub (public)"
and "Back up secret key (nsec)" which copies the real secret.
- Added inline username claim (availability check + NIP-98 register over Tor,
off-thread) for anonymous users; persists nip05 + republishes identity.
- Added archive Export / Wipe history controls.
- Receive "Slatepack" button now copies the grin1 address; "Scan" tab relabeled
"Receive" (no scanner was wired).
- Goblin view resets on wallet switch so a half-filled send can't leak across.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wallet:
- identity.json (NIP-49 ncryptsec) now written 0600 in a 0700 dir so a local
user can't grind the wallet password offline (+ regression test).
- Wallet password held as a ZeroingString through init_nostr so it's scrubbed
on drop instead of lingering in a plain String for the session.
- Replaced 4 .unwrap() on re-read tx_meta with graceful guards (archive wipe
mid-send could otherwise panic the nostr/task thread).
- Tor::http_request/post: bind the client once via let-else and propagate TLS
builder errors, fixing a TOCTOU unwrap panic on concurrent Tor restart.
goblin-nip05d server (redeployed to goblin.st, verified live):
- One-name-per-pubkey now enforced by a partial UNIQUE index (closes the
check-then-insert race); INSERT rows-affected==0 returns 409 not a false 201.
- NIP-98 replay protection: one-time auth event-id enforcement within the
freshness window; tightened forward skew to +5s.
- Rate-limited the unauthenticated GET endpoints; SQLite in WAL mode.
- Verified live: replay rejected, second name for a pubkey blocked.
Audit verdict: fund-safety invariants (never auto-pay Invoice1; S2/I2
finalization bound to counterparty npub) and Tor-from-day-one all hold.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Infrastructure (P0): deployed nostr-rs-relay (wss://nrelay.us-ea.st) and the
goblin-nip05d NIP-05 service (goblin.st) on us-ea.st with TLS + DNS.
Brand & theme (P1): Goblin name/icon/data-dir (.goblin); three-theme token
system (light/dark/yellow) in gui/theme.rs with colors.rs remapped as a shim;
Geist + Geist Mono fonts; AppConfig theme/density/last_wallet_id.
Nostr subsystem (P2-P3): src/nostr/ with NIP-06 identity (seed-derived,
NIP-49 encrypted), per-wallet rkv archive, guarded ingest policy (never
auto-pays Invoice1; binds replies to the stored counterparty npub), NIP-17
send/receive pipeline, NIP-05 client. Relay traffic routed over the embedded
arti Tor client via a custom WebSocketTransport. Wired into Wallet lifecycle
and the task handler. 26 unit tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Limit loading at tix list
- Sort txs by confirmation status to show txs waiting for an action at top
- Ability to delete wallet from the list without opening
- Optimize Tor connection on account switch
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/53
- Ability to export and verify payment proofs
Some fixes:
- Migrated tx heights store from lmdb (also changed heights key from local id to slate_id to avoid conflict between wallets)
- Close address panel on wallet change
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/52
- Wgpu renderer by default for Windows
- Fix items borders sizes
- Start camera at messages on scan press
- Do not show pull-to-refresh on empty tx list
- Do not close non-closeable modal on Back/Esc key press
- Cut long connection name at wallet list
- Fix setting of tx receiver address
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/47
- Update common dependencies
- Optimize check of running Tor service
- Async peer saving
- Add mainnet and testnet seeds
- Remove grinnode.live from default external connections
Reviewed-on: https://code.gri.mw/GUI/grim/pulls/43