fix(pairing): widen pairing relay set so signer-chosen relays are covered

Amber picks whichever relay it likes from the nostrconnect:// URI and
some relays drop ephemeral kind-24133 traffic, so pairing could appear
successful on the signer while nothing reached us. The pairing set is
now the user's relays UNION a curated fallback of well-known relays;
loopback-only sets (e2e harness) skip widening to keep tests isolated.
This commit is contained in:
Avi 2026-09-12 21:07:27 -05:00
commit f59c2b1b45
3 changed files with 45 additions and 7 deletions

View file

@ -14,6 +14,21 @@ pub fn default_relays() -> Vec<RelayConfig> {
] ]
} }
/// 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<String> {
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. /// Validate that a string is a well-formed relay URL.
pub fn validate_url(raw: &str) -> Result<(), AppError> { pub fn validate_url(raw: &str) -> Result<(), AppError> {
let cleaned = raw.trim().trim_end_matches('/'); let cleaned = raw.trim().trim_end_matches('/');

View file

@ -430,8 +430,13 @@ impl Nip46ClientSigner {
} }
} }
// Pair over the user's enabled relays; fall back to the app defaults // Pair over the user's enabled relays UNION a curated fallback set:
// when none are configured. // 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<RelayUrl> = { let relays: Vec<RelayUrl> = {
let app = self.app.lock().await; let app = self.app.lock().await;
let mut urls: Vec<String> = app let mut urls: Vec<String> = app
@ -441,11 +446,17 @@ impl Nip46ClientSigner {
.filter(|r| r.enabled) .filter(|r| r.enabled)
.map(|r| r.url.clone()) .map(|r| r.url.clone())
.collect(); .collect();
if urls.is_empty() { let loopback_only = !urls.is_empty()
urls = crate::relays::default_relays() && urls.iter().all(|u| {
.into_iter() let u = u.to_lowercase();
.map(|r| r.url) u.contains("127.0.0.1") || u.contains("localhost") || u.contains("[::1]")
.collect(); });
if !loopback_only {
for extra in crate::relays::pairing_relays() {
if !urls.contains(&extra) {
urls.push(extra);
}
}
} }
urls urls
} }

View file

@ -138,6 +138,11 @@ async fn start_relay() -> String {
]) ])
.to_string(); .to_string();
if let Some(sess) = s.sessions.get(session_idx) { 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); let _ = sess.tx.send(out);
} }
break; break;
@ -526,8 +531,13 @@ async fn run_fake_scanner(
continue; continue;
} }
let Some(plain) = nip44_dec(&conversation, &ev.content) else { let Some(plain) = nip44_dec(&conversation, &ev.content) else {
eprintln!(
"[scanner] event from {} not decryptable",
&ev.pubkey.to_hex()[..16]
);
continue; continue;
}; };
eprintln!("[scanner] decrypted: {}", &plain[..plain.len().min(120)]);
let Ok(req) = serde_json::from_str::<Value>(&plain) else { let Ok(req) = serde_json::from_str::<Value>(&plain) else {
continue; continue;
}; };
@ -641,6 +651,8 @@ async fn nip46_qr_pairing_handshake_and_sign() {
else { else {
panic!("pairing URI must be the client variant"); 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_eq!(relays.len(), 1);
assert!(!secret.is_empty()); assert!(!secret.is_empty());