1
0
forked from GRIN/grim

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:
2ro
2026-07-06 17:49:01 -04:00
parent 5da92d0147
commit 2f52df509e
3 changed files with 53 additions and 29 deletions
+43 -17
View File
@@ -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() {
+1 -1
View File
@@ -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
View File
@@ -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)),