Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
/pkg/*

.idea/
.worktrees/
Cargo.lock
1 change: 1 addition & 0 deletions crates/bcr-ebill-api/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ pub const VALID_FILE_MIME_TYPES: [&str; 3] = ["image/jpeg", "image/png", "applic
// When subscribing events we subtract this from the last received event time
pub const NOSTR_EVENT_TIME_SLACK: u64 = 3600 * 24 * 7; // 1 week
pub const DEFAULT_INITIAL_SUBSCRIPTION_DELAY_SECONDS: u32 = 1;
pub const NOSTR_MAX_RELAYS: usize = 200;
15 changes: 14 additions & 1 deletion crates/bcr-ebill-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,25 @@ pub struct PaymentConfig {
}

/// Nostr specific configuration
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct NostrConfig {
/// Only known contacts can message us via DM.
pub only_known_contacts: bool,
/// All relays we want to publish our messages to and receive messages from.
pub relays: Vec<url::Url>,
/// Maximum number of contact relays to add (in addition to user relays which are always included).
/// Defaults to 50 if not specified.
pub max_relays: Option<usize>,
}

impl Default for NostrConfig {
fn default() -> Self {
Self {
only_known_contacts: false,
relays: vec![],
max_relays: Some(50),
}
}
}

/// Mint configuration
Expand Down
33 changes: 33 additions & 0 deletions crates/bcr-ebill-api/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ pub mod tests {
impl NostrContactStoreApi for NostrContactStore {
async fn by_node_id(&self, node_id: &NodeId) -> Result<Option<NostrContact>>;
async fn by_node_ids(&self, node_ids: Vec<NodeId>) -> Result<Vec<NostrContact>>;
async fn get_all(&self) -> Result<Vec<NostrContact>>;
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>>;
async fn upsert(&self, data: &NostrContact) -> Result<()>;
async fn delete(&self, node_id: &NodeId) -> Result<()>;
Expand Down Expand Up @@ -492,6 +493,7 @@ pub mod tests {
nostr_config: NostrConfig {
only_known_contacts: false,
relays: vec![url::Url::parse("ws://localhost:8080").unwrap()],
max_relays: Some(50),
},
mint_config: MintConfig {
default_mint_url: url::Url::parse("http://localhost:4242/").unwrap(),
Expand Down Expand Up @@ -681,4 +683,35 @@ pub mod tests {
pub fn test_ts() -> Timestamp {
Timestamp::new(1731593928).unwrap()
}

#[cfg(test)]
mod config_tests {
use crate::NostrConfig;

#[test]
fn test_nostr_config_default_max_relays() {
let config = NostrConfig::default();
assert_eq!(config.max_relays, Some(50));
}

#[test]
fn test_nostr_config_with_custom_max_relays() {
let config = NostrConfig {
only_known_contacts: true,
relays: vec![],
max_relays: Some(100),
};
assert_eq!(config.max_relays, Some(100));
}

#[test]
fn test_nostr_config_with_no_relay_limit() {
let config = NostrConfig {
only_known_contacts: false,
relays: vec![],
max_relays: None,
};
assert_eq!(config.max_relays, None);
}
}
}
17 changes: 17 additions & 0 deletions crates/bcr-ebill-persistence/src/db/nostr_contact_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use bcr_ebill_core::{
protocol::SecretKey,
protocol::Timestamp,
};
use log::error;
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;

Expand Down Expand Up @@ -65,6 +66,22 @@ impl NostrContactStoreApi for SurrealNostrContactStore {
Ok(values.unwrap_or_default())
}

/// Get all Nostr contacts from the store.
async fn get_all(&self) -> Result<Vec<NostrContact>> {
let result: Vec<NostrContactDb> = self.db.select_all(Self::TABLE).await?;
let values = result
.into_iter()
.filter_map(|c| match c.try_into() {
Ok(v) => Some(v),
Err(e) => {
error!("Failed to convert NostrContactDb to NostrContact: {e}");
None
}
})
.collect::<Vec<NostrContact>>();
Ok(values)
}

/// Find a Nostr contact by the npub. This is the public Nostr key of the contact.
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>> {
let result: Option<NostrContactDb> = self.db.select_one(Self::TABLE, npub.to_hex()).await?;
Expand Down
2 changes: 2 additions & 0 deletions crates/bcr-ebill-persistence/src/nostr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ pub trait NostrContactStoreApi: ServiceTraitBounds {
async fn by_node_id(&self, node_id: &NodeId) -> Result<Option<NostrContact>>;
/// Find multiple Nostr contacts by their node ids.
async fn by_node_ids(&self, node_ids: Vec<NodeId>) -> Result<Vec<NostrContact>>;
/// Get all Nostr contacts from the store.
async fn get_all(&self) -> Result<Vec<NostrContact>>;
/// Find a Nostr contact by the npub. This is the public Nostr key of the contact.
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>>;
/// Creates a new or updates an existing Nostr contact.
Expand Down
1 change: 1 addition & 0 deletions crates/bcr-ebill-transport/src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ mod test_utils {
impl NostrContactStoreApi for NostrContactStore {
async fn by_node_id(&self, node_id: &NodeId) -> Result<Option<NostrContact>>;
async fn by_node_ids(&self, node_ids: Vec<NodeId>) -> Result<Vec<NostrContact>>;
async fn get_all(&self) -> Result<Vec<NostrContact>>;
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>>;
async fn upsert(&self, data: &NostrContact) -> Result<()>;
async fn delete(&self, node_id: &NodeId) -> Result<()>;
Expand Down
18 changes: 16 additions & 2 deletions crates/bcr-ebill-transport/src/handler/nostr_contact_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,21 @@ pub struct NostrContactProcessor {
transport: Arc<dyn TransportClientApi>,
nostr_contact_store: Arc<dyn NostrContactStoreApi>,
bitcoin_network: bitcoin::Network,
nostr_client: Option<Arc<crate::nostr::NostrClient>>,
}

impl NostrContactProcessor {
pub fn new(
transport: Arc<dyn TransportClientApi>,
nostr_contact_store: Arc<dyn NostrContactStoreApi>,
bitcoin_network: bitcoin::Network,
nostr_client: Option<Arc<crate::nostr::NostrClient>>,
) -> Self {
Self {
transport,
nostr_contact_store,
bitcoin_network,
nostr_client,
}
}
}
Expand Down Expand Up @@ -73,8 +76,19 @@ impl NostrContactProcessor {
async fn upsert_contact(&self, node_id: &NodeId, contact: &NostrContact) {
if let Err(e) = self.nostr_contact_store.upsert(contact).await {
error!("Failed to save nostr contact information for node_id {node_id}: {e}");
} else if let Err(e) = self.transport.add_contact_subscription(node_id).await {
error!("Failed to add nostr contact subscription for contact node_id {node_id}: {e}");
} else {
if let Err(e) = self.transport.add_contact_subscription(node_id).await {
error!(
"Failed to add nostr contact subscription for contact node_id {node_id}: {e}"
);
}

// Trigger relay refresh to include new contact's relays
if let Some(ref client) = self.nostr_client
&& let Err(e) = client.refresh_relays().await
{
warn!("Failed to refresh relays after contact update for {node_id}: {e}");
}
}
}
}
Expand Down
14 changes: 13 additions & 1 deletion crates/bcr-ebill-transport/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use handler::{
IdentityChainEventHandler, IdentityChainEventProcessor, LoggingEventHandler,
NostrContactProcessor, NotificationHandlerApi,
};
use log::{debug, error};
use log::{debug, error, warn};
pub use nostr_transport::NostrTransportService;

mod block_transport;
Expand Down Expand Up @@ -54,6 +54,7 @@ pub async fn create_nostr_clients(
config: &Config,
identity_store: Arc<dyn IdentityStoreApi>,
company_store: Arc<dyn CompanyStoreApi>,
nostr_contact_store: Arc<dyn bcr_ebill_persistence::nostr::NostrContactStoreApi>,
) -> Result<Arc<NostrClient>> {
// primary identity is required to launch
let keys = identity_store.get_or_create_key_pair().await.map_err(|e| {
Expand Down Expand Up @@ -101,9 +102,17 @@ pub async fn create_nostr_clients(
identities,
config.nostr_config.relays.clone(),
std::time::Duration::from_secs(20),
config.nostr_config.max_relays,
Some(nostr_contact_store),
)
.await?;

// Initial relay refresh to include contact relays
if let Err(e) = client.refresh_relays().await {
warn!("Failed initial relay refresh: {}", e);
// Continue anyway - we have user relays at minimum
}

Ok(Arc::new(client))
}

Expand All @@ -121,6 +130,7 @@ pub async fn create_transport_service(
transport.clone(),
db_context.nostr_contact_store.clone(),
get_config().bitcoin_network(),
Some(client.clone()),
));
let bill_processor = Arc::new(BillChainEventProcessor::new(
db_context.bill_blockchain_store.clone(),
Expand Down Expand Up @@ -214,6 +224,7 @@ pub async fn create_nostr_consumer(
transport.clone(),
db_context.nostr_contact_store.clone(),
get_config().bitcoin_network(),
Some(client.clone()),
));

let bill_processor = Arc::new(BillChainEventProcessor::new(
Expand Down Expand Up @@ -329,6 +340,7 @@ pub async fn create_restore_account_service(
nostr_client.clone(),
db_context.nostr_contact_store.clone(),
config.bitcoin_network(),
Some(nostr_client.clone()),
));

let bill_processor = Arc::new(BillChainEventProcessor::new(
Expand Down
Loading
Loading