@@ -20,7 +20,11 @@ use nostr_sdk::{
2020 PublicKey , RelayPoolNotification , RelayUrl , SingleLetterTag , TagKind , TagStandard , ToBech32 ,
2121} ;
2222use 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
2529use 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
6975impl 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
318398impl 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) ]
13111392mod 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