Skip to content

Commit b4beef6

Browse files
committed
feat: add relay management methods and update NostrClient creation
1 parent 4715817 commit b4beef6

File tree

3 files changed

+157
-44
lines changed

3 files changed

+157
-44
lines changed

crates/bcr-ebill-transport/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use handler::{
2121
IdentityChainEventHandler, IdentityChainEventProcessor, LoggingEventHandler,
2222
NostrContactProcessor, NotificationHandlerApi,
2323
};
24-
use log::{debug, error};
24+
use log::{debug, error, warn};
2525
pub use nostr_transport::NostrTransportService;
2626

2727
mod block_transport;
@@ -54,6 +54,7 @@ pub async fn create_nostr_clients(
5454
config: &Config,
5555
identity_store: Arc<dyn IdentityStoreApi>,
5656
company_store: Arc<dyn CompanyStoreApi>,
57+
nostr_contact_store: Arc<dyn bcr_ebill_persistence::nostr::NostrContactStoreApi>,
5758
) -> Result<Arc<NostrClient>> {
5859
// primary identity is required to launch
5960
let keys = identity_store.get_or_create_key_pair().await.map_err(|e| {
@@ -101,9 +102,17 @@ pub async fn create_nostr_clients(
101102
identities,
102103
config.nostr_config.relays.clone(),
103104
std::time::Duration::from_secs(20),
105+
config.nostr_config.max_relays,
106+
Some(nostr_contact_store),
104107
)
105108
.await?;
106109

110+
// Initial relay refresh to include contact relays
111+
if let Err(e) = client.refresh_relays().await {
112+
warn!("Failed initial relay refresh: {}", e);
113+
// Continue anyway - we have user relays at minimum
114+
}
115+
107116
Ok(Arc::new(client))
108117
}
109118

crates/bcr-ebill-transport/src/nostr.rs

Lines changed: 141 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ use nostr_sdk::{
2020
PublicKey, RelayPoolNotification, RelayUrl, SingleLetterTag, TagKind, TagStandard, ToBech32,
2121
};
2222
use std::sync::{Arc, Mutex, atomic::Ordering};
23-
use std::{collections::{HashMap, HashSet}, sync::atomic::AtomicBool, time::Duration};
23+
use std::{
24+
collections::{HashMap, HashSet},
25+
sync::atomic::AtomicBool,
26+
time::Duration,
27+
};
2428

2529
use bcr_ebill_api::{
2630
constants::NOSTR_EVENT_TIME_SLACK,
@@ -64,6 +68,8 @@ pub struct NostrClient {
6468
relays: Vec<url::Url>,
6569
default_timeout: Duration,
6670
connected: Arc<AtomicBool>,
71+
max_relays: Option<usize>,
72+
nostr_contact_store: Option<Arc<dyn bcr_ebill_persistence::nostr::NostrContactStoreApi>>,
6773
}
6874

6975
impl NostrClient {
@@ -72,6 +78,8 @@ impl NostrClient {
7278
identities: Vec<(NodeId, BcrKeys)>,
7379
relays: Vec<url::Url>,
7480
default_timeout: Duration,
81+
max_relays: Option<usize>,
82+
nostr_contact_store: Option<Arc<dyn bcr_ebill_persistence::nostr::NostrContactStoreApi>>,
7583
) -> Result<Self> {
7684
if identities.is_empty() {
7785
return Err(Error::Message("At least one identity required".to_string()));
@@ -105,13 +113,22 @@ impl NostrClient {
105113
relays,
106114
default_timeout,
107115
connected: Arc::new(AtomicBool::new(false)),
116+
max_relays,
117+
nostr_contact_store,
108118
})
109119
}
110120

111121
/// Creates a new nostr client with the given config.
112122
pub async fn default(config: &NostrConfig) -> Result<Self> {
113123
let identities = vec![(config.node_id.clone(), config.keys.clone())];
114-
Self::new(identities, config.relays.clone(), config.default_timeout).await
124+
Self::new(
125+
identities,
126+
config.relays.clone(),
127+
config.default_timeout,
128+
None, // max_relays not available in old config
129+
None, // contact_store not available
130+
)
131+
.await
115132
}
116133

117134
/// Get the signer for a specific identity
@@ -313,6 +330,69 @@ impl NostrClient {
313330
}
314331
Ok(&self.client)
315332
}
333+
334+
/// Calculate the complete relay set from user relays + contact relays
335+
async fn calculate_relay_set(&self) -> Result<HashSet<url::Url>> {
336+
// Get contacts from store if available
337+
let contacts = if let Some(store) = &self.nostr_contact_store {
338+
store.get_all().await.map_err(|e| {
339+
error!("Failed to fetch contacts for relay calculation: {e}");
340+
Error::Message("Failed to fetch contacts".to_string())
341+
})?
342+
} else {
343+
vec![]
344+
};
345+
346+
Ok(calculate_relay_set_internal(&self.relays, &contacts, self.max_relays))
347+
}
348+
349+
/// Update the client's relay connections to match the target set
350+
async fn update_relays(&self, target_relays: HashSet<url::Url>) -> Result<()> {
351+
let client = &self.client;
352+
353+
// Get current relays
354+
let current_relays: HashSet<url::Url> = client
355+
.relays()
356+
.await
357+
.into_iter()
358+
.filter_map(|(_, relay)| relay.url().as_str().parse::<url::Url>().ok())
359+
.collect();
360+
361+
// Add new relays
362+
for relay in target_relays.iter() {
363+
if !current_relays.contains(relay) {
364+
match client.add_relay(relay).await {
365+
Ok(_) => debug!("Added relay: {}", relay),
366+
Err(e) => warn!("Failed to add relay {}: {}", relay, e),
367+
}
368+
}
369+
}
370+
371+
// Remove old relays (relays not in target set)
372+
for relay in current_relays.iter() {
373+
if !target_relays.contains(relay) {
374+
// Convert url::Url to RelayUrl
375+
if let Ok(relay_url) = relay.as_str().parse::<RelayUrl>() {
376+
match client.remove_relay(relay_url).await {
377+
Ok(_) => debug!("Removed relay: {}", relay),
378+
Err(e) => warn!("Failed to remove relay {}: {}", relay, e),
379+
}
380+
}
381+
}
382+
}
383+
384+
Ok(())
385+
}
386+
387+
/// Public method to refresh relay connections based on current contacts
388+
pub async fn refresh_relays(&self) -> Result<()> {
389+
info!("Refreshing relay connections based on contacts");
390+
let relay_set = self.calculate_relay_set().await?;
391+
self.update_relays(relay_set).await?;
392+
info!("Relay refresh complete, connected to {} relays",
393+
self.client.relays().await.len());
394+
Ok(())
395+
}
316396
}
317397

318398
impl ServiceTraitBounds for NostrClient {}
@@ -1140,7 +1220,7 @@ mod tests {
11401220
(node_id2.clone(), keys2.clone()),
11411221
];
11421222

1143-
let client = NostrClient::new(identities, vec![url], Duration::from_secs(20))
1223+
let client = NostrClient::new(identities, vec![url], Duration::from_secs(20), None, None)
11441224
.await
11451225
.expect("failed to create multi-identity client");
11461226

@@ -1168,7 +1248,7 @@ mod tests {
11681248
(node_id2.clone(), keys2.clone()),
11691249
];
11701250

1171-
let client = NostrClient::new(identities, vec![url.clone()], Duration::from_secs(20))
1251+
let client = NostrClient::new(identities, vec![url.clone()], Duration::from_secs(20), None, None)
11721252
.await
11731253
.expect("failed to create client");
11741254

@@ -1215,7 +1295,7 @@ mod tests {
12151295
];
12161296

12171297
let client = Arc::new(
1218-
NostrClient::new(identities, vec![url.clone()], Duration::from_secs(20))
1298+
NostrClient::new(identities, vec![url.clone()], Duration::from_secs(20), None, None)
12191299
.await
12201300
.expect("failed to create multi-identity client"),
12211301
);
@@ -1261,29 +1341,30 @@ fn calculate_relay_set_internal(
12611341
) -> HashSet<url::Url> {
12621342
use bcr_ebill_core::application::nostr_contact::TrustLevel;
12631343
use std::collections::HashSet;
1264-
1344+
12651345
let mut relay_set = HashSet::new();
1266-
1346+
12671347
// Pass 1: Add all user relays (exempt from limit)
12681348
for relay in user_relays {
12691349
relay_set.insert(relay.clone());
12701350
}
1271-
1351+
12721352
// Filter and sort contacts by trust level
1273-
let mut eligible_contacts: Vec<&bcr_ebill_core::application::nostr_contact::NostrContact> = contacts
1274-
.iter()
1275-
.filter(|c| matches!(c.trust_level, TrustLevel::Trusted | TrustLevel::Participant))
1276-
.collect();
1277-
1353+
let mut eligible_contacts: Vec<&bcr_ebill_core::application::nostr_contact::NostrContact> =
1354+
contacts
1355+
.iter()
1356+
.filter(|c| matches!(c.trust_level, TrustLevel::Trusted | TrustLevel::Participant))
1357+
.collect();
1358+
12781359
// Sort: Trusted (0) before Participant (1)
12791360
eligible_contacts.sort_by_key(|c| match c.trust_level {
12801361
TrustLevel::Trusted => 0,
12811362
TrustLevel::Participant => 1,
12821363
_ => 2, // unreachable due to filter
12831364
});
1284-
1365+
12851366
let limit = max_relays.unwrap_or(usize::MAX);
1286-
1367+
12871368
// Pass 2: Add first relay from each contact (priority order)
12881369
for contact in &eligible_contacts {
12891370
if relay_set.len() >= limit {
@@ -1293,7 +1374,7 @@ fn calculate_relay_set_internal(
12931374
relay_set.insert(first_relay.clone());
12941375
}
12951376
}
1296-
1377+
12971378
// Pass 3: Fill remaining slots with additional contact relays
12981379
for contact in &eligible_contacts {
12991380
for relay in contact.relays.iter().skip(1) {
@@ -1303,14 +1384,14 @@ fn calculate_relay_set_internal(
13031384
relay_set.insert(relay.clone());
13041385
}
13051386
}
1306-
1387+
13071388
relay_set
13081389
}
13091390

13101391
#[cfg(test)]
13111392
mod relay_calculation_tests {
13121393
use super::*;
1313-
use bcr_ebill_core::application::nostr_contact::{NostrContact, TrustLevel, HandshakeStatus};
1394+
use bcr_ebill_core::application::nostr_contact::{HandshakeStatus, NostrContact, TrustLevel};
13141395

13151396
fn create_test_contact(trust_level: TrustLevel, relays: Vec<&str>) -> NostrContact {
13161397
use bcr_ebill_core::protocol::crypto::BcrKeys;
@@ -1335,9 +1416,9 @@ mod relay_calculation_tests {
13351416
];
13361417
let contacts = vec![];
13371418
let max_relays = Some(1); // Very low limit
1338-
1419+
13391420
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1340-
1421+
13411422
// User relays should all be present despite low limit
13421423
assert_eq!(result.len(), 2);
13431424
assert!(result.contains(&url::Url::parse("wss://relay1.com").unwrap()));
@@ -1352,9 +1433,9 @@ mod relay_calculation_tests {
13521433
create_test_contact(TrustLevel::Trusted, vec!["wss://trusted.com"]),
13531434
];
13541435
let max_relays = Some(1);
1355-
1436+
13561437
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1357-
1438+
13581439
// Should only include trusted contact's relay (higher priority)
13591440
assert_eq!(result.len(), 1);
13601441
assert!(result.contains(&url::Url::parse("wss://trusted.com").unwrap()));
@@ -1364,14 +1445,20 @@ mod relay_calculation_tests {
13641445
fn test_one_relay_per_contact_guaranteed() {
13651446
let user_relays = vec![];
13661447
let contacts = vec![
1367-
create_test_contact(TrustLevel::Trusted, vec!["wss://contact1-relay1.com", "wss://contact1-relay2.com"]),
1368-
create_test_contact(TrustLevel::Trusted, vec!["wss://contact2-relay1.com", "wss://contact2-relay2.com"]),
1448+
create_test_contact(
1449+
TrustLevel::Trusted,
1450+
vec!["wss://contact1-relay1.com", "wss://contact1-relay2.com"],
1451+
),
1452+
create_test_contact(
1453+
TrustLevel::Trusted,
1454+
vec!["wss://contact2-relay1.com", "wss://contact2-relay2.com"],
1455+
),
13691456
create_test_contact(TrustLevel::Trusted, vec!["wss://contact3-relay1.com"]),
13701457
];
13711458
let max_relays = Some(3);
1372-
1459+
13731460
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1374-
1461+
13751462
// Should have exactly 3 relays (first relay from each contact)
13761463
assert_eq!(result.len(), 3);
13771464
assert!(result.contains(&url::Url::parse("wss://contact1-relay1.com").unwrap()));
@@ -1383,13 +1470,19 @@ mod relay_calculation_tests {
13831470
fn test_deduplication_across_contacts() {
13841471
let user_relays = vec![];
13851472
let contacts = vec![
1386-
create_test_contact(TrustLevel::Trusted, vec!["wss://shared.com", "wss://unique1.com"]),
1387-
create_test_contact(TrustLevel::Trusted, vec!["wss://shared.com", "wss://unique2.com"]),
1473+
create_test_contact(
1474+
TrustLevel::Trusted,
1475+
vec!["wss://shared.com", "wss://unique1.com"],
1476+
),
1477+
create_test_contact(
1478+
TrustLevel::Trusted,
1479+
vec!["wss://shared.com", "wss://unique2.com"],
1480+
),
13881481
];
13891482
let max_relays = Some(10);
1390-
1483+
13911484
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1392-
1485+
13931486
// Should only include shared.com once
13941487
assert_eq!(result.len(), 3);
13951488
assert!(result.contains(&url::Url::parse("wss://shared.com").unwrap()));
@@ -1405,9 +1498,9 @@ mod relay_calculation_tests {
14051498
create_test_contact(TrustLevel::Trusted, vec!["wss://trusted.com"]),
14061499
];
14071500
let max_relays = Some(10);
1408-
1501+
14091502
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1410-
1503+
14111504
assert_eq!(result.len(), 1);
14121505
assert!(result.contains(&url::Url::parse("wss://trusted.com").unwrap()));
14131506
assert!(!result.contains(&url::Url::parse("wss://banned.com").unwrap()));
@@ -1421,9 +1514,9 @@ mod relay_calculation_tests {
14211514
create_test_contact(TrustLevel::Participant, vec!["wss://participant.com"]),
14221515
];
14231516
let max_relays = Some(10);
1424-
1517+
14251518
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1426-
1519+
14271520
assert_eq!(result.len(), 1);
14281521
assert!(result.contains(&url::Url::parse("wss://participant.com").unwrap()));
14291522
assert!(!result.contains(&url::Url::parse("wss://unknown.com").unwrap()));
@@ -1433,13 +1526,19 @@ mod relay_calculation_tests {
14331526
fn test_no_limit_when_max_relays_none() {
14341527
let user_relays = vec![url::Url::parse("wss://user.com").unwrap()];
14351528
let contacts = vec![
1436-
create_test_contact(TrustLevel::Trusted, vec!["wss://relay1.com", "wss://relay2.com"]),
1437-
create_test_contact(TrustLevel::Trusted, vec!["wss://relay3.com", "wss://relay4.com"]),
1529+
create_test_contact(
1530+
TrustLevel::Trusted,
1531+
vec!["wss://relay1.com", "wss://relay2.com"],
1532+
),
1533+
create_test_contact(
1534+
TrustLevel::Trusted,
1535+
vec!["wss://relay3.com", "wss://relay4.com"],
1536+
),
14381537
];
14391538
let max_relays = None;
1440-
1539+
14411540
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1442-
1541+
14431542
// All relays should be included
14441543
assert_eq!(result.len(), 5);
14451544
}
@@ -1449,9 +1548,9 @@ mod relay_calculation_tests {
14491548
let user_relays = vec![url::Url::parse("wss://user.com").unwrap()];
14501549
let contacts = vec![];
14511550
let max_relays = Some(50);
1452-
1551+
14531552
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1454-
1553+
14551554
assert_eq!(result.len(), 1);
14561555
assert!(result.contains(&url::Url::parse("wss://user.com").unwrap()));
14571556
}
@@ -1463,9 +1562,9 @@ mod relay_calculation_tests {
14631562
contact.relays = vec![]; // Explicitly no relays
14641563
let contacts = vec![contact];
14651564
let max_relays = Some(10);
1466-
1565+
14671566
let result = calculate_relay_set_internal(&user_relays, &contacts, max_relays);
1468-
1567+
14691568
// Should handle gracefully
14701569
assert_eq!(result.len(), 0);
14711570
}

0 commit comments

Comments
 (0)