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.
pub fn validate_url(raw: &str) -> Result<(), AppError> {
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
// 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<RelayUrl> = {
let app = self.app.lock().await;
let mut urls: Vec<String> = 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
}

View file

@ -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::<Value>(&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());