Skip to content

Commit 10bc38c

Browse files
authored
Multi relay support (#757)
* Add .worktrees/ to .gitignore for isolated workspaces * feat: add get_all() method to NostrContactStoreApi * feat: add max_relays config field with default of 50 * feat: implement relay calculation algorithm with tests * fix: add max_relays to wasm config and clean up warnings * feat: add relay management methods and update NostrClient creation * feat: trigger relay refresh on contact updates * docs: update design doc with implementation status * docs: add implementation plan for dynamic relay management * style: collapse nested if blocks per clippy suggestion * style: apply rustfmt formatting * Remove plans * Review fixes * Cleanup * Review fixes
1 parent ff067e3 commit 10bc38c

File tree

13 files changed

+484
-19
lines changed

13 files changed

+484
-19
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@
1515
/pkg/*
1616

1717
.idea/
18+
.worktrees/
1819
Cargo.lock

crates/bcr-ebill-api/src/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ pub const VALID_FILE_MIME_TYPES: [&str; 3] = ["image/jpeg", "image/png", "applic
88
// When subscribing events we subtract this from the last received event time
99
pub const NOSTR_EVENT_TIME_SLACK: u64 = 3600 * 24 * 7; // 1 week
1010
pub const DEFAULT_INITIAL_SUBSCRIPTION_DELAY_SECONDS: u32 = 1;
11+
pub const NOSTR_MAX_RELAYS: usize = 200;

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,25 @@ pub struct PaymentConfig {
8484
}
8585

8686
/// Nostr specific configuration
87-
#[derive(Debug, Clone, Default)]
87+
#[derive(Debug, Clone)]
8888
pub struct NostrConfig {
8989
/// Only known contacts can message us via DM.
9090
pub only_known_contacts: bool,
9191
/// All relays we want to publish our messages to and receive messages from.
9292
pub relays: Vec<url::Url>,
93+
/// Maximum number of contact relays to add (in addition to user relays which are always included).
94+
/// Defaults to 50 if not specified.
95+
pub max_relays: Option<usize>,
96+
}
97+
98+
impl Default for NostrConfig {
99+
fn default() -> Self {
100+
Self {
101+
only_known_contacts: false,
102+
relays: vec![],
103+
max_relays: Some(50),
104+
}
105+
}
93106
}
94107

95108
/// Mint configuration

crates/bcr-ebill-api/src/tests/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ pub mod tests {
142142
impl NostrContactStoreApi for NostrContactStore {
143143
async fn by_node_id(&self, node_id: &NodeId) -> Result<Option<NostrContact>>;
144144
async fn by_node_ids(&self, node_ids: Vec<NodeId>) -> Result<Vec<NostrContact>>;
145+
async fn get_all(&self) -> Result<Vec<NostrContact>>;
145146
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>>;
146147
async fn upsert(&self, data: &NostrContact) -> Result<()>;
147148
async fn delete(&self, node_id: &NodeId) -> Result<()>;
@@ -492,6 +493,7 @@ pub mod tests {
492493
nostr_config: NostrConfig {
493494
only_known_contacts: false,
494495
relays: vec![url::Url::parse("ws://localhost:8080").unwrap()],
496+
max_relays: Some(50),
495497
},
496498
mint_config: MintConfig {
497499
default_mint_url: url::Url::parse("http://localhost:4242/").unwrap(),
@@ -681,4 +683,35 @@ pub mod tests {
681683
pub fn test_ts() -> Timestamp {
682684
Timestamp::new(1731593928).unwrap()
683685
}
686+
687+
#[cfg(test)]
688+
mod config_tests {
689+
use crate::NostrConfig;
690+
691+
#[test]
692+
fn test_nostr_config_default_max_relays() {
693+
let config = NostrConfig::default();
694+
assert_eq!(config.max_relays, Some(50));
695+
}
696+
697+
#[test]
698+
fn test_nostr_config_with_custom_max_relays() {
699+
let config = NostrConfig {
700+
only_known_contacts: true,
701+
relays: vec![],
702+
max_relays: Some(100),
703+
};
704+
assert_eq!(config.max_relays, Some(100));
705+
}
706+
707+
#[test]
708+
fn test_nostr_config_with_no_relay_limit() {
709+
let config = NostrConfig {
710+
only_known_contacts: false,
711+
relays: vec![],
712+
max_relays: None,
713+
};
714+
assert_eq!(config.max_relays, None);
715+
}
716+
}
684717
}

crates/bcr-ebill-persistence/src/db/nostr_contact_store.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use bcr_ebill_core::{
1919
protocol::SecretKey,
2020
protocol::Timestamp,
2121
};
22+
use log::error;
2223
use serde::{Deserialize, Serialize};
2324
use surrealdb::sql::Thing;
2425

@@ -65,6 +66,22 @@ impl NostrContactStoreApi for SurrealNostrContactStore {
6566
Ok(values.unwrap_or_default())
6667
}
6768

69+
/// Get all Nostr contacts from the store.
70+
async fn get_all(&self) -> Result<Vec<NostrContact>> {
71+
let result: Vec<NostrContactDb> = self.db.select_all(Self::TABLE).await?;
72+
let values = result
73+
.into_iter()
74+
.filter_map(|c| match c.try_into() {
75+
Ok(v) => Some(v),
76+
Err(e) => {
77+
error!("Failed to convert NostrContactDb to NostrContact: {e}");
78+
None
79+
}
80+
})
81+
.collect::<Vec<NostrContact>>();
82+
Ok(values)
83+
}
84+
6885
/// Find a Nostr contact by the npub. This is the public Nostr key of the contact.
6986
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>> {
7087
let result: Option<NostrContactDb> = self.db.select_one(Self::TABLE, npub.to_hex()).await?;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ pub trait NostrContactStoreApi: ServiceTraitBounds {
8484
async fn by_node_id(&self, node_id: &NodeId) -> Result<Option<NostrContact>>;
8585
/// Find multiple Nostr contacts by their node ids.
8686
async fn by_node_ids(&self, node_ids: Vec<NodeId>) -> Result<Vec<NostrContact>>;
87+
/// Get all Nostr contacts from the store.
88+
async fn get_all(&self) -> Result<Vec<NostrContact>>;
8789
/// Find a Nostr contact by the npub. This is the public Nostr key of the contact.
8890
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>>;
8991
/// Creates a new or updates an existing Nostr contact.

crates/bcr-ebill-transport/src/handler/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ mod test_utils {
507507
impl NostrContactStoreApi for NostrContactStore {
508508
async fn by_node_id(&self, node_id: &NodeId) -> Result<Option<NostrContact>>;
509509
async fn by_node_ids(&self, node_ids: Vec<NodeId>) -> Result<Vec<NostrContact>>;
510+
async fn get_all(&self) -> Result<Vec<NostrContact>>;
510511
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>>;
511512
async fn upsert(&self, data: &NostrContact) -> Result<()>;
512513
async fn delete(&self, node_id: &NodeId) -> Result<()>;

crates/bcr-ebill-transport/src/handler/nostr_contact_processor.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@ pub struct NostrContactProcessor {
1717
transport: Arc<dyn TransportClientApi>,
1818
nostr_contact_store: Arc<dyn NostrContactStoreApi>,
1919
bitcoin_network: bitcoin::Network,
20+
nostr_client: Option<Arc<crate::nostr::NostrClient>>,
2021
}
2122

2223
impl NostrContactProcessor {
2324
pub fn new(
2425
transport: Arc<dyn TransportClientApi>,
2526
nostr_contact_store: Arc<dyn NostrContactStoreApi>,
2627
bitcoin_network: bitcoin::Network,
28+
nostr_client: Option<Arc<crate::nostr::NostrClient>>,
2729
) -> Self {
2830
Self {
2931
transport,
3032
nostr_contact_store,
3133
bitcoin_network,
34+
nostr_client,
3235
}
3336
}
3437
}
@@ -73,8 +76,19 @@ impl NostrContactProcessor {
7376
async fn upsert_contact(&self, node_id: &NodeId, contact: &NostrContact) {
7477
if let Err(e) = self.nostr_contact_store.upsert(contact).await {
7578
error!("Failed to save nostr contact information for node_id {node_id}: {e}");
76-
} else if let Err(e) = self.transport.add_contact_subscription(node_id).await {
77-
error!("Failed to add nostr contact subscription for contact node_id {node_id}: {e}");
79+
} else {
80+
if let Err(e) = self.transport.add_contact_subscription(node_id).await {
81+
error!(
82+
"Failed to add nostr contact subscription for contact node_id {node_id}: {e}"
83+
);
84+
}
85+
86+
// Trigger relay refresh to include new contact's relays
87+
if let Some(ref client) = self.nostr_client
88+
&& let Err(e) = client.refresh_relays().await
89+
{
90+
warn!("Failed to refresh relays after contact update for {node_id}: {e}");
91+
}
7892
}
7993
}
8094
}

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

Lines changed: 13 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

@@ -121,6 +130,7 @@ pub async fn create_transport_service(
121130
transport.clone(),
122131
db_context.nostr_contact_store.clone(),
123132
get_config().bitcoin_network(),
133+
Some(client.clone()),
124134
));
125135
let bill_processor = Arc::new(BillChainEventProcessor::new(
126136
db_context.bill_blockchain_store.clone(),
@@ -214,6 +224,7 @@ pub async fn create_nostr_consumer(
214224
transport.clone(),
215225
db_context.nostr_contact_store.clone(),
216226
get_config().bitcoin_network(),
227+
Some(client.clone()),
217228
));
218229

219230
let bill_processor = Arc::new(BillChainEventProcessor::new(
@@ -329,6 +340,7 @@ pub async fn create_restore_account_service(
329340
nostr_client.clone(),
330341
db_context.nostr_contact_store.clone(),
331342
config.bitcoin_network(),
343+
Some(nostr_client.clone()),
332344
));
333345

334346
let bill_processor = Arc::new(BillChainEventProcessor::new(

0 commit comments

Comments
 (0)