The wallet routed its ENTIRE relay.floonet.dev session (own identity, recipient
lookups, profile, catch-up, subscribe, publish) through the scoped Nym exit,
saturating its metered free-tier bandwidth so a payment gift-wrap arrived
minutes late or dropped — while nostr-sdk falsely reported "sent" (it returns on
the local mixnet-stream write, not a relay OK). Root cause was contention: ~97%
of the exit's relay bytes were non-payment overhead.
Split the nostr service into two clients:
- money client (Nym scoped exit): kind-1059 slatepacks + gift-wrap
subscribe/catch-up + recipient resolution — NIP-05 name lookup (already
mixnet via the HTTP tunnel) and the kind-10050 DM-relay lookup (moved here;
it's private payment-target resolution, and cheap). Relays kept, not dropped.
- general client (clearnet): own identity (0/10002/10050), discovery, the fat
kind-0 profile/avatar, general subs, and catch-up of non-1059.
Plus confirm-before-sent: a payment publish is not reported "sent" until a real
relay OK read-back confirms it — a slow/failed exit now surfaces as a retryable
error instead of silent money loss.
Runtime-verified: a normal session puts 0 bytes on the scoped exit (all
clearnet); a kind-1059 slatepack rides the exit and lands on relay.floonet.dev
(exit read-back + independent clearnet oracle). Exit non-payment overhead
dropped from ~50 KiB in / 164 KiB out per session to ~0.
Adds E2E test harnesses (wallet::e2e::funded_e2e_pay,
examples/nostr_split_measure, an exit publish repro in streamexit tests).
Measured (30 cold trials at the gateway-race commit): the worst cold-start
outliers (51s, 72s) were dominated by DEAD-EXIT condemnation cost, not the
gateway. A fresh tunnel on a blackholing exit burned probe_fresh's doubled
patient budget (2 calls x 2 rounds x 8s ~= 32s) before reselecting.
Split the liveness probe into two budgets over a shared probe_with_budget:
- probe() (established tunnels: watchdog keepalive + condemnation checks):
UNCHANGED - 8s x 2 rounds, worst 16s, same patience as before.
- probe_fresh() (gating a just-built tunnel before publish): 5s x 2 rounds
x 1 call = 10s worst. 5s is 4.2x the measured worst healthy probe
(successful probes: min 465ms / median 774ms / max 1197ms across 15
trials), so the build130 false-condemn regression stays far away.
DEAD-EXIT declaration drops from ~36s measured to ~13-17s total; the warn
now logs the isolated probe_ms. Also adds the gateway-race observability the
verification flagged (race START / WON-by-draw + ms / loser reaped-connected
vs dropped-pending / survivor-after-error), so the race is visible in
[timing] logs instead of inferred statistically.
Verify on the next emulator pass: DEAD EXIT probe_ms <= ~10s, zero false
DEAD EXIT across ~15 cold trials, established keepalive/condemn cadence
unchanged.
Symptom: after 'Save & reconnect' of the relay list, the home/onboarding UI sat on 'Connecting relays…' for ~30s even though the relays had physically reconnected over the exit in ~2-4s.
Cause: on restart the service clears connected=false, then the UI flag was only restored AFTER publish_identity (serial, untimed per-event sends) AND a catch-up fetch_events_from bounded by FETCH_TIMEOUT=30s. One relay slow to EOSE pinned is_connected() false for the whole window while the connection was already usable. A separate FAST probe task already detects first-relay-Connected at 250ms poll (~2-4s) and reports relay-live to nymproc, but it did not touch the UI flag.
Fix: in that fast probe, when relays first report Connected (same point that calls report_relay_live), also set svc.connected=true. The indicator now tracks the real ~2-4s relay-up signal; publish_identity + the catch-up fetch continue in the background. Tradeoff (documented in code): a relay drop between the probe store(true) and the 2s status loop taking over wouldn't flip the flag for up to ~30s until the post-catch-up re-check re-syncs to reality — the same-order staleness as the old pessimistic gap, just optimistic; the transport watchdog still tracks real exit health independently.
Hardening: publish_identity's per-event send_event_to was untimed, so a stalled relay delayed the catch-up fetch and the kind:1059 subscription that follow it (real incoming-message latency). Each publish is now wrapped in tokio::time::timeout(SEND_TIMEOUT), mirroring dispatch_dm; on timeout it warns and continues to the next event, never aborting the sequence.
Audit: all readers of is_connected() were reviewed for the relaxed invariant (flag can now be true before the giftwrap subscription is established). gui/goblin/mod.rs and gui/goblin/onboarding.rs use it for display + repaint scheduling and to enable the claim-username button — claiming needs relays connected (which the flag now genuinely means), not the incoming kind:1059 subscription. wallet/e2e.rs uses it as a test precondition with downstream waits of 900s/2400s and relays replay stored gift wraps on subscribe, so it still converges. No reader treats is_connected() as 'safe to receive now', so no separate ui_connected flag is needed.
The read-tunnel cold/auto path drew ONE random entry gateway; a dead draw blocked connect_to_mixnet() until run_tunnel's 10s cap and consecutive dead draws stacked into a 15-35s tail (measured 6/15 cold starts). With no warm gateway hint, build_tunnel now races TWO ephemeral gateway connects (identical anonymity cfg, ephemeral keys) via connect_gateway_racing and takes the first up; the loser is reaped (disconnected if connected, dropped if pending) so no gateway session leaks. Only the gateway handshake is doubled - the exit/IPR is still built ONCE on the winner. Warm reconnects (cached gateway hint) stay single-build. Money path (streamexit.rs) untouched.
Verify before release: emulator cold-start timing (~15 trials) shows the tail bounded, no session/task leak across many cold starts, warm path unaffected.
The 'Update is available' fallback row rendered crate::VERSION (CARGO_PKG_VERSION = GRIM's 0.3.6) as the current version, e.g. '0.3.6 > build131' — a mismatched, GRIM-leaking string. Show the Goblin build number: 'build130 > build131'.
The single-target, single-shot probe(1.1.1.1:443) was the read tunnel's
liveness gate AND its keepalive. On a slow/unlucky exit->1.1.1.1 path it
false-declared healthy tunnels DEAD, which rejected freshly-built tunnels and
reselected forever ("Connecting to Nym" for minutes or never) and condemned
working tunnels via keepalive. Now it races two anycast targets (1.1.1.1,
9.9.9.9) over two rounds, mirroring the DoT/DoH resolver racing above it: a
tunnel is DEAD only if it reaches NEITHER across both rounds. Verified over 15
cold starts: 15/15 connected, zero DEAD-EXIT, zero hangs (was the sole cause of
the never-resolves).
relay.goblin.st + nrelay.us-ea.st retire 2026-07-04 04:44 UTC. The live
gist pool already dropped relay.goblin.st; this removes it from the
compiled-in PINNED_POOL fallback so first-run/offline no longer ships a
dead relay. relay.floonet.dev remains THE primary dm+discovery+exit
(money-path) relay.
- pool.rs: remove the relay.goblin.st entry (12 relays now) and repoint
the pinned-pool tests onto relay.floonet.dev (counts 13->12, dm 11->10,
discovery 4->3; the dm-membership and exit assertions). Synthetic
parsing-logic fixtures that name relay.goblin.st are left untouched.
- wallet/e2e.rs: refresh the money-path E2E harness doc comment onto
relay.floonet.dev (the RELAY_A/RELAY_B consts already point there) so
the ignored on-chain test documents a live relay after retirement.
Validated: cargo test --lib pool (5 passed), cargo build ok.
Add round-trip tests asserting a realistic magick.market checkout URI
(nostr:<nprofile>?amount=<decimal GRIN>&memo=MM-<hex>) parses to the exact
recipient / amount / memo. This guards the magick.market <-> Goblin pay-URI
contract: magick converts its internal integer nanogrin to the decimal-GRIN
`amount` string the wallet's amount_from_hr_string accepts, and carries the
opaque invoice number as the memo. Test-only; the parser is unchanged.
Cold connect gets a fast path: the last-known-good entry gateway and IPR
are persisted (choices only, keys stay ephemeral) and tried first on the
next launch - measured 4.4s to tunnel-ready vs 5.6s cold, and no dead-
random-gateway lottery. Cached hints self-clear on failure and fall back
to auto-select.
The price appears instantly: the last fetched rate (under 48h) paints on
the first frame and a live fetch fires the moment the tunnel is ready
instead of waiting for the balance screen. The eager fetch doubles as an
end-to-end probe: if the first reads all fail while the tunnel claims
ready, the exit is condemned and reselected in seconds instead of
minutes. warm_up now runs first at startup.
Money path (streamexit.rs, transport.rs) untouched; ponytail sweep
verified all fallback paths and gates green.
Names never affect the avatar anymore: claimed and anonymous identities
render identically (picture or pubkey-seeded gradient). Drops
gradient_avatar_ringed/name_ring/is_named and the ringed rows in the
avatar_ring example sheet.
Fire a separate high-importance system notification when an incoming
payment request (Invoice1 -> SurfaceRequest) is ingested over nostr,
mirroring the existing received-payment notification (id=2). Fail-open on
a missing JNI handle; fires once per not-yet-seen slate. No-op off Android.
Also add examples/tunnel_measure.rs, a dev harness for measuring the Nym
read tunnel (cold connect + warm per-fetch latency over the real transport).
The in-process smolmix tunnel was far slower than the old SOCKS5 model.
Fixes:
- smolmix TCP buffers 8KB -> 256KB and burst 1 -> 64: bulk throughput
ceiling rises ~32x (was capping relay backfill and JSON reads at a few
KB/s).
- Read tunnel runs a high-traffic mixnet profile (cover traffic off, higher
send rate, fewer reply SURBs) for lower latency; per-hop mix delays kept.
The scoped-exit money-path client is separate and unchanged.
- HTTP over the mixnet now reuses connections (keep-alive pool) instead of a
fresh TCP+TLS+HTTP handshake per request - fixes slow price and username
reads.
- DNS prewarm no longer skips on cold start and serves stale-while-revalidate
for known hosts, so a dial never blocks on DoT/DoH.
Money path (streamexit.rs, transport.rs) byte-for-byte unchanged.
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 in-app updater built the ARM Android download as goblin-<tag>-
android-arm.apk and the Linux download as .AppImage, but releases ship
goblin-<tag>-android-arm.apk (this build on) and a linux .tar.gz. On an
older release the ARM apk was named -android.apk, so the in-app update
404'd. Releases now name the ARM apk -android-arm.apk and the updater
targets the tar.gz, so Android and Linux in-app updates resolve.
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 180s cold connect was the public-IPR path (nested TCP over the mixnet) plus
the public-exit lottery -- NOT the scoped exit, which was removed by mistake in
3372202. Restore the co-located MixnetStream exit: a relay connects in 0-2s over
it (measured), vs 15-180s over the tunnel.
- Cold-start sequencer: streamexit::is_ready + a bounded EXIT_HEAD_START gate in
nymproc so the exit client claims its Nym bandwidth grant before the tunnel,
avoiding two-client serialization (~1min otherwise). No SDK surgery.
- Pin relay.floonet.dev as the primary money-path relay (with its co-located
exit) in PINNED_POOL; keep relay.goblin.st as a secondary through transition.
- E2E: a funded 0.1 GRIN payment finalizes in 6s over the exit across two
different Grin nodes (grincoin.org, main.gri.mw).
The exit is unpinned everywhere, so the streamexit module + the exit_for/
exit_connect forks in transport + http were dead code. Remove them; the
wallet's only mixnet path is the smolmix tunnel.
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 money-path exit ran a second mixnet client whose cold bootstrap blocked
first-connect ~100s per session. Unset the exit so the wallet reaches
relay.goblin.st over the fast smolmix tunnel (still fully over the mixnet).
Privacy backbone unchanged; the exit stays deployed for a non-blocking
redesign later.
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.
The name service no longer exposes /api/v1/transfer (removed server-side), and
the model is release-and-reclaim, not transfer: on a key rotation you release
the old name and re-register (or import your existing identity). Removed the dead
nip05::transfer() client fn (it had no callers) and the "transfer" wording in the
README.
Upstream Grim advanced past the fork base with a node + wallet version update
(b51a46b → its "node + wallet: update to latest versions"). Bumped both
submodules (node → bce5a71, wallet → c2db754) and applied the one source
adaptation that update requires: `tx_log_iter()` now yields Result items, so the
three call sites filter Ok + unwrap before use. The upstream Tor/arti-0.43 commit
is skipped — Goblin removed Tor entirely.
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.
- The macOS bundle still shipped the old GRIM paw AppIcon.icns. Regenerated it
from Goblin-Logo-Gradient-SMALLER.png (the yellow-gradient goblin mark) as a
proper multi-resolution PNG-icns (16–512), so Finder/Dock show the Goblin icon.
- Declining (or cancelling) a request never re-resolved the counterparty, so
their @name could drop to a bare npub just because the request didn't go
through. handle_wrap now re-resolves the counterparty after a void — cheap,
authoritative via the by-pubkey reverse lookup, and a no-op for anonymous keys.
The requester still saw a bare npub for the payer because resolving a name
depended on fetching the peer's kind-0 off a relay (a nostr REQ with tight
timeouts over the private transport) before it could verify the NIP-05. When
that fetch doesn't land, resolution never starts — even though the name server
knows the name perfectly well.
resolve_contact_identity now asks the home authority directly:
GET /api/v1/by-pubkey/{hex} → the active @name for that key, in one HTTP
round-trip, with no profile fetch. That answer is authoritative, so the name is
set verified immediately. The kind-0 + verify path stays as a fallback for
FOREIGN authorities (which the home server can't speak for) and is still what
CLEARS a released/reassigned name. New nip05::name_by_pubkey helper.
Pairs with the goblin-nip05d by-pubkey endpoint. Verified by me only as far as
compile + unit/i18n tests; the live two-party resolution is the owner's call.
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.
Cached names were verified once and never re-checked, so a contact who
released or changed their username kept showing the stale name forever.
Re-validate names on a 78s sweep (capped per tick to bound mixnet lookups):
- nip05::check — tri-state Verified/Mismatch/Unreachable, so we only clear on
a definitive server answer (released, or reassigned to a different key),
never on a network blip.
- resolve_contact_identity now re-checks names older than the freshness window
and clears nip05 + nip05_verified_at on Mismatch (a user petname is kept);
display falls back to the npub automatically.
- A periodic sweep in run_service re-verifies the stalest due contacts.
Tests for the tri-state parsing and the clear-keeps-petname logic.
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.
- Create/refresh a contact when you SEND (not just receive) so people you
pay show up under Suggested and resolve their @name.
- Approve-request: set SENT on the Standard1 success path and fail_send on
the error + non-payable paths, so the button never sticks greyed.
- create_nostr_backup() + import of the encrypted .backup envelope.
Add to_encrypted_backup / from_encrypted_backup: the secret key is the
password-protected NIP-49 ncryptsec, and the rest of the identity is
NIP-44-sealed to our own key — so a GOBLIN-*.backup file leaks no npub or
username, yet any Goblin wallet reopens it with the password.
Profiles never loaded when scanning a bare npub (username/avatar stayed
blank) even though the relay stores and serves the kind-0 fine. Two causes:
fetch_profile_blocking ran on a throwaway current-thread runtime that can't
drive the relay connections (which live on the service runtime, behind the
custom Nym mixnet transport), and it only dialed nprofile hints, never the
user's own default relays. Run the fetch on the service runtime via a stored
Handle, and always dial the default relay set (incl relay.goblin.st).
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.