diff --git a/src/relays.rs b/src/relays.rs index 51b26ba..3a6ecfb 100644 --- a/src/relays.rs +++ b/src/relays.rs @@ -14,6 +14,21 @@ pub fn default_relays() -> Vec { ] } +/// Extra relays always included in the NIP-46 *pairing* set (on top of the +/// user's enabled relays). Signer apps (Amber et al.) are free to pick any +/// relay listed in the nostrconnect:// URI, and relays differ wildly in +/// reliability for ephemeral kind-24133 traffic; listening on a few extra +/// well-known relays costs nothing and makes pairing robust whichever one +/// the signer happens to choose. +pub fn pairing_relays() -> Vec { + vec![ + "wss://relay.damus.io".to_string(), + "wss://relay.primal.net".to_string(), + "wss://purplepag.es".to_string(), + "wss://relay.nostr.band".to_string(), + ] +} + /// Validate that a string is a well-formed relay URL. pub fn validate_url(raw: &str) -> Result<(), AppError> { let cleaned = raw.trim().trim_end_matches('/'); diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs index ae64c33..7cbd046 100644 --- a/src/signer/nip46_client.rs +++ b/src/signer/nip46_client.rs @@ -430,8 +430,13 @@ impl Nip46ClientSigner { } } - // Pair over the user's enabled relays; fall back to the app defaults - // when none are configured. + // Pair over the user's enabled relays UNION a curated fallback set: + // signer apps (Amber et al.) pick whichever relay they like from the + // nostrconnect:// URI, and relays vary wildly in reliability for + // ephemeral kind-24133 traffic. Listening on a few extra well-known + // relays costs nothing and covers whichever one the signer chooses. + // Loopback-only relay sets (the e2e harness) skip widening so test + // pairing traffic never touches the real network. let relays: Vec = { let app = self.app.lock().await; let mut urls: Vec = app @@ -441,11 +446,17 @@ impl Nip46ClientSigner { .filter(|r| r.enabled) .map(|r| r.url.clone()) .collect(); - if urls.is_empty() { - urls = crate::relays::default_relays() - .into_iter() - .map(|r| r.url) - .collect(); + let loopback_only = !urls.is_empty() + && urls.iter().all(|u| { + let u = u.to_lowercase(); + u.contains("127.0.0.1") || u.contains("localhost") || u.contains("[::1]") + }); + if !loopback_only { + for extra in crate::relays::pairing_relays() { + if !urls.contains(&extra) { + urls.push(extra); + } + } } urls } diff --git a/tests/nip46_e2e.rs b/tests/nip46_e2e.rs index bf8771e..90d50c6 100644 --- a/tests/nip46_e2e.rs +++ b/tests/nip46_e2e.rs @@ -138,6 +138,11 @@ async fn start_relay() -> String { ]) .to_string(); if let Some(sess) = s.sessions.get(session_idx) { + eprintln!( + "[relay] replay {} to new sub {}", + &ev.id.to_hex()[..16], + sub_id + ); let _ = sess.tx.send(out); } break; @@ -526,8 +531,13 @@ async fn run_fake_scanner( continue; } let Some(plain) = nip44_dec(&conversation, &ev.content) else { + eprintln!( + "[scanner] event from {} not decryptable", + &ev.pubkey.to_hex()[..16] + ); continue; }; + eprintln!("[scanner] decrypted: {}", &plain[..plain.len().min(120)]); let Ok(req) = serde_json::from_str::(&plain) else { continue; }; @@ -641,6 +651,8 @@ async fn nip46_qr_pairing_handshake_and_sign() { else { panic!("pairing URI must be the client variant"); }; + // The e2e relay set is loopback-only, and loopback sets skip the curated + // pairing-relay widening, so the token carries exactly our relay. assert_eq!(relays.len(), 1); assert!(!secret.is_empty());