From 286bbcaa041d74b34fc875fef9979adb7ca70f64 Mon Sep 17 00:00:00 2001 From: Avi Date: Sat, 12 Sep 2026 15:29:53 -0500 Subject: [PATCH] fix(signer): connect immediately after identity; fetch kind-0 metadata in background The adopt step blocked on a best-effort relay metadata fetch before flipping the session to Connected, so after Amber approved, the UI sat looking dead for many seconds (the 'Amber said yes but nothing changed' bug). Connected now flips as soon as the identity is verified and persisted; the real display name / picture / nip05 land asynchronously via a background task that never overrides a user-chosen label. --- src/signer/nip46_client.rs | 118 ++++++++++++++++++++++--------------- 1 file changed, 72 insertions(+), 46 deletions(-) diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs index f9a9fa8..18b48fc 100644 --- a/src/signer/nip46_client.rs +++ b/src/signer/nip46_client.rs @@ -1450,58 +1450,22 @@ impl Nip46ClientSigner { .to_bech32() .map_err(|e| format!("Could not encode identity npub: {e}"))?; - // Best-effort: look up the identity's kind-0 metadata so the profile - // row carries the real display name / picture / nip05 instead of a - // bare npub or the generic pairing label. Relay hiccups fall back to - // the pairing label — the connection must not depend on them. - let relays_for_meta = { - let app = self.app.lock().await; - crate::relays::enabled_urls(&app.settings) - }; - let meta_npub = identity; - let metadata = match tokio::time::timeout( - Duration::from_secs(3), - tokio::task::spawn_blocking(move || { - profiles::fetch_profile_metadata(&meta_npub, &relays_for_meta) - }), - ) - .await - { - Ok(joined) => joined.ok().flatten(), - Err(_elapsed) => None, - }; - let display_label = metadata - .as_ref() - .and_then(|m| m.display_name.as_deref().or(m.name.as_deref())) - .map(str::trim) - .filter(|name| !name.is_empty()) - .map(str::to_string) - .unwrap_or(label); + // NOTE: kind-0 metadata (display name / picture / nip05) is fetched + // AFTER the session flips to Connected, as a background enrichment + // (see below). A slow or silent relay must never delay the connection + // becoming usable — this used to block here and made pairing look + // dead in the UI for many seconds after the signer had already + // approved. + let pairing_label = label; + let display_label = pairing_label.clone(); // Persist: profile row for the identity, connection + secret store // re-keyed under it. Refuses (fails the handshake) if the identity // collides with a local profile. { let mut app = self.app.lock().await; - profiles::store_remote_profile(&mut app.vault, &identity_npub, display_label) + profiles::store_remote_profile(&mut app.vault, &identity_npub, display_label.clone()) .map_err(|e| e.message().to_string())?; - // Carry the looked-up metadata (picture / nip05) onto the row so - // the profile shows its real identity, not just a name. - if let Some(meta) = &metadata { - if let Some(row) = app - .vault - .profiles - .iter_mut() - .find(|p| p.public_key == identity_npub) - { - if row.picture.is_none() { - row.picture = meta.picture.clone(); - } - if row.nip05.is_none() { - row.nip05 = meta.nip05.clone(); - } - } - } // Re-key the stored secret under the identity npub so the // connection row, VaultRef, and secret store all agree. let connect_ref = crate::signer::VaultRef::from_connection(&connection); @@ -1541,9 +1505,71 @@ impl Nip46ClientSigner { // so later teardown (disconnect) deletes the secret under the ref it // was actually stored at. if let Some(conn) = inner.connection.as_mut() { - conn.profile_npub = Some(identity_npub); + conn.profile_npub = Some(identity_npub.clone()); } inner.phase = Nip46Phase::Connected; + drop(inner); + + // Background enrichment (post-Connected by design): look up the + // identity's kind-0 metadata so the profile row carries the real + // display name / picture / nip05 instead of the generic pairing + // label. A slow or silent relay must never gate the session — the + // UI polls status and vault, so the row fills in a moment later. + { + let app = self.app.clone(); + tokio::spawn(async move { + let relays_for_meta = { + let app = app.lock().await; + crate::relays::enabled_urls(&app.settings) + }; + let meta = match tokio::time::timeout( + Duration::from_secs(10), + tokio::task::spawn_blocking(move || { + profiles::fetch_profile_metadata(&identity, &relays_for_meta) + }), + ) + .await + { + Ok(Ok(Some(meta))) => meta, + _ => return, + }; + let mut app = app.lock().await; + let mut changed = false; + if let Some(row) = app + .vault + .profiles + .iter_mut() + .find(|p| p.public_key == identity_npub) + { + if row.picture.is_none() { + row.picture = meta.picture.clone(); + changed = true; + } + if row.nip05.is_none() { + row.nip05 = meta.nip05.clone(); + changed = true; + } + // Upgrade the generic pairing label to the real display + // name only while the row still carries the label we set + // during pairing — a user rename always wins. + let real_name = meta + .display_name + .as_deref() + .or(meta.name.as_deref()) + .map(str::trim) + .filter(|name| !name.is_empty()); + if let Some(name) = real_name { + if row.label == pairing_label { + row.label = name.to_string(); + changed = true; + } + } + } + if changed { + let _ = app.save_vault(); + } + }); + } Ok(()) }