Build 8: stability fixes, Cash App-style shell, settings, key rotation

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>
This commit is contained in:
Claude
2026-06-10 23:04:40 -04:00
parent 906fee9c71
commit 908df117e6
20 changed files with 2033 additions and 292 deletions
+24 -3
View File
@@ -29,8 +29,13 @@ lazy_static! {
static ref RATE: RwLock<Option<(f64, i64)>> = RwLock::new(None);
static ref FETCHING: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
static ref LAST_TRY: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
}
/// Minimum delay between fetch attempts, so a failing fetch (e.g. Tor still
/// bootstrapping) does not respawn a thread every frame.
const RETRY_SECS: i64 = 30;
fn now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -58,9 +63,14 @@ pub fn grin_usd_rate() -> Option<f64> {
/// Spawn a background refresh over Tor (deduplicated).
fn trigger_refresh() {
use std::sync::atomic::Ordering;
let t = now();
if t - LAST_TRY.load(Ordering::SeqCst) < RETRY_SECS {
return;
}
if FETCHING.swap(true, Ordering::SeqCst) {
return;
}
LAST_TRY.store(t, Ordering::SeqCst);
std::thread::spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
@@ -80,7 +90,18 @@ fn trigger_refresh() {
async fn fetch_rate() -> Option<f64> {
let url =
"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies=usd".to_string();
let body = Tor::http_request("GET", url, None, vec![]).await?;
let doc: serde_json::Value = serde_json::from_str(&body).ok()?;
doc.get("grin")?.get("usd")?.as_f64()
// CoinGecko rejects requests without a User-Agent (403). A static,
// non-identifying UA is fine over Tor.
let headers = vec![("User-Agent".to_string(), "goblin-wallet".to_string())];
let body = Tor::http_request("GET", url, None, headers).await?;
let parsed: Option<f64> = serde_json::from_str::<serde_json::Value>(&body)
.ok()
.and_then(|doc| doc.get("grin")?.get("usd")?.as_f64());
if parsed.is_none() {
log::warn!(
"price: unexpected response from rate API (Tor exit blocked?): {}",
body.chars().take(120).collect::<String>()
);
}
parsed
}