@@ -20,7 +20,7 @@ use nostr_sdk::{
2020 PublicKey , RelayPoolNotification , RelayUrl , SingleLetterTag , TagKind , TagStandard , ToBech32 ,
2121} ;
2222use std:: sync:: { Arc , Mutex , atomic:: Ordering } ;
23- use std:: { collections:: HashMap , sync:: atomic:: AtomicBool , time:: Duration } ;
23+ use std:: { collections:: { HashMap , HashSet } , sync:: atomic:: AtomicBool , time:: Duration } ;
2424
2525use bcr_ebill_api:: {
2626 constants:: NOSTR_EVENT_TIME_SLACK ,
@@ -1251,3 +1251,222 @@ mod tests {
12511251 tasks. abort_all ( ) ;
12521252 }
12531253}
1254+
1255+ /// Internal relay calculation function (pure function for testing)
1256+ fn calculate_relay_set_internal (
1257+ user_relays : & [ url:: Url ] ,
1258+ contacts : & [ bcr_ebill_core:: application:: nostr_contact:: NostrContact ] ,
1259+ max_relays : Option < usize > ,
1260+ ) -> HashSet < url:: Url > {
1261+ use bcr_ebill_core:: application:: nostr_contact:: TrustLevel ;
1262+ use std:: collections:: HashSet ;
1263+
1264+ let mut relay_set = HashSet :: new ( ) ;
1265+
1266+ // Pass 1: Add all user relays (exempt from limit)
1267+ for relay in user_relays {
1268+ relay_set. insert ( relay. clone ( ) ) ;
1269+ }
1270+
1271+ // Filter and sort contacts by trust level
1272+ let mut eligible_contacts: Vec < & bcr_ebill_core:: application:: nostr_contact:: NostrContact > = contacts
1273+ . iter ( )
1274+ . filter ( |c| matches ! ( c. trust_level, TrustLevel :: Trusted | TrustLevel :: Participant ) )
1275+ . collect ( ) ;
1276+
1277+ // Sort: Trusted (0) before Participant (1)
1278+ eligible_contacts. sort_by_key ( |c| match c. trust_level {
1279+ TrustLevel :: Trusted => 0 ,
1280+ TrustLevel :: Participant => 1 ,
1281+ _ => 2 , // unreachable due to filter
1282+ } ) ;
1283+
1284+ let limit = max_relays. unwrap_or ( usize:: MAX ) ;
1285+
1286+ // Pass 2: Add first relay from each contact (priority order)
1287+ for contact in & eligible_contacts {
1288+ if relay_set. len ( ) >= limit {
1289+ break ;
1290+ }
1291+ if let Some ( first_relay) = contact. relays . first ( ) {
1292+ relay_set. insert ( first_relay. clone ( ) ) ;
1293+ }
1294+ }
1295+
1296+ // Pass 3: Fill remaining slots with additional contact relays
1297+ for contact in & eligible_contacts {
1298+ for relay in contact. relays . iter ( ) . skip ( 1 ) {
1299+ if relay_set. len ( ) >= limit {
1300+ return relay_set;
1301+ }
1302+ relay_set. insert ( relay. clone ( ) ) ;
1303+ }
1304+ }
1305+
1306+ relay_set
1307+ }
1308+
1309+ #[ cfg( test) ]
1310+ mod relay_calculation_tests {
1311+ use super :: * ;
1312+ use bcr_ebill_core:: application:: nostr_contact:: { NostrContact , TrustLevel , HandshakeStatus , NostrPublicKey } ;
1313+ use std:: collections:: HashSet ;
1314+
1315+ fn create_test_contact ( trust_level : TrustLevel , relays : Vec < & str > ) -> NostrContact {
1316+ use bcr_ebill_core:: protocol:: crypto:: BcrKeys ;
1317+ let keys = BcrKeys :: new ( ) ;
1318+ let node_id = NodeId :: new ( keys. pub_key ( ) , bitcoin:: Network :: Testnet ) ;
1319+ NostrContact {
1320+ npub : node_id. npub ( ) ,
1321+ node_id,
1322+ name : None ,
1323+ relays : relays. iter ( ) . map ( |r| url:: Url :: parse ( r) . unwrap ( ) ) . collect ( ) ,
1324+ trust_level,
1325+ handshake_status : HandshakeStatus :: None ,
1326+ contact_private_key : None ,
1327+ }
1328+ }
1329+
1330+ #[ test]
1331+ fn test_user_relays_always_included ( ) {
1332+ let user_relays = vec ! [
1333+ url:: Url :: parse( "wss://relay1.com" ) . unwrap( ) ,
1334+ url:: Url :: parse( "wss://relay2.com" ) . unwrap( ) ,
1335+ ] ;
1336+ let contacts = vec ! [ ] ;
1337+ let max_relays = Some ( 1 ) ; // Very low limit
1338+
1339+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1340+
1341+ // User relays should all be present despite low limit
1342+ assert_eq ! ( result. len( ) , 2 ) ;
1343+ assert ! ( result. contains( & url:: Url :: parse( "wss://relay1.com" ) . unwrap( ) ) ) ;
1344+ assert ! ( result. contains( & url:: Url :: parse( "wss://relay2.com" ) . unwrap( ) ) ) ;
1345+ }
1346+
1347+ #[ test]
1348+ fn test_trusted_contacts_prioritized ( ) {
1349+ let user_relays = vec ! [ ] ;
1350+ let contacts = vec ! [
1351+ create_test_contact( TrustLevel :: Participant , vec![ "wss://participant.com" ] ) ,
1352+ create_test_contact( TrustLevel :: Trusted , vec![ "wss://trusted.com" ] ) ,
1353+ ] ;
1354+ let max_relays = Some ( 1 ) ;
1355+
1356+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1357+
1358+ // Should only include trusted contact's relay (higher priority)
1359+ assert_eq ! ( result. len( ) , 1 ) ;
1360+ assert ! ( result. contains( & url:: Url :: parse( "wss://trusted.com" ) . unwrap( ) ) ) ;
1361+ }
1362+
1363+ #[ test]
1364+ fn test_one_relay_per_contact_guaranteed ( ) {
1365+ let user_relays = vec ! [ ] ;
1366+ 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" ] ) ,
1369+ create_test_contact( TrustLevel :: Trusted , vec![ "wss://contact3-relay1.com" ] ) ,
1370+ ] ;
1371+ let max_relays = Some ( 3 ) ;
1372+
1373+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1374+
1375+ // Should have exactly 3 relays (first relay from each contact)
1376+ assert_eq ! ( result. len( ) , 3 ) ;
1377+ assert ! ( result. contains( & url:: Url :: parse( "wss://contact1-relay1.com" ) . unwrap( ) ) ) ;
1378+ assert ! ( result. contains( & url:: Url :: parse( "wss://contact2-relay1.com" ) . unwrap( ) ) ) ;
1379+ assert ! ( result. contains( & url:: Url :: parse( "wss://contact3-relay1.com" ) . unwrap( ) ) ) ;
1380+ }
1381+
1382+ #[ test]
1383+ fn test_deduplication_across_contacts ( ) {
1384+ let user_relays = vec ! [ ] ;
1385+ 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" ] ) ,
1388+ ] ;
1389+ let max_relays = Some ( 10 ) ;
1390+
1391+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1392+
1393+ // Should only include shared.com once
1394+ assert_eq ! ( result. len( ) , 3 ) ;
1395+ assert ! ( result. contains( & url:: Url :: parse( "wss://shared.com" ) . unwrap( ) ) ) ;
1396+ assert ! ( result. contains( & url:: Url :: parse( "wss://unique1.com" ) . unwrap( ) ) ) ;
1397+ assert ! ( result. contains( & url:: Url :: parse( "wss://unique2.com" ) . unwrap( ) ) ) ;
1398+ }
1399+
1400+ #[ test]
1401+ fn test_banned_contacts_excluded ( ) {
1402+ let user_relays = vec ! [ ] ;
1403+ let contacts = vec ! [
1404+ create_test_contact( TrustLevel :: Banned , vec![ "wss://banned.com" ] ) ,
1405+ create_test_contact( TrustLevel :: Trusted , vec![ "wss://trusted.com" ] ) ,
1406+ ] ;
1407+ let max_relays = Some ( 10 ) ;
1408+
1409+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1410+
1411+ assert_eq ! ( result. len( ) , 1 ) ;
1412+ assert ! ( result. contains( & url:: Url :: parse( "wss://trusted.com" ) . unwrap( ) ) ) ;
1413+ assert ! ( !result. contains( & url:: Url :: parse( "wss://banned.com" ) . unwrap( ) ) ) ;
1414+ }
1415+
1416+ #[ test]
1417+ fn test_none_trust_level_excluded ( ) {
1418+ let user_relays = vec ! [ ] ;
1419+ let contacts = vec ! [
1420+ create_test_contact( TrustLevel :: None , vec![ "wss://unknown.com" ] ) ,
1421+ create_test_contact( TrustLevel :: Participant , vec![ "wss://participant.com" ] ) ,
1422+ ] ;
1423+ let max_relays = Some ( 10 ) ;
1424+
1425+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1426+
1427+ assert_eq ! ( result. len( ) , 1 ) ;
1428+ assert ! ( result. contains( & url:: Url :: parse( "wss://participant.com" ) . unwrap( ) ) ) ;
1429+ assert ! ( !result. contains( & url:: Url :: parse( "wss://unknown.com" ) . unwrap( ) ) ) ;
1430+ }
1431+
1432+ #[ test]
1433+ fn test_no_limit_when_max_relays_none ( ) {
1434+ let user_relays = vec ! [ url:: Url :: parse( "wss://user.com" ) . unwrap( ) ] ;
1435+ 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" ] ) ,
1438+ ] ;
1439+ let max_relays = None ;
1440+
1441+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1442+
1443+ // All relays should be included
1444+ assert_eq ! ( result. len( ) , 5 ) ;
1445+ }
1446+
1447+ #[ test]
1448+ fn test_empty_contacts ( ) {
1449+ let user_relays = vec ! [ url:: Url :: parse( "wss://user.com" ) . unwrap( ) ] ;
1450+ let contacts = vec ! [ ] ;
1451+ let max_relays = Some ( 50 ) ;
1452+
1453+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1454+
1455+ assert_eq ! ( result. len( ) , 1 ) ;
1456+ assert ! ( result. contains( & url:: Url :: parse( "wss://user.com" ) . unwrap( ) ) ) ;
1457+ }
1458+
1459+ #[ test]
1460+ fn test_contact_with_no_relays ( ) {
1461+ let user_relays = vec ! [ ] ;
1462+ let mut contact = create_test_contact ( TrustLevel :: Trusted , vec ! [ ] ) ;
1463+ contact. relays = vec ! [ ] ; // Explicitly no relays
1464+ let contacts = vec ! [ contact] ;
1465+ let max_relays = Some ( 10 ) ;
1466+
1467+ let result = calculate_relay_set_internal ( & user_relays, & contacts, max_relays) ;
1468+
1469+ // Should handle gracefully
1470+ assert_eq ! ( result. len( ) , 0 ) ;
1471+ }
1472+ }
0 commit comments