goblin: honour the trust relay hint on the channel + clippy clean
- The session channel now subscribes and publishes on the wallet's relays UNION each session's relay hint (channel_relays), dialing any hinted relay via the idempotent connect_relays, so wallet and site meet even without a shared default relay. - Clean up clippy findings in the new code (abs_diff, collapsible ifs via let-chains, char-array find).
This commit is contained in:
+43
-17
@@ -1544,15 +1544,15 @@ async fn run_service(svc: Arc<NostrService>, wallet: Wallet) {
|
||||
// the encrypted channel and publish `session-open` for new sessions;
|
||||
// then sign/decline any money-tier prompts the user answered.
|
||||
if svc.sessions_dirty.swap(false, Ordering::SeqCst) {
|
||||
resubscribe_channel(&client, &relays, &svc).await;
|
||||
announce_new_sessions(&svc, &client, &relays).await;
|
||||
resubscribe_channel(&client, &svc).await;
|
||||
announce_new_sessions(&svc, &client).await;
|
||||
}
|
||||
serve_money_answers(&svc, &client).await;
|
||||
// Drain requests queued while backgrounded on a resume (the Build-95
|
||||
// frame-heartbeat pattern), gated on the app being foregrounded.
|
||||
let fg = crate::app_foreground();
|
||||
if fg && !was_foreground && svc.has_sessions() {
|
||||
drain_channel(&svc, &client, &relays).await;
|
||||
drain_channel(&svc, &client).await;
|
||||
}
|
||||
was_foreground = fg;
|
||||
}
|
||||
@@ -2151,7 +2151,7 @@ async fn handle_channel(svc: &Arc<NostrService>, client: &Client, event: &Event)
|
||||
svc.money_pending.lock().push(p);
|
||||
}
|
||||
if let Some(ev) = publish {
|
||||
let urls = svc.relays();
|
||||
let urls = channel_relays(svc);
|
||||
let _ = tokio::time::timeout(SEND_TIMEOUT, client.send_event_to(&urls, &ev)).await;
|
||||
}
|
||||
}
|
||||
@@ -2186,12 +2186,32 @@ async fn serve_money_answers(svc: &Arc<NostrService>, client: &Client) {
|
||||
}
|
||||
}
|
||||
if let Some(ev) = publish {
|
||||
let urls = svc.relays();
|
||||
let urls = channel_relays(svc);
|
||||
let _ = tokio::time::timeout(SEND_TIMEOUT, client.send_event_to(&urls, &ev)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The relays the session channel runs on: the wallet's own configured relays
|
||||
/// UNION every live session's relay hint, deduplicated. Honouring the site's
|
||||
/// hint (spec 5.9) while keeping the wallet's own relays as fallback is what lets
|
||||
/// the wallet and a site meet even when they share no default relay.
|
||||
fn channel_relays(svc: &Arc<NostrService>) -> Vec<String> {
|
||||
let mut out = svc.relays();
|
||||
for hint in svc
|
||||
.sessions
|
||||
.read()
|
||||
.iter()
|
||||
.filter(|s| !s.ended)
|
||||
.flat_map(|s| s.relays.clone())
|
||||
{
|
||||
if !out.contains(&hint) {
|
||||
out.push(hint);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The channel subscription/fetch filter over the live sessions' wallet channel
|
||||
/// keys, or `None` when there are no sessions. Bounded `since` to the request
|
||||
/// expiration: anything older has lapsed its NIP-40 expiration anyway.
|
||||
@@ -2216,22 +2236,27 @@ fn channel_filter(svc: &Arc<NostrService>) -> Option<Filter> {
|
||||
)
|
||||
}
|
||||
|
||||
/// (Re)subscribe the encrypted session channel over the current session set.
|
||||
async fn resubscribe_channel(client: &Client, relays: &[String], svc: &Arc<NostrService>) {
|
||||
if let Some(filter) = channel_filter(svc) {
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id_to(relays, SubscriptionId::new(CHANNEL_SUB), filter, None)
|
||||
/// (Re)subscribe the encrypted session channel over the current session set,
|
||||
/// dialing any relay hint the wallet is not already connected to first.
|
||||
async fn resubscribe_channel(client: &Client, svc: &Arc<NostrService>) {
|
||||
let relays = channel_relays(svc);
|
||||
// `add_relay`/`connect` are idempotent, so re-dialing already-live relays is
|
||||
// cheap; this brings up any newly hinted relay.
|
||||
connect_relays(client, &relays).await;
|
||||
if let Some(filter) = channel_filter(svc)
|
||||
&& let Err(e) = client
|
||||
.subscribe_with_id_to(&relays, SubscriptionId::new(CHANNEL_SUB), filter, None)
|
||||
.await
|
||||
{
|
||||
warn!("nostr: session-channel subscribe failed: {e}");
|
||||
}
|
||||
{
|
||||
warn!("nostr: session-channel subscribe failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish the one-time `session-open` for every session not yet announced, and
|
||||
/// mark them announced. Called when the session set changes.
|
||||
async fn announce_new_sessions(svc: &Arc<NostrService>, client: &Client, relays: &[String]) {
|
||||
async fn announce_new_sessions(svc: &Arc<NostrService>, client: &Client) {
|
||||
let now = unix_time() as u64;
|
||||
let relays = channel_relays(svc);
|
||||
let mut events = Vec::new();
|
||||
{
|
||||
let mut sessions = svc.sessions.write();
|
||||
@@ -2243,19 +2268,20 @@ async fn announce_new_sessions(svc: &Arc<NostrService>, client: &Client, relays:
|
||||
}
|
||||
}
|
||||
for ev in events {
|
||||
let _ = tokio::time::timeout(SEND_TIMEOUT, client.send_event_to(relays, &ev)).await;
|
||||
let _ = tokio::time::timeout(SEND_TIMEOUT, client.send_event_to(&relays, &ev)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain any channel requests queued on the relay while the wallet was asleep,
|
||||
/// serving each. Called on a background→foreground transition (the Build-95
|
||||
/// frame-heartbeat resume pattern) and once at loop start.
|
||||
async fn drain_channel(svc: &Arc<NostrService>, client: &Client, relays: &[String]) {
|
||||
async fn drain_channel(svc: &Arc<NostrService>, client: &Client) {
|
||||
let Some(filter) = channel_filter(svc) else {
|
||||
return;
|
||||
};
|
||||
let relays = channel_relays(svc);
|
||||
if let Ok(events) = client
|
||||
.fetch_events_from(relays, filter, FETCH_TIMEOUT)
|
||||
.fetch_events_from(&relays, filter, FETCH_TIMEOUT)
|
||||
.await
|
||||
{
|
||||
for ev in events.into_iter() {
|
||||
|
||||
@@ -576,7 +576,7 @@ pub fn sign_session_event(keys: &Keys, ev: &RequestEvent, now: u64) -> Result<Ev
|
||||
|
||||
/// `|a - b|` on unsigned clocks without overflow.
|
||||
fn abs_diff(a: u64, b: u64) -> u64 {
|
||||
if a > b { a - b } else { b - a }
|
||||
a.abs_diff(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+9
-11
@@ -199,10 +199,10 @@ fn validate_callback(raw: &str) -> Option<String> {
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if let Some(rest) = strip_prefix_ignore_case(decoded, "http://") {
|
||||
if is_localhost_authority(rest) {
|
||||
return Some(decoded.to_string());
|
||||
}
|
||||
if let Some(rest) = strip_prefix_ignore_case(decoded, "http://")
|
||||
&& is_localhost_authority(rest)
|
||||
{
|
||||
return Some(decoded.to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -239,10 +239,10 @@ fn validate_relay(raw: &str) -> Option<String> {
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if let Some(rest) = strip_prefix_ignore_case(decoded, "ws://") {
|
||||
if is_localhost_authority(rest) {
|
||||
return Some(decoded.to_string());
|
||||
}
|
||||
if let Some(rest) = strip_prefix_ignore_case(decoded, "ws://")
|
||||
&& is_localhost_authority(rest)
|
||||
{
|
||||
return Some(decoded.to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -295,9 +295,7 @@ fn strip_prefix_ignore_case<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
||||
/// `localhost`, optionally with a `:port` (a valid non-zero u16), followed by
|
||||
/// nothing or a `/ ? #` delimiter. `localhost.evil.com` and friends do NOT pass.
|
||||
fn is_localhost_authority(rest: &str) -> bool {
|
||||
let authority_end = rest
|
||||
.find(|c| c == '/' || c == '?' || c == '#')
|
||||
.unwrap_or(rest.len());
|
||||
let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
|
||||
let authority = &rest[..authority_end];
|
||||
let (host, port) = match authority.split_once(':') {
|
||||
Some((h, p)) => (h, Some(p)),
|
||||
|
||||
Reference in New Issue
Block a user