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.
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.