From 4f7c44f896a353bcbd51c163b7bd0560444c5b64 Mon Sep 17 00:00:00 2001 From: tompro Date: Fri, 4 Jul 2025 10:36:41 +0200 Subject: [PATCH 01/11] Events and generators --- .../notification_service/default_service.rs | 2 +- .../src/event/bill_events.rs | 2 +- ...lockchain_event.rs => blockchain_event.rs} | 22 ++++++- .../src/event/company_events.rs | 64 ++++++++++++++++++ .../src/event/identity_events.rs | 66 +++++++++++++++++++ crates/bcr-ebill-transport/src/event/mod.rs | 18 ++++- .../src/handler/bill_chain_event_handler.rs | 2 +- .../src/handler/bill_invite_handler.rs | 2 +- 8 files changed, 170 insertions(+), 8 deletions(-) rename crates/bcr-ebill-transport/src/event/{bill_blockchain_event.rs => blockchain_event.rs} (70%) create mode 100644 crates/bcr-ebill-transport/src/event/company_events.rs create mode 100644 crates/bcr-ebill-transport/src/event/identity_events.rs diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index e5a227d4..c964154e 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -642,7 +642,7 @@ mod tests { HandshakeStatus, NostrContact, NostrPublicKey, TrustLevel, }; use bcr_ebill_core::util::{BcrKeys, date::now}; - use bcr_ebill_transport::event::bill_blockchain_event::ChainInvite; + use bcr_ebill_transport::event::blockchain_event::ChainInvite; use bcr_ebill_transport::{EventEnvelope, EventType, PushApi}; use mockall::{mock, predicate::eq}; use std::sync::Arc; diff --git a/crates/bcr-ebill-transport/src/event/bill_events.rs b/crates/bcr-ebill-transport/src/event/bill_events.rs index bccaa5a7..6fea30d7 100644 --- a/crates/bcr-ebill-transport/src/event/bill_events.rs +++ b/crates/bcr-ebill-transport/src/event/bill_events.rs @@ -15,7 +15,7 @@ use crate::{Error, Result}; use super::{ Event, EventType, - bill_blockchain_event::{BillBlockEvent, ChainInvite}, + blockchain_event::{BillBlockEvent, ChainInvite}, }; pub struct BillChainEvent { diff --git a/crates/bcr-ebill-transport/src/event/bill_blockchain_event.rs b/crates/bcr-ebill-transport/src/event/blockchain_event.rs similarity index 70% rename from crates/bcr-ebill-transport/src/event/bill_blockchain_event.rs rename to crates/bcr-ebill-transport/src/event/blockchain_event.rs index a065f514..777eb72c 100644 --- a/crates/bcr-ebill-transport/src/event/bill_blockchain_event.rs +++ b/crates/bcr-ebill-transport/src/event/blockchain_event.rs @@ -1,7 +1,7 @@ use bcr_ebill_core::{ - PublicKey, SecretKey, + NodeId, PublicKey, SecretKey, bill::{BillId, BillKeys}, - blockchain::{BlockchainType, bill::BillBlock}, + blockchain::{BlockchainType, bill::BillBlock, company::CompanyBlock, identity::IdentityBlock}, company::CompanyKeys, util::BcrKeys, }; @@ -56,10 +56,26 @@ pub struct ChainKeys { pub public_key: PublicKey, } -/// The encrypted BCR payload contained in a public block Nostr event. +/// The encrypted BCR bill payload contained in a public block Nostr event. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BillBlockEvent { pub bill_id: BillId, pub block_height: usize, pub block: BillBlock, } + +/// The encrypted BCR identity payload contained in a public block Nostr event. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct IdentityBlockEvent { + pub node_id: NodeId, + pub block_height: usize, + pub block: IdentityBlock, +} +/// +/// The encrypted BCR company payload contained in a public block Nostr event. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CompanyBlockEvent { + pub node_id: NodeId, + pub block_height: usize, + pub block: CompanyBlock, +} diff --git a/crates/bcr-ebill-transport/src/event/company_events.rs b/crates/bcr-ebill-transport/src/event/company_events.rs new file mode 100644 index 00000000..74d46c31 --- /dev/null +++ b/crates/bcr-ebill-transport/src/event/company_events.rs @@ -0,0 +1,64 @@ +use bcr_ebill_core::{ + NodeId, + blockchain::{ + Blockchain, + company::{CompanyBlock, CompanyBlockchain}, + }, + company::{Company, CompanyKeys}, +}; + +use crate::Result; + +use super::{Event, blockchain_event::CompanyBlockEvent}; + +pub struct CompanyChainEvent { + pub company: Company, + chain: CompanyBlockchain, + pub keys: CompanyKeys, + new_blocks: bool, + sender_node_id: NodeId, +} + +impl CompanyChainEvent { + /// Create a new CompanyChainEvent instance. New blocks indicate whether the given chain contains + /// new blocks for the company. Currently we only send a message if a new block has been added. + pub fn new( + company: &Company, + chain: &CompanyBlockchain, + keys: &CompanyKeys, + new_blocks: bool, + ) -> Result { + Ok(Self { + company: company.clone(), + chain: chain.clone(), + keys: keys.clone(), + new_blocks, + sender_node_id: company.id.to_owned(), + }) + } + + pub fn sender(&self) -> NodeId { + self.sender_node_id.clone() + } + + // Returns the latest block in the chain. + fn latest_block(&self) -> CompanyBlock { + self.chain.get_latest_block().clone() + } + + pub fn block_height(&self) -> usize { + self.chain.block_height() + } + + /// generates the latest block event for the bill. + pub fn generate_blockchain_message(&self) -> Option> { + if !self.new_blocks { + return None; + } + Some(Event::new_identity_chain(CompanyBlockEvent { + node_id: self.company.id.clone(), + block_height: self.block_height(), + block: self.latest_block(), + })) + } +} diff --git a/crates/bcr-ebill-transport/src/event/identity_events.rs b/crates/bcr-ebill-transport/src/event/identity_events.rs new file mode 100644 index 00000000..ed1d798d --- /dev/null +++ b/crates/bcr-ebill-transport/src/event/identity_events.rs @@ -0,0 +1,66 @@ +use bcr_ebill_core::{ + NodeId, + blockchain::{ + Blockchain, + identity::{IdentityBlock, IdentityBlockchain}, + }, + identity::Identity, + util::BcrKeys, +}; + +use crate::Result; + +use super::{Event, blockchain_event::IdentityBlockEvent}; + +pub struct IdentityChainEvent { + pub identity: Identity, + chain: IdentityBlockchain, + pub keys: BcrKeys, + new_blocks: bool, + sender_node_id: NodeId, +} + +impl IdentityChainEvent { + /// Create a new IdentityChainEvent instance. New blocks indicate whether the given chain contains + /// new blocks for the identity. Currently we only send a message if a new block has been + /// added. + pub fn new( + identity: &Identity, + chain: &IdentityBlockchain, + keys: &BcrKeys, + new_blocks: bool, + ) -> Result { + Ok(Self { + identity: identity.clone(), + chain: chain.clone(), + keys: keys.clone(), + new_blocks, + sender_node_id: identity.node_id.to_owned(), + }) + } + + pub fn sender(&self) -> NodeId { + self.sender_node_id.clone() + } + + // Returns the latest block in the chain. + fn latest_block(&self) -> IdentityBlock { + self.chain.get_latest_block().clone() + } + + pub fn block_height(&self) -> usize { + self.chain.block_height() + } + + /// generates the latest block event for the bill. + pub fn generate_blockchain_message(&self) -> Option> { + if !self.new_blocks { + return None; + } + Some(Event::new_identity_chain(IdentityBlockEvent { + node_id: self.identity.node_id.clone(), + block_height: self.block_height(), + block: self.latest_block(), + })) + } +} diff --git a/crates/bcr-ebill-transport/src/event/mod.rs b/crates/bcr-ebill-transport/src/event/mod.rs index 7a140e4d..5cc75051 100644 --- a/crates/bcr-ebill-transport/src/event/mod.rs +++ b/crates/bcr-ebill-transport/src/event/mod.rs @@ -1,5 +1,7 @@ -pub mod bill_blockchain_event; pub mod bill_events; +pub mod blockchain_event; +pub mod company_events; +pub mod identity_events; use crate::{Error, Result}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; @@ -14,6 +16,10 @@ pub enum EventType { BillChain, /// Private Bill invites with keys BillChainInvite, + /// Public identity events + IdentityChain, + /// Public company chain events + CompanyChain, } impl EventType { @@ -22,6 +28,8 @@ impl EventType { EventType::Bill, EventType::BillChain, EventType::BillChainInvite, + EventType::IdentityChain, + EventType::CompanyChain, ] } } @@ -54,6 +62,14 @@ impl Event { Self::new(EventType::BillChain, data) } + pub fn new_identity_chain(data: T) -> Self { + Self::new(EventType::IdentityChain, data) + } + + pub fn new_company_chain(data: T) -> Self { + Self::new(EventType::CompanyChain, data) + } + pub fn new_invite(data: T) -> Self { Self::new(EventType::BillChainInvite, data) } diff --git a/crates/bcr-ebill-transport/src/handler/bill_chain_event_handler.rs b/crates/bcr-ebill-transport/src/handler/bill_chain_event_handler.rs index 852f5903..3fa7b6b9 100644 --- a/crates/bcr-ebill-transport/src/handler/bill_chain_event_handler.rs +++ b/crates/bcr-ebill-transport/src/handler/bill_chain_event_handler.rs @@ -1,7 +1,7 @@ use super::BillChainEventProcessorApi; use super::NotificationHandlerApi; use crate::EventType; -use crate::event::bill_blockchain_event::BillBlockEvent; +use crate::event::blockchain_event::BillBlockEvent; use crate::transport::root_and_reply_id; use crate::{Event, EventEnvelope, Result}; use async_trait::async_trait; diff --git a/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs b/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs index a35c768a..1bfb2ae3 100644 --- a/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs +++ b/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs @@ -16,7 +16,7 @@ use nostr::{ use crate::{ Error, Event, EventEnvelope, EventType, NotificationJsonTransportApi, Result, - event::bill_blockchain_event::{BillBlockEvent, ChainInvite, ChainKeys}, + event::blockchain_event::{BillBlockEvent, ChainInvite, ChainKeys}, transport::{decrypt_public_chain_event, unwrap_public_chain_event}, }; From 79cbd0ed3500a87e467d12cfaaa190348fd28ab7 Mon Sep 17 00:00:00 2001 From: tompro Date: Fri, 4 Jul 2025 10:59:37 +0200 Subject: [PATCH 02/11] Add send events skeleton --- .../notification_service/default_service.rs | 51 ++++++++++++++----- crates/bcr-ebill-api/src/tests/mod.rs | 6 ++- .../src/event/company_events.rs | 1 + .../src/event/identity_events.rs | 1 + .../src/notification_service.rs | 13 ++++- 5 files changed, 57 insertions(+), 15 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index c964154e..731a7b07 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -9,9 +9,11 @@ use bcr_ebill_core::contact::{BillAnonParticipant, BillParticipant, ContactType} use bcr_ebill_persistence::nostr::{ NostrChainEvent, NostrChainEventStoreApi, NostrQueuedMessage, NostrQueuedMessageStoreApi, }; +use bcr_ebill_transport::event::company_events::CompanyChainEvent; +use bcr_ebill_transport::event::identity_events::IdentityChainEvent; use bcr_ebill_transport::transport::NostrContactData; use bcr_ebill_transport::{BillChainEvent, BillChainEventPayload, Error, Event, EventEnvelope}; -use log::{error, warn}; +use log::{error, info, warn}; use super::NotificationJsonTransportApi; use super::{NotificationServiceApi, Result}; @@ -107,7 +109,7 @@ impl DefaultNotificationService { } } - async fn send_all_events( + async fn send_all_bill_events( &self, sender: &NodeId, events: HashMap>, @@ -242,6 +244,18 @@ impl DefaultNotificationService { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl NotificationServiceApi for DefaultNotificationService { + /// Sent when an identity chain is created or updated + async fn send_identity_chain_events(&self, events: &IdentityChainEvent) -> Result<()> { + info!("sending identity chain events with {events:#?}"); + Ok(()) + } + + /// Sent when a company chain is created or updated + async fn send_company_chain_events(&self, events: &CompanyChainEvent) -> Result<()> { + info!("sending company chain events with {events:#?}"); + Ok(()) + } + async fn send_bill_is_signed_event(&self, event: &BillChainEvent) -> Result<()> { let event_type = BillEventType::BillSigned; @@ -261,7 +275,8 @@ impl NotificationServiceApi for DefaultNotificationService { ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; Ok(()) } @@ -275,7 +290,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; Ok(()) } @@ -292,7 +308,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; Ok(()) } @@ -306,7 +323,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; Ok(()) } @@ -320,7 +338,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; Ok(()) } @@ -334,7 +353,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(bill).await?; - self.send_all_events(&bill.sender(), all_events).await?; + self.send_all_bill_events(&bill.sender(), all_events) + .await?; Ok(()) } @@ -352,7 +372,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; Ok(()) } @@ -370,7 +391,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; Ok(()) } @@ -388,7 +410,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; Ok(()) } @@ -422,7 +445,8 @@ impl NotificationServiceApi for DefaultNotificationService { Some(rejected_action), ); - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; } Ok(()) } @@ -473,7 +497,8 @@ impl NotificationServiceApi for DefaultNotificationService { None, ); self.send_bill_chain_events(event).await?; - self.send_all_events(&event.sender(), all_events).await?; + self.send_all_bill_events(&event.sender(), all_events) + .await?; } Ok(()) } diff --git a/crates/bcr-ebill-api/src/tests/mod.rs b/crates/bcr-ebill-api/src/tests/mod.rs index 7c86cd5f..42c9f1d2 100644 --- a/crates/bcr-ebill-api/src/tests/mod.rs +++ b/crates/bcr-ebill-api/src/tests/mod.rs @@ -35,7 +35,9 @@ pub mod tests { notification::NotificationFilter, }; use bcr_ebill_transport::{ - BillChainEvent, NotificationServiceApi, chain_keys::ChainKeyServiceApi, + BillChainEvent, NotificationServiceApi, + chain_keys::ChainKeyServiceApi, + event::{company_events::CompanyChainEvent, identity_events::IdentityChainEvent}, transport::NostrContactData, }; use std::path::Path; @@ -371,6 +373,8 @@ pub mod tests { #[async_trait] impl NotificationServiceApi for NotificationService { + async fn send_identity_chain_events(&self, events: &IdentityChainEvent) -> bcr_ebill_transport::Result<()>; + async fn send_company_chain_events(&self, events: &CompanyChainEvent) -> bcr_ebill_transport::Result<()>; async fn send_bill_is_signed_event(&self, event: &BillChainEvent) -> bcr_ebill_transport::Result<()>; async fn send_bill_is_accepted_event(&self, event: &BillChainEvent) -> bcr_ebill_transport::Result<()>; async fn send_request_to_accept_event(&self, event: &BillChainEvent) -> bcr_ebill_transport::Result<()>; diff --git a/crates/bcr-ebill-transport/src/event/company_events.rs b/crates/bcr-ebill-transport/src/event/company_events.rs index 74d46c31..0d351008 100644 --- a/crates/bcr-ebill-transport/src/event/company_events.rs +++ b/crates/bcr-ebill-transport/src/event/company_events.rs @@ -11,6 +11,7 @@ use crate::Result; use super::{Event, blockchain_event::CompanyBlockEvent}; +#[derive(Clone, Debug)] pub struct CompanyChainEvent { pub company: Company, chain: CompanyBlockchain, diff --git a/crates/bcr-ebill-transport/src/event/identity_events.rs b/crates/bcr-ebill-transport/src/event/identity_events.rs index ed1d798d..d85d1290 100644 --- a/crates/bcr-ebill-transport/src/event/identity_events.rs +++ b/crates/bcr-ebill-transport/src/event/identity_events.rs @@ -12,6 +12,7 @@ use crate::Result; use super::{Event, blockchain_event::IdentityBlockEvent}; +#[derive(Clone, Debug)] pub struct IdentityChainEvent { pub identity: Identity, chain: IdentityBlockchain, diff --git a/crates/bcr-ebill-transport/src/notification_service.rs b/crates/bcr-ebill-transport/src/notification_service.rs index aa4f73f6..29a924f6 100644 --- a/crates/bcr-ebill-transport/src/notification_service.rs +++ b/crates/bcr-ebill-transport/src/notification_service.rs @@ -1,4 +1,11 @@ -use crate::{Result, event::bill_events::BillChainEvent, transport::NostrContactData}; +use crate::{ + Result, + event::{ + bill_events::BillChainEvent, company_events::CompanyChainEvent, + identity_events::IdentityChainEvent, + }, + transport::NostrContactData, +}; use async_trait::async_trait; use bcr_ebill_core::ServiceTraitBounds; use bcr_ebill_core::{ @@ -21,6 +28,10 @@ impl ServiceTraitBounds for MockNotificationServiceApi {} #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NotificationServiceApi: ServiceTraitBounds { + /// Sent when an identity chain is created or updated + async fn send_identity_chain_events(&self, events: &IdentityChainEvent) -> Result<()>; + /// Sent when a company chain is created or updated + async fn send_company_chain_events(&self, events: &CompanyChainEvent) -> Result<()>; /// Sent when: A bill is signed by: Drawer /// Receiver: Payer, Action: AcceptBill /// Receiver: Payee, Action: CheckBill From 9ade73b66c8fc8904936b2ffdd9d47d50a4fc21c Mon Sep 17 00:00:00 2001 From: tompro Date: Fri, 4 Jul 2025 11:14:46 +0200 Subject: [PATCH 03/11] Inject required dependencies --- .../src/service/company_service.rs | 68 ++++++++++++++++++- .../src/service/identity_service.rs | 8 ++- crates/bcr-ebill-wasm/src/context.rs | 2 + 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/company_service.rs b/crates/bcr-ebill-api/src/service/company_service.rs index 30d61e04..872db2b5 100644 --- a/crates/bcr-ebill-api/src/service/company_service.rs +++ b/crates/bcr-ebill-api/src/service/company_service.rs @@ -28,6 +28,7 @@ use crate::{ use async_trait::async_trait; use bcr_ebill_core::identity::IdentityType; use bcr_ebill_core::{NodeId, PublicKey, SecretKey, ServiceTraitBounds, ValidationError}; +use bcr_ebill_transport::NotificationServiceApi; use log::{debug, error, info}; use std::sync::Arc; @@ -115,6 +116,7 @@ pub struct CompanyService { contact_store: Arc, identity_blockchain_store: Arc, company_blockchain_store: Arc, + notification_service: Arc, } impl CompanyService { @@ -126,6 +128,7 @@ impl CompanyService { contact_store: Arc, identity_blockchain_store: Arc, company_blockchain_store: Arc, + notification_service: Arc, ) -> Self { Self { store, @@ -135,6 +138,7 @@ impl CompanyService { contact_store, identity_blockchain_store, company_blockchain_store, + notification_service, } } @@ -757,8 +761,8 @@ pub mod tests { tests::tests::{ MockCompanyChainStoreApiMock, MockCompanyStoreApiMock, MockContactStoreApiMock, MockFileUploadStoreApiMock, MockIdentityChainStoreApiMock, MockIdentityStoreApiMock, - empty_address, empty_identity, empty_optional_address, node_id_test, - node_id_test_other, node_id_test_other2, private_key_test, + MockNotificationService, empty_address, empty_identity, empty_optional_address, + node_id_test, node_id_test_other, node_id_test_other2, private_key_test, }, }; use std::{collections::HashMap, str::FromStr}; @@ -772,6 +776,7 @@ pub mod tests { mock_contacts_storage: MockContactStoreApiMock, mock_identity_chain_storage: MockIdentityChainStoreApiMock, mock_company_chain_storage: MockCompanyChainStoreApiMock, + notification_service: MockNotificationService, ) -> CompanyService { CompanyService::new( Arc::new(mock_storage), @@ -781,6 +786,7 @@ pub mod tests { Arc::new(mock_contacts_storage), Arc::new(mock_identity_chain_storage), Arc::new(mock_company_chain_storage), + Arc::new(notification_service), ) } @@ -792,6 +798,7 @@ pub mod tests { MockContactStoreApiMock, MockIdentityChainStoreApiMock, MockCompanyChainStoreApiMock, + MockNotificationService, ) { ( MockCompanyStoreApiMock::new(), @@ -801,6 +808,7 @@ pub mod tests { MockContactStoreApiMock::new(), MockIdentityChainStoreApiMock::new(), MockCompanyChainStoreApiMock::new(), + MockNotificationService::new(), ) } @@ -853,6 +861,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_get_all().returning(|| { let mut map = HashMap::new(); @@ -868,6 +877,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service.get_list_of_companies().await; @@ -886,6 +896,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_get_all().returning(|| { Err(bcr_ebill_persistence::Error::Io(std::io::Error::other( @@ -900,6 +911,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service.get_list_of_companies().await; assert!(res.is_err()); @@ -915,6 +927,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); storage @@ -931,6 +944,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service.get_company_by_id(&node_id_test()).await; @@ -948,6 +962,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| false); let service = get_service( @@ -958,6 +973,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service.get_company_by_id(&node_id_test()).await; assert!(res.is_err()); @@ -973,6 +989,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); storage.expect_get().returning(|_| { @@ -988,6 +1005,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service.get_company_by_id(&node_id_test()).await; assert!(res.is_err()); @@ -1003,6 +1021,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, + notification, ) = get_storages(); company_chain_store .expect_add_block() @@ -1052,6 +1071,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service @@ -1095,6 +1115,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_save_key_pair().returning(|_, _| Ok(())); storage.expect_insert().returning(|_| { @@ -1117,6 +1138,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .create_company( @@ -1147,6 +1169,7 @@ pub mod tests { contact_store, identity_chain_store, mut company_chain_store, + notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1193,6 +1216,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .edit_company( @@ -1222,6 +1246,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| false); let service = get_service( @@ -1232,6 +1257,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .edit_company( @@ -1261,6 +1287,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| false); storage.expect_get().returning(|_| { @@ -1283,6 +1310,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .edit_company( @@ -1312,6 +1340,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); let keys = BcrKeys::new(); let node_id = NodeId::new(keys.pub_key(), bitcoin::Network::Testnet); @@ -1346,6 +1375,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .edit_company( @@ -1375,6 +1405,7 @@ pub mod tests { mut contact_store, mut identity_chain_store, mut company_chain_store, + notification, ) = get_storages(); let signatory_node_id = NodeId::new(BcrKeys::new().pub_key(), bitcoin::Network::Testnet); storage.expect_exists().returning(|_| true); @@ -1430,6 +1461,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .add_signatory(&node_id_test(), signatory_node_id, 1731593928) @@ -1447,6 +1479,7 @@ pub mod tests { mut contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); let signatory_node_id = NodeId::new(BcrKeys::new().pub_key(), bitcoin::Network::Testnet); storage.expect_exists().returning(|_| true); @@ -1474,6 +1507,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .add_signatory(&node_id_test(), signatory_node_id, 1731593928) @@ -1491,6 +1525,7 @@ pub mod tests { mut contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); contact_store @@ -1511,6 +1546,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .add_signatory(&node_id_test(), node_id_test_other(), 1731593928) @@ -1528,6 +1564,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| false); identity_store.expect_get_full().returning(|| { @@ -1545,6 +1582,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .add_signatory(&node_id_test(), node_id_test_other(), 1731593928) @@ -1562,6 +1600,7 @@ pub mod tests { mut contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); identity_store.expect_get_full().returning(|| { @@ -1593,6 +1632,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .add_signatory(&node_id_test(), node_id_test(), 1731593928) @@ -1610,6 +1650,7 @@ pub mod tests { mut contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); identity_store.expect_get_full().returning(|| { @@ -1644,6 +1685,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .add_signatory(&node_id_test(), node_id_test(), 1731593928) @@ -1661,6 +1703,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, + notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1708,6 +1751,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .remove_signatory(&node_id_test(), node_id_test_other(), 1731593928) @@ -1725,6 +1769,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| false); identity_store.expect_get_full().returning(|| { @@ -1742,6 +1787,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .remove_signatory(&node_id_test(), node_id_test_other(), 1731593928) @@ -1760,6 +1806,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, + notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1819,6 +1866,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .remove_signatory( @@ -1840,6 +1888,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); storage.expect_get().returning(|_| { @@ -1866,6 +1915,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .remove_signatory(&node_id_test(), node_id_test_other2(), 1731593928) @@ -1883,6 +1933,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); storage.expect_get().returning(|_| { @@ -1907,6 +1958,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .remove_signatory(&node_id_test(), node_id_test(), 1731593928) @@ -1924,6 +1976,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); storage.expect_get().returning(|_| { @@ -1955,6 +2008,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .remove_signatory(&node_id_test(), node_id_test(), 1731593928) @@ -1979,6 +2033,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); file_upload_client @@ -2003,6 +2058,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let file = service @@ -2045,6 +2101,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); file_upload_client.expect_upload().returning(|_, _| { Err(crate::external::Error::ExternalFileStorageApi( @@ -2059,6 +2116,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); assert!( @@ -2085,6 +2143,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); file_upload_client.expect_upload().returning(|_, _| { Err(crate::external::Error::ExternalFileStorageApi( @@ -2099,6 +2158,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); assert!( @@ -2124,6 +2184,7 @@ pub mod tests { mut contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); storage.expect_get().returning(|_| { @@ -2148,6 +2209,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service.list_signatories(&node_id_test()).await; @@ -2165,6 +2227,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); let mainnet_node_id = NodeId::new(BcrKeys::new().pub_key(), bitcoin::Network::Bitcoin); let service = get_service( @@ -2175,6 +2238,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); assert!(service.list_signatories(&mainnet_node_id).await.is_err()); assert!( diff --git a/crates/bcr-ebill-api/src/service/identity_service.rs b/crates/bcr-ebill-api/src/service/identity_service.rs index c95931a5..e8c896d3 100644 --- a/crates/bcr-ebill-api/src/service/identity_service.rs +++ b/crates/bcr-ebill-api/src/service/identity_service.rs @@ -16,6 +16,7 @@ use async_trait::async_trait; use bcr_ebill_core::identity::validation::{validate_create_identity, validate_update_identity}; use bcr_ebill_core::identity::{ActiveIdentityState, IdentityType}; use bcr_ebill_core::{NodeId, ServiceTraitBounds, ValidationError}; +use bcr_ebill_transport::NotificationServiceApi; use log::{debug, error, info}; use std::sync::Arc; @@ -102,6 +103,7 @@ pub struct IdentityService { file_upload_store: Arc, file_upload_client: Arc, blockchain_store: Arc, + notification_service: Arc, } impl IdentityService { @@ -110,12 +112,14 @@ impl IdentityService { file_upload_store: Arc, file_upload_client: Arc, blockchain_store: Arc, + notification_service: Arc, ) -> Self { Self { store, file_upload_store, file_upload_client, blockchain_store, + notification_service, } } @@ -627,7 +631,7 @@ mod tests { external::file_storage::MockFileStorageClientApi, tests::tests::{ MockFileUploadStoreApiMock, MockIdentityChainStoreApiMock, MockIdentityStoreApiMock, - empty_identity, empty_optional_address, init_test_cfg, + MockNotificationService, empty_identity, empty_optional_address, init_test_cfg, }, }; use mockall::predicate::eq; @@ -638,6 +642,7 @@ mod tests { Arc::new(MockFileUploadStoreApiMock::new()), Arc::new(MockFileStorageClientApi::new()), Arc::new(MockIdentityChainStoreApiMock::new()), + Arc::new(MockNotificationService::new()), ) } @@ -650,6 +655,7 @@ mod tests { Arc::new(MockFileUploadStoreApiMock::new()), Arc::new(MockFileStorageClientApi::new()), Arc::new(mock_chain_storage), + Arc::new(MockNotificationService::new()), ) } diff --git a/crates/bcr-ebill-wasm/src/context.rs b/crates/bcr-ebill-wasm/src/context.rs index 5972c426..b5a1586f 100644 --- a/crates/bcr-ebill-wasm/src/context.rs +++ b/crates/bcr-ebill-wasm/src/context.rs @@ -82,6 +82,7 @@ impl Context { db.file_upload_store.clone(), file_upload_client.clone(), db.identity_chain_store.clone(), + notification_service.clone(), ); let company_service = CompanyService::new( @@ -92,6 +93,7 @@ impl Context { db.contact_store, db.identity_chain_store, db.company_chain_store, + notification_service.clone(), ); let file_upload_service = FileUploadService::new(db.file_upload_store); From 8189059eefac48e0dde0ea2fd7ee586416bef075 Mon Sep 17 00:00:00 2001 From: tompro Date: Fri, 4 Jul 2025 13:00:41 +0200 Subject: [PATCH 04/11] Added all populate block hooks, fixed tests --- .../src/service/company_service.rs | 137 +++++++++++++++++- .../src/service/identity_service.rs | 70 +++++++-- .../notification_service/default_service.rs | 4 +- crates/bcr-ebill-api/src/tests/mod.rs | 4 +- .../src/event/company_events.rs | 8 +- .../src/event/identity_events.rs | 45 ++---- .../src/notification_service.rs | 4 +- 7 files changed, 205 insertions(+), 67 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/company_service.rs b/crates/bcr-ebill-api/src/service/company_service.rs index 872db2b5..b5851e10 100644 --- a/crates/bcr-ebill-api/src/service/company_service.rs +++ b/crates/bcr-ebill-api/src/service/company_service.rs @@ -29,6 +29,8 @@ use async_trait::async_trait; use bcr_ebill_core::identity::IdentityType; use bcr_ebill_core::{NodeId, PublicKey, SecretKey, ServiceTraitBounds, ValidationError}; use bcr_ebill_transport::NotificationServiceApi; +use bcr_ebill_transport::event::company_events::CompanyChainEvent; +use bcr_ebill_transport::event::identity_events::IdentityChainEvent; use log::{debug, error, info}; use std::sync::Arc; @@ -182,6 +184,18 @@ impl CompanyService { nostr_hash: nostr_hash.to_string(), }) } + + async fn populate_block( + &self, + company: &Company, + chain: &CompanyBlockchain, + keys: &CompanyKeys, + ) -> Result<()> { + self.notification_service + .send_company_chain_events(CompanyChainEvent::new(company, chain, keys, true)) + .await?; + Ok(()) + } } impl ServiceTraitBounds for CompanyService {} @@ -333,7 +347,20 @@ impl CompanyServiceApi for CompanyService { self.company_blockchain_store .add_block(&id, create_company_block) .await?; + + let company_chain = self.company_blockchain_store.get_chain(&id).await?; + self.populate_block(&company, &company_chain, &company_keys) + .await?; + self.identity_blockchain_store.add_block(&new_block).await?; + self.notification_service + .send_identity_chain_events(IdentityChainEvent::new( + &full_identity.identity, + &new_block, + &full_identity.key_pair, + )) + .await?; + debug!("company with id {id} created"); // TODO NOSTR: create company topic and subscribe to it @@ -512,6 +539,10 @@ impl CompanyServiceApi for CompanyService { self.company_blockchain_store .add_block(id, &new_block) .await?; + let company_chain = self.company_blockchain_store.get_chain(id).await?; + self.populate_block(&company, &company_chain, &company_keys) + .await?; + debug!("company with id {id} updated"); if let Some(upload_id) = logo_file_upload_id { @@ -598,9 +629,21 @@ impl CompanyServiceApi for CompanyService { self.company_blockchain_store .add_block(id, &new_block) .await?; + let company_chain = self.company_blockchain_store.get_chain(id).await?; + self.populate_block(&company, &company_chain, &company_keys) + .await?; + self.identity_blockchain_store .add_block(&new_identity_block) .await?; + self.notification_service + .send_identity_chain_events(IdentityChainEvent::new( + &full_identity.identity, + &new_identity_block, + &full_identity.key_pair, + )) + .await?; + debug!( "added signatory {} to company with id: {id}", &signatory_node_id @@ -684,9 +727,20 @@ impl CompanyServiceApi for CompanyService { self.company_blockchain_store .add_block(id, &new_block) .await?; + let company_chain = self.company_blockchain_store.get_chain(id).await?; + self.populate_block(&company, &company_chain, &company_keys) + .await?; + self.identity_blockchain_store .add_block(&new_identity_block) .await?; + self.notification_service + .send_identity_chain_events(IdentityChainEvent::new( + &full_identity.identity, + &new_identity_block, + &full_identity.key_pair, + )) + .await?; // TODO NOSTR: propagate block to company topic @@ -838,8 +892,11 @@ pub mod tests { } pub fn get_valid_company_block() -> CompanyBlock { - let (_id, (company, company_keys)) = get_baseline_company_data(); + get_valid_company_chain().get_latest_block().to_owned() + } + fn get_valid_company_chain() -> CompanyBlockchain { + let (_id, (company, company_keys)) = get_baseline_company_data(); CompanyBlockchain::new( &CompanyCreateBlockData::from(company), &BcrKeys::new(), @@ -847,8 +904,6 @@ pub mod tests { 1731593928, ) .unwrap() - .get_latest_block() - .to_owned() } #[tokio::test] @@ -1021,7 +1076,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, - notification, + mut notification, ) = get_storages(); company_chain_store .expect_add_block() @@ -1062,6 +1117,20 @@ pub mod tests { identity_chain_store .expect_add_block() .returning(|_| Ok(())); + // sends identity block + notification + .expect_send_identity_chain_events() + .returning(|_| Ok(())) + .once(); + company_chain_store + .expect_get_chain() + .returning(|_| Ok(get_valid_company_chain())) + .once(); + // sends company block + notification + .expect_send_company_chain_events() + .returning(|_| Ok(())) + .once(); let service = get_service( storage, @@ -1169,7 +1238,7 @@ pub mod tests { contact_store, identity_chain_store, mut company_chain_store, - notification, + mut notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1177,6 +1246,16 @@ pub mod tests { company_chain_store .expect_add_block() .returning(|_, _| Ok(())); + company_chain_store + .expect_get_chain() + .returning(|_| Ok(get_valid_company_chain())) + .once(); + // sends company block + notification + .expect_send_company_chain_events() + .returning(|_| Ok(())) + .once(); + let node_id_clone = node_id.clone(); storage.expect_get().returning(move |_| { let mut data = get_baseline_company_data().1.0; @@ -1405,7 +1484,7 @@ pub mod tests { mut contact_store, mut identity_chain_store, mut company_chain_store, - notification, + mut notification, ) = get_storages(); let signatory_node_id = NodeId::new(BcrKeys::new().pub_key(), bitcoin::Network::Testnet); storage.expect_exists().returning(|_| true); @@ -1419,6 +1498,15 @@ pub mod tests { company_chain_store .expect_add_block() .returning(|_, _| Ok(())); + company_chain_store + .expect_get_chain() + .returning(|_| Ok(get_valid_company_chain())) + .once(); + // sends company block + notification + .expect_send_company_chain_events() + .returning(|_| Ok(())) + .once(); let signatory_node_id_clone = signatory_node_id.clone(); contact_store.expect_get_map().returning(move || { let mut map = HashMap::new(); @@ -1453,6 +1541,11 @@ pub mod tests { identity_chain_store .expect_add_block() .returning(|_| Ok(())); + // sends identity block + notification + .expect_send_identity_chain_events() + .returning(|_| Ok(())) + .once(); let service = get_service( storage, file_upload_store, @@ -1703,7 +1796,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, - notification, + mut notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1743,6 +1836,20 @@ pub mod tests { identity_chain_store .expect_add_block() .returning(|_| Ok(())); + // sends identity block + notification + .expect_send_identity_chain_events() + .returning(|_| Ok(())) + .once(); + company_chain_store + .expect_get_chain() + .returning(|_| Ok(get_valid_company_chain())) + .once(); + // sends company block + notification + .expect_send_company_chain_events() + .returning(|_| Ok(())) + .once(); let service = get_service( storage, file_upload_store, @@ -1806,7 +1913,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, - notification, + mut notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1814,6 +1921,15 @@ pub mod tests { company_chain_store .expect_add_block() .returning(|_, _| Ok(())); + company_chain_store + .expect_get_chain() + .returning(|_| Ok(get_valid_company_chain())) + .once(); + // sends company block + notification + .expect_send_company_chain_events() + .returning(|_| Ok(())) + .once(); company_chain_store.expect_remove().returning(|_| Ok(())); storage.expect_exists().returning(|_| true); let keys_clone = keys.clone(); @@ -1858,6 +1974,11 @@ pub mod tests { identity_chain_store .expect_add_block() .returning(|_| Ok(())); + // sends identity block + notification + .expect_send_identity_chain_events() + .returning(|_| Ok(())) + .once(); let service = get_service( storage, file_upload_store, diff --git a/crates/bcr-ebill-api/src/service/identity_service.rs b/crates/bcr-ebill-api/src/service/identity_service.rs index e8c896d3..1fa62445 100644 --- a/crates/bcr-ebill-api/src/service/identity_service.rs +++ b/crates/bcr-ebill-api/src/service/identity_service.rs @@ -17,6 +17,7 @@ use bcr_ebill_core::identity::validation::{validate_create_identity, validate_up use bcr_ebill_core::identity::{ActiveIdentityState, IdentityType}; use bcr_ebill_core::{NodeId, ServiceTraitBounds, ValidationError}; use bcr_ebill_transport::NotificationServiceApi; +use bcr_ebill_transport::event::identity_events::IdentityChainEvent; use log::{debug, error, info}; use std::sync::Arc; @@ -163,6 +164,18 @@ impl IdentityService { nostr_hash: nostr_hash.to_string(), }) } + + async fn populate_block( + &self, + identity: &Identity, + block: &IdentityBlock, + keys: &BcrKeys, + ) -> Result<()> { + self.notification_service + .send_identity_chain_events(IdentityChainEvent::new(identity, block, keys)) + .await?; + Ok(()) + } } impl ServiceTraitBounds for IdentityService {} @@ -319,8 +332,8 @@ impl IdentityServiceApi for IdentityService { timestamp, )?; self.blockchain_store.add_block(&new_block).await?; - self.store.save(&identity).await?; + self.populate_block(&identity, &new_block, &keys).await?; debug!("updated identity"); Ok(()) } @@ -425,6 +438,8 @@ impl IdentityServiceApi for IdentityService { // persist the identity in the DB self.store.save(&identity).await?; + self.populate_block(&identity, first_block, &keys).await?; + debug!("created identity"); Ok(()) } @@ -528,8 +543,8 @@ impl IdentityServiceApi for IdentityService { timestamp, )?; self.blockchain_store.add_block(&new_block).await?; - self.store.save(&identity).await?; + self.populate_block(&identity, &new_block, &keys).await?; debug!("deanonymized identity"); Ok(()) } @@ -649,13 +664,14 @@ mod tests { fn get_service_with_chain_storage( mock_storage: MockIdentityStoreApiMock, mock_chain_storage: MockIdentityChainStoreApiMock, + notification: MockNotificationService, ) -> IdentityService { IdentityService::new( Arc::new(mock_storage), Arc::new(MockFileUploadStoreApiMock::new()), Arc::new(MockFileStorageClientApi::new()), Arc::new(mock_chain_storage), - Arc::new(MockNotificationService::new()), + Arc::new(notification), ) } @@ -672,8 +688,13 @@ mod tests { .returning(|| Ok(BcrKeys::new())); let mut chain_storage = MockIdentityChainStoreApiMock::new(); chain_storage.expect_add_block().returning(|_| Ok(())); + let mut notification = MockNotificationService::new(); + notification + .expect_send_identity_chain_events() + .returning(|_| Ok(())) + .once(); - let service = get_service_with_chain_storage(storage, chain_storage); + let service = get_service_with_chain_storage(storage, chain_storage, notification); let res = service .create_identity( IdentityType::Ident, @@ -697,6 +718,7 @@ mod tests { async fn create_anon_identity_baseline() { init_test_cfg(); let mut storage = MockIdentityStoreApiMock::new(); + storage .expect_get_or_create_key_pair() .returning(|| Ok(BcrKeys::new())); @@ -706,8 +728,13 @@ mod tests { .returning(|| Ok(BcrKeys::new())); let mut chain_storage = MockIdentityChainStoreApiMock::new(); chain_storage.expect_add_block().returning(|_| Ok(())); + let mut notification = MockNotificationService::new(); + notification + .expect_send_identity_chain_events() + .returning(|_| Ok(())) + .once(); - let service = get_service_with_chain_storage(storage, chain_storage); + let service = get_service_with_chain_storage(storage, chain_storage, notification); let res = service .create_identity( IdentityType::Anon, @@ -755,8 +782,13 @@ mod tests { .clone(), ) }); + let mut notification = MockNotificationService::new(); + notification + .expect_send_identity_chain_events() + .returning(|_| Ok(())) + .once(); - let service = get_service_with_chain_storage(storage, chain_storage); + let service = get_service_with_chain_storage(storage, chain_storage, notification); let res = service .deanonymize_identity( IdentityType::Ident, @@ -795,8 +827,10 @@ mod tests { }); let mut chain_storage = MockIdentityChainStoreApiMock::new(); chain_storage.expect_add_block().returning(|_| Ok(())); + let mut notification = MockNotificationService::new(); + notification.expect_send_identity_chain_events().never(); - let service = get_service_with_chain_storage(storage, chain_storage); + let service = get_service_with_chain_storage(storage, chain_storage, notification); let res = service .deanonymize_identity( IdentityType::Anon, @@ -838,8 +872,10 @@ mod tests { }); let mut chain_storage = MockIdentityChainStoreApiMock::new(); chain_storage.expect_add_block().returning(|_| Ok(())); + let mut notification = MockNotificationService::new(); + notification.expect_send_identity_chain_events().never(); - let service = get_service_with_chain_storage(storage, chain_storage); + let service = get_service_with_chain_storage(storage, chain_storage, notification); let res = service .deanonymize_identity( IdentityType::Ident, @@ -886,8 +922,13 @@ mod tests { ) }); chain_storage.expect_add_block().returning(|_| Ok(())); + let mut notification = MockNotificationService::new(); + notification + .expect_send_identity_chain_events() + .returning(|_| Ok(())) + .once(); - let service = get_service_with_chain_storage(storage, chain_storage); + let service = get_service_with_chain_storage(storage, chain_storage, notification); let res = service .update_identity( Some("new_name".to_string()), @@ -963,9 +1004,14 @@ mod tests { .clone(), ) }); - chain_storage.expect_add_block().returning(|_| Ok(())); - - let service = get_service_with_chain_storage(storage, chain_storage); + chain_storage + .expect_add_block() + .returning(|_| Ok(())) + .once(); + let mut notification = MockNotificationService::new(); + notification.expect_send_identity_chain_events().never(); + + let service = get_service_with_chain_storage(storage, chain_storage, notification); let res = service .update_identity( Some("new_name".to_string()), diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index 731a7b07..3674b734 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -245,13 +245,13 @@ impl DefaultNotificationService { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl NotificationServiceApi for DefaultNotificationService { /// Sent when an identity chain is created or updated - async fn send_identity_chain_events(&self, events: &IdentityChainEvent) -> Result<()> { + async fn send_identity_chain_events(&self, events: IdentityChainEvent) -> Result<()> { info!("sending identity chain events with {events:#?}"); Ok(()) } /// Sent when a company chain is created or updated - async fn send_company_chain_events(&self, events: &CompanyChainEvent) -> Result<()> { + async fn send_company_chain_events(&self, events: CompanyChainEvent) -> Result<()> { info!("sending company chain events with {events:#?}"); Ok(()) } diff --git a/crates/bcr-ebill-api/src/tests/mod.rs b/crates/bcr-ebill-api/src/tests/mod.rs index 42c9f1d2..0432cb8c 100644 --- a/crates/bcr-ebill-api/src/tests/mod.rs +++ b/crates/bcr-ebill-api/src/tests/mod.rs @@ -373,8 +373,8 @@ pub mod tests { #[async_trait] impl NotificationServiceApi for NotificationService { - async fn send_identity_chain_events(&self, events: &IdentityChainEvent) -> bcr_ebill_transport::Result<()>; - async fn send_company_chain_events(&self, events: &CompanyChainEvent) -> bcr_ebill_transport::Result<()>; + async fn send_identity_chain_events(&self, events: IdentityChainEvent) -> bcr_ebill_transport::Result<()>; + async fn send_company_chain_events(&self, events: CompanyChainEvent) -> bcr_ebill_transport::Result<()>; async fn send_bill_is_signed_event(&self, event: &BillChainEvent) -> bcr_ebill_transport::Result<()>; async fn send_bill_is_accepted_event(&self, event: &BillChainEvent) -> bcr_ebill_transport::Result<()>; async fn send_request_to_accept_event(&self, event: &BillChainEvent) -> bcr_ebill_transport::Result<()>; diff --git a/crates/bcr-ebill-transport/src/event/company_events.rs b/crates/bcr-ebill-transport/src/event/company_events.rs index 0d351008..d4a81d35 100644 --- a/crates/bcr-ebill-transport/src/event/company_events.rs +++ b/crates/bcr-ebill-transport/src/event/company_events.rs @@ -7,8 +7,6 @@ use bcr_ebill_core::{ company::{Company, CompanyKeys}, }; -use crate::Result; - use super::{Event, blockchain_event::CompanyBlockEvent}; #[derive(Clone, Debug)] @@ -28,14 +26,14 @@ impl CompanyChainEvent { chain: &CompanyBlockchain, keys: &CompanyKeys, new_blocks: bool, - ) -> Result { - Ok(Self { + ) -> Self { + Self { company: company.clone(), chain: chain.clone(), keys: keys.clone(), new_blocks, sender_node_id: company.id.to_owned(), - }) + } } pub fn sender(&self) -> NodeId { diff --git a/crates/bcr-ebill-transport/src/event/identity_events.rs b/crates/bcr-ebill-transport/src/event/identity_events.rs index d85d1290..ddd60f52 100644 --- a/crates/bcr-ebill-transport/src/event/identity_events.rs +++ b/crates/bcr-ebill-transport/src/event/identity_events.rs @@ -1,23 +1,14 @@ use bcr_ebill_core::{ - NodeId, - blockchain::{ - Blockchain, - identity::{IdentityBlock, IdentityBlockchain}, - }, - identity::Identity, - util::BcrKeys, + NodeId, blockchain::identity::IdentityBlock, identity::Identity, util::BcrKeys, }; -use crate::Result; - use super::{Event, blockchain_event::IdentityBlockEvent}; #[derive(Clone, Debug)] pub struct IdentityChainEvent { - pub identity: Identity, - chain: IdentityBlockchain, + identity: Identity, + block: IdentityBlock, pub keys: BcrKeys, - new_blocks: bool, sender_node_id: NodeId, } @@ -25,43 +16,25 @@ impl IdentityChainEvent { /// Create a new IdentityChainEvent instance. New blocks indicate whether the given chain contains /// new blocks for the identity. Currently we only send a message if a new block has been /// added. - pub fn new( - identity: &Identity, - chain: &IdentityBlockchain, - keys: &BcrKeys, - new_blocks: bool, - ) -> Result { - Ok(Self { + pub fn new(identity: &Identity, block: &IdentityBlock, keys: &BcrKeys) -> Self { + Self { identity: identity.clone(), - chain: chain.clone(), + block: block.clone(), keys: keys.clone(), - new_blocks, sender_node_id: identity.node_id.to_owned(), - }) + } } pub fn sender(&self) -> NodeId { self.sender_node_id.clone() } - // Returns the latest block in the chain. - fn latest_block(&self) -> IdentityBlock { - self.chain.get_latest_block().clone() - } - - pub fn block_height(&self) -> usize { - self.chain.block_height() - } - /// generates the latest block event for the bill. pub fn generate_blockchain_message(&self) -> Option> { - if !self.new_blocks { - return None; - } Some(Event::new_identity_chain(IdentityBlockEvent { node_id: self.identity.node_id.clone(), - block_height: self.block_height(), - block: self.latest_block(), + block_height: self.block.id as usize, + block: self.block.clone(), })) } } diff --git a/crates/bcr-ebill-transport/src/notification_service.rs b/crates/bcr-ebill-transport/src/notification_service.rs index 29a924f6..de3b3e86 100644 --- a/crates/bcr-ebill-transport/src/notification_service.rs +++ b/crates/bcr-ebill-transport/src/notification_service.rs @@ -29,9 +29,9 @@ impl ServiceTraitBounds for MockNotificationServiceApi {} #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NotificationServiceApi: ServiceTraitBounds { /// Sent when an identity chain is created or updated - async fn send_identity_chain_events(&self, events: &IdentityChainEvent) -> Result<()>; + async fn send_identity_chain_events(&self, events: IdentityChainEvent) -> Result<()>; /// Sent when a company chain is created or updated - async fn send_company_chain_events(&self, events: &CompanyChainEvent) -> Result<()>; + async fn send_company_chain_events(&self, events: CompanyChainEvent) -> Result<()>; /// Sent when: A bill is signed by: Drawer /// Receiver: Payer, Action: AcceptBill /// Receiver: Payee, Action: CheckBill From aa15ce9dc9fb9db0dca967ca11b3ae993589bd47 Mon Sep 17 00:00:00 2001 From: tompro Date: Fri, 4 Jul 2025 13:43:55 +0200 Subject: [PATCH 05/11] Actually send chain events and store them --- .../notification_service/default_service.rs | 197 ++++++++++++++---- 1 file changed, 153 insertions(+), 44 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index 3674b734..e3bbabc0 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -148,33 +148,42 @@ impl DefaultNotificationService { Ok(()) } + async fn find_root_and_previous_event( + &self, + previous_hash: &str, + chain_id: &str, + chain_type: BlockchainType, + ) -> Result<(Option, Option)> { + // find potential previous block event + let previous_event = self + .chain_event_store + .find_by_block_hash(previous_hash) + .await + .map_err(|_| Error::Persistence("failed to read from chain events".to_owned()))?; + + // if there is a previous and it is not the root event, also get the root event + let root_event = if previous_event.clone().is_some_and(|f| !f.is_root_event()) { + self.chain_event_store + .find_root_event(chain_id, chain_type) + .await + .map_err(|_| Error::Persistence("failed to read from chain events".to_owned()))? + } else { + previous_event.clone() + }; + Ok((previous_event, root_event)) + } + // sends all required bill chain events like public bill data and bill invites async fn send_bill_chain_events(&self, events: &BillChainEvent) -> Result<()> { if let Some(node) = self.notification_transport.get(&events.sender()) { if let Some(block_event) = events.generate_blockchain_message() { - // find potential previous block event - let previous_event = self - .chain_event_store - .find_by_block_hash(&block_event.data.block.previous_hash) - .await - .map_err(|_| { - Error::Persistence("failed to read from chain events".to_owned()) - })?; - - // if there is a previous and it is not the root event, also get the root event - let root_event = if previous_event.clone().is_some_and(|f| !f.is_root_event()) { - self.chain_event_store - .find_root_event( - &block_event.data.bill_id.to_string(), - BlockchainType::Bill, - ) - .await - .map_err(|_| { - Error::Persistence("failed to read from chain events".to_owned()) - })? - } else { - previous_event.clone() - }; + let (previous_event, root_event) = self + .find_root_and_previous_event( + &block_event.data.block.previous_hash, + &block_event.data.bill_id.to_string(), + BlockchainType::Bill, + ) + .await?; // now send the event let event = node @@ -189,27 +198,16 @@ impl DefaultNotificationService { ) .await?; - self.chain_event_store - .add_chain_event(NostrChainEvent { - event_id: event.id.to_string(), - root_id: root_event - .map(|e| e.event_id.to_string()) - .unwrap_or(event.id.to_string()), - reply_id: previous_event.map(|e| e.event_id.to_string()), - author: event.pubkey.to_string(), - chain_id: block_event.data.bill_id.to_string(), - chain_type: BlockchainType::Bill, - block_height: events.block_height(), - block_hash: block_event.data.block.hash.to_owned(), - received: block_event.data.block.timestamp, - time: event.created_at.as_u64(), - payload: event, - valid: true, - }) - .await - .map_err(|_| { - Error::Persistence("failed to write to chain events".to_owned()) - })?; + self.add_chain_event( + &event, + &root_event, + &previous_event, + &block_event.data.bill_id.to_string(), + BlockchainType::Bill, + block_event.data.block.id as usize, + &block_event.data.block.hash, + ) + .await?; } let invites = events.generate_bill_invite_events(); @@ -225,6 +223,39 @@ impl DefaultNotificationService { Ok(()) } + async fn add_chain_event( + &self, + event: &nostr::event::Event, + root: &Option, + previous: &Option, + chain_id: &str, + chain_type: BlockchainType, + block_height: usize, + block_hash: &str, + ) -> Result<()> { + self.chain_event_store + .add_chain_event(NostrChainEvent { + event_id: event.id.to_string(), + root_id: root + .clone() + .map(|e| e.event_id.to_string()) + .unwrap_or(event.id.to_string()), + reply_id: previous.clone().map(|e| e.event_id.to_string()), + author: event.pubkey.to_string(), + chain_id: chain_id.to_string(), + chain_type, + block_height, + block_hash: block_hash.to_owned(), + received: event.created_at.as_u64(), + time: event.created_at.as_u64(), + payload: event.clone(), + valid: true, + }) + .await + .map_err(|_| Error::Persistence("failed to write to chain events".to_owned()))?; + Ok(()) + } + async fn send_retry_message( &self, sender: &NodeId, @@ -247,12 +278,90 @@ impl NotificationServiceApi for DefaultNotificationService { /// Sent when an identity chain is created or updated async fn send_identity_chain_events(&self, events: IdentityChainEvent) -> Result<()> { info!("sending identity chain events with {events:#?}"); + if let Some(node) = self.notification_transport.get(&events.sender()) { + if let Some(event) = events.generate_blockchain_message() { + let (previous_event, root_event) = self + .find_root_and_previous_event( + &event.data.block.previous_hash, + &event.data.node_id.to_string(), + BlockchainType::Identity, + ) + .await?; + // now send the event + let nostr_event = node + .send_public_chain_event( + &event.data.node_id.to_string(), + BlockchainType::Identity, + event.data.block.timestamp, + events.keys.clone(), + event.clone().try_into()?, + previous_event.clone().map(|e| e.payload), + root_event.clone().map(|e| e.payload), + ) + .await?; + self.add_chain_event( + &nostr_event, + &root_event, + &previous_event, + &event.data.node_id.to_string(), + BlockchainType::Bill, + event.data.block.id as usize, + &event.data.block.hash, + ) + .await?; + } + } else { + error!( + "could not find transport instance for sender node {}", + events.sender() + ); + } + Ok(()) } /// Sent when a company chain is created or updated async fn send_company_chain_events(&self, events: CompanyChainEvent) -> Result<()> { info!("sending company chain events with {events:#?}"); + if let Some(node) = self.notification_transport.get(&events.sender()) { + if let Some(event) = events.generate_blockchain_message() { + let (previous_event, root_event) = self + .find_root_and_previous_event( + &event.data.block.previous_hash, + &event.data.node_id.to_string(), + BlockchainType::Identity, + ) + .await?; + // now send the event + let nostr_event = node + .send_public_chain_event( + &event.data.node_id.to_string(), + BlockchainType::Identity, + event.data.block.timestamp, + events.keys.clone().try_into()?, + event.clone().try_into()?, + previous_event.clone().map(|e| e.payload), + root_event.clone().map(|e| e.payload), + ) + .await?; + self.add_chain_event( + &nostr_event, + &root_event, + &previous_event, + &event.data.node_id.to_string(), + BlockchainType::Bill, + event.data.block.id as usize, + &event.data.block.hash, + ) + .await?; + } + } else { + error!( + "could not find transport instance for sender node {}", + events.sender() + ); + } + Ok(()) } From d79ed9d6bba18eac36a85b4d2fce89a568671e8f Mon Sep 17 00:00:00 2001 From: tompro Date: Mon, 7 Jul 2025 11:29:58 +0200 Subject: [PATCH 06/11] Fix event types --- .../notification_service/default_service.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index e3bbabc0..f74bc2e7 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -287,7 +287,7 @@ impl NotificationServiceApi for DefaultNotificationService { BlockchainType::Identity, ) .await?; - // now send the event + // send the event let nostr_event = node .send_public_chain_event( &event.data.node_id.to_string(), @@ -299,12 +299,13 @@ impl NotificationServiceApi for DefaultNotificationService { root_event.clone().map(|e| e.payload), ) .await?; + // and store the event locally self.add_chain_event( &nostr_event, &root_event, &previous_event, &event.data.node_id.to_string(), - BlockchainType::Bill, + BlockchainType::Identity, event.data.block.id as usize, &event.data.block.hash, ) @@ -329,14 +330,14 @@ impl NotificationServiceApi for DefaultNotificationService { .find_root_and_previous_event( &event.data.block.previous_hash, &event.data.node_id.to_string(), - BlockchainType::Identity, + BlockchainType::Company, ) .await?; - // now send the event + // send the event let nostr_event = node .send_public_chain_event( &event.data.node_id.to_string(), - BlockchainType::Identity, + BlockchainType::Company, event.data.block.timestamp, events.keys.clone().try_into()?, event.clone().try_into()?, @@ -344,12 +345,13 @@ impl NotificationServiceApi for DefaultNotificationService { root_event.clone().map(|e| e.payload), ) .await?; + // and store the event locally self.add_chain_event( &nostr_event, &root_event, &previous_event, &event.data.node_id.to_string(), - BlockchainType::Bill, + BlockchainType::Company, event.data.block.id as usize, &event.data.block.hash, ) From ddba9fa16fc607948d1f5cc4a62ae26f9613afb5 Mon Sep 17 00:00:00 2001 From: tompro Date: Mon, 7 Jul 2025 12:58:06 +0200 Subject: [PATCH 07/11] Dynamic addition of new Nostr transports --- .../src/service/company_service.rs | 10 ++ .../notification_service/default_service.rs | 130 +++++++++++++----- crates/bcr-ebill-api/src/tests/mod.rs | 1 + .../src/event/identity_events.rs | 2 +- .../src/notification_service.rs | 4 +- 5 files changed, 114 insertions(+), 33 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/company_service.rs b/crates/bcr-ebill-api/src/service/company_service.rs index b5851e10..ef6cbb99 100644 --- a/crates/bcr-ebill-api/src/service/company_service.rs +++ b/crates/bcr-ebill-api/src/service/company_service.rs @@ -348,6 +348,11 @@ impl CompanyServiceApi for CompanyService { .add_block(&id, create_company_block) .await?; + let bcr_keys: BcrKeys = company_keys.clone().try_into()?; + self.notification_service + .add_company_transport(&company, &bcr_keys) + .await?; + let company_chain = self.company_blockchain_store.get_chain(&id).await?; self.populate_block(&company, &company_chain, &company_keys) .await?; @@ -1126,6 +1131,11 @@ pub mod tests { .expect_get_chain() .returning(|_| Ok(get_valid_company_chain())) .once(); + // adds company client + notification + .expect_add_company_transport() + .returning(|_, _| Ok(())) + .once(); // sends company block notification .expect_send_company_chain_events() diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index f74bc2e7..43dfe40a 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -5,7 +5,9 @@ use std::sync::Arc; use async_trait::async_trait; use bcr_ebill_core::bill::BillId; use bcr_ebill_core::blockchain::BlockchainType; +use bcr_ebill_core::company::Company; use bcr_ebill_core::contact::{BillAnonParticipant, BillParticipant, ContactType}; +use bcr_ebill_core::util::BcrKeys; use bcr_ebill_persistence::nostr::{ NostrChainEvent, NostrChainEventStoreApi, NostrQueuedMessage, NostrQueuedMessageStoreApi, }; @@ -13,9 +15,11 @@ use bcr_ebill_transport::event::company_events::CompanyChainEvent; use bcr_ebill_transport::event::identity_events::IdentityChainEvent; use bcr_ebill_transport::transport::NostrContactData; use bcr_ebill_transport::{BillChainEvent, BillChainEventPayload, Error, Event, EventEnvelope}; -use log::{error, info, warn}; +use log::{debug, error, warn}; +use serde_json::Value; +use tokio::sync::Mutex; -use super::NotificationJsonTransportApi; +use super::{NostrClient, NostrConfig, NotificationJsonTransportApi}; use super::{NotificationServiceApi, Result}; use crate::data::{ bill::BitcreditBill, @@ -23,6 +27,7 @@ use crate::data::{ notification::{Notification, NotificationType}, }; use crate::data::{validate_bill_id_network, validate_node_id_network}; +use crate::get_config; use crate::persistence::notification::{NotificationFilter, NotificationStoreApi}; use crate::service::contact_service::ContactServiceApi; use bcr_ebill_core::notification::{ActionType, BillEventType}; @@ -32,7 +37,7 @@ use bcr_ebill_core::{NodeId, PostalAddress, ServiceTraitBounds}; /// send events via json and email transports. #[allow(dead_code)] pub struct DefaultNotificationService { - notification_transport: HashMap>, + notification_transport: Mutex>>, notification_store: Arc, contact_service: Arc, queued_message_store: Arc, @@ -54,11 +59,14 @@ impl DefaultNotificationService { chain_event_store: Arc, nostr_relays: Vec, ) -> Self { - Self { - notification_transport: notification_transport + let transports: Mutex>> = Mutex::new( + notification_transport .into_iter() .map(|t| (t.get_sender_node_id(), t)) .collect(), + ); + Self { + notification_transport: transports, notification_store, contact_service, queued_message_store, @@ -67,8 +75,16 @@ impl DefaultNotificationService { } } - fn get_local_identity(&self, node_id: &NodeId) -> Option { - if self.notification_transport.contains_key(node_id) { + async fn get_node_transport( + &self, + node_id: &NodeId, + ) -> Option> { + let transports = self.notification_transport.lock().await; + transports.get(node_id).cloned() + } + + async fn get_local_identity(&self, node_id: &NodeId) -> Option { + if self.get_node_transport(node_id).await.is_some() { Some(BillParticipant::Ident(BillIdentParticipant { // we create an ident, but it doesn't matter, since we just need the node id and nostr relay t: ContactType::Person, @@ -84,7 +100,7 @@ impl DefaultNotificationService { } async fn resolve_identity(&self, node_id: &NodeId) -> Option { - match self.get_local_identity(node_id) { + match self.get_local_identity(node_id).await { Some(id) => Some(id), None => { if let Ok(Some(identity)) = @@ -109,12 +125,37 @@ impl DefaultNotificationService { } } + async fn add_compay_client(&self, company: &Company, keys: &BcrKeys) -> Result<()> { + let config = get_config(); + let node_id = NodeId::new(keys.pub_key(), get_config().bitcoin_network()); + + let mut transports = self.notification_transport.lock().await; + if transports.contains_key(&node_id) { + debug!("transport for node {node_id} already present"); + return Ok(()); + } + + let nostr_config = NostrConfig::new( + keys.clone(), + config.nostr_config.relays.clone(), + company.name.clone(), + false, + node_id.clone(), + ); + + if let Ok(client) = NostrClient::new(&nostr_config).await { + debug!("added nostr client for {}", &nostr_config.get_npub()); + transports.insert(node_id, Arc::new(client)); + } + Ok(()) + } + async fn send_all_bill_events( &self, sender: &NodeId, events: HashMap>, ) -> Result<()> { - if let Some(node) = self.notification_transport.get(sender) { + if let Some(node) = self.get_node_transport(sender).await { for (node_id, event_to_process) in events.into_iter() { if let Some(identity) = self.resolve_identity(&node_id).await { if let Err(e) = node @@ -124,19 +165,12 @@ impl DefaultNotificationService { error!( "Failed to send block notification, will add it to retry queue: {e}" ); - let queue_message = NostrQueuedMessage { - id: uuid::Uuid::new_v4().to_string(), - sender_id: sender.to_owned(), - node_id: node_id.to_owned(), - payload: serde_json::to_value(event_to_process)?, - }; - if let Err(e) = self - .queued_message_store - .add_message(queue_message, Self::NOSTR_MAX_RETRIES) - .await - { - error!("Failed to add block notification to retry queue: {e}"); - } + self.queue_retry_message( + sender, + &node_id, + serde_json::to_value(event_to_process)?, + ) + .await?; } } else { warn!("Failed to find recipient in contacts for node_id: {node_id}"); @@ -148,6 +182,28 @@ impl DefaultNotificationService { Ok(()) } + async fn queue_retry_message( + &self, + sender: &NodeId, + recipient: &NodeId, + payload: Value, + ) -> Result<()> { + let queue_message = NostrQueuedMessage { + id: uuid::Uuid::new_v4().to_string(), + sender_id: sender.to_owned(), + node_id: recipient.to_owned(), + payload, + }; + if let Err(e) = self + .queued_message_store + .add_message(queue_message, Self::NOSTR_MAX_RETRIES) + .await + { + error!("Failed to add send nostr event to retry queue: {e}"); + } + Ok(()) + } + async fn find_root_and_previous_event( &self, previous_hash: &str, @@ -175,7 +231,7 @@ impl DefaultNotificationService { // sends all required bill chain events like public bill data and bill invites async fn send_bill_chain_events(&self, events: &BillChainEvent) -> Result<()> { - if let Some(node) = self.notification_transport.get(&events.sender()) { + if let Some(node) = self.get_node_transport(&events.sender()).await { if let Some(block_event) = events.generate_blockchain_message() { let (previous_event, root_event) = self .find_root_and_previous_event( @@ -262,7 +318,7 @@ impl DefaultNotificationService { node_id: &NodeId, message: EventEnvelope, ) -> Result<()> { - if let Some(node) = self.notification_transport.get(sender) { + if let Some(node) = self.get_node_transport(sender).await { if let Ok(Some(identity)) = self.contact_service.get_identity_by_node_id(node_id).await { node.send_private_event(&identity, message).await?; @@ -275,10 +331,18 @@ impl DefaultNotificationService { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl NotificationServiceApi for DefaultNotificationService { + /// Adds a new transport client for a company if it does not already exist + async fn add_company_transport(&self, company: &Company, keys: &BcrKeys) -> Result<()> { + self.add_compay_client(company, keys).await + } + /// Sent when an identity chain is created or updated async fn send_identity_chain_events(&self, events: IdentityChainEvent) -> Result<()> { - info!("sending identity chain events with {events:#?}"); - if let Some(node) = self.notification_transport.get(&events.sender()) { + debug!( + "sending identity chain events for node: {}", + events.identity.node_id + ); + if let Some(node) = self.get_node_transport(&events.sender()).await { if let Some(event) = events.generate_blockchain_message() { let (previous_event, root_event) = self .find_root_and_previous_event( @@ -323,8 +387,11 @@ impl NotificationServiceApi for DefaultNotificationService { /// Sent when a company chain is created or updated async fn send_company_chain_events(&self, events: CompanyChainEvent) -> Result<()> { - info!("sending company chain events with {events:#?}"); - if let Some(node) = self.notification_transport.get(&events.sender()) { + debug!( + "sending company chain events for company id: {}", + events.company.id + ); + if let Some(node) = self.get_node_transport(&events.sender()).await { if let Some(event) = events.generate_blockchain_message() { let (previous_event, root_event) = self .find_root_and_previous_event( @@ -538,7 +605,7 @@ impl NotificationServiceApi for DefaultNotificationService { action_type: Some(ActionType::CheckBill), sum: Some(bill.sum), }); - if let Some(node) = self.notification_transport.get(sender_node_id) { + if let Some(node) = self.get_node_transport(sender_node_id).await { node.send_private_event(mint, event.try_into()?).await?; } Ok(()) @@ -570,7 +637,7 @@ impl NotificationServiceApi for DefaultNotificationService { timed_out_action: ActionType, recipients: Vec, ) -> Result<()> { - if let Some(node) = self.notification_transport.get(sender_node_id) { + if let Some(node) = self.get_node_transport(sender_node_id).await { if let Some(event_type) = timed_out_action.get_timeout_event_type() { // only send to a recipient once let unique: HashMap = @@ -752,8 +819,9 @@ impl NotificationServiceApi for DefaultNotificationService { async fn resolve_contact(&self, node_id: &NodeId) -> Result> { validate_node_id_network(node_id)?; + let transports = self.notification_transport.lock().await; // take any transport - doesn't matter - if let Some((_node, transport)) = self.notification_transport.iter().next() { + if let Some((_node, transport)) = transports.iter().nth(0) { let res = transport.resolve_contact(node_id).await?; Ok(res) } else { diff --git a/crates/bcr-ebill-api/src/tests/mod.rs b/crates/bcr-ebill-api/src/tests/mod.rs index 0432cb8c..3137d01b 100644 --- a/crates/bcr-ebill-api/src/tests/mod.rs +++ b/crates/bcr-ebill-api/src/tests/mod.rs @@ -373,6 +373,7 @@ pub mod tests { #[async_trait] impl NotificationServiceApi for NotificationService { + async fn add_company_transport(&self, company: &Company, keys: &BcrKeys) -> bcr_ebill_transport::Result<()>; async fn send_identity_chain_events(&self, events: IdentityChainEvent) -> bcr_ebill_transport::Result<()>; async fn send_company_chain_events(&self, events: CompanyChainEvent) -> bcr_ebill_transport::Result<()>; async fn send_bill_is_signed_event(&self, event: &BillChainEvent) -> bcr_ebill_transport::Result<()>; diff --git a/crates/bcr-ebill-transport/src/event/identity_events.rs b/crates/bcr-ebill-transport/src/event/identity_events.rs index ddd60f52..f78ff707 100644 --- a/crates/bcr-ebill-transport/src/event/identity_events.rs +++ b/crates/bcr-ebill-transport/src/event/identity_events.rs @@ -6,7 +6,7 @@ use super::{Event, blockchain_event::IdentityBlockEvent}; #[derive(Clone, Debug)] pub struct IdentityChainEvent { - identity: Identity, + pub identity: Identity, block: IdentityBlock, pub keys: BcrKeys, sender_node_id: NodeId, diff --git a/crates/bcr-ebill-transport/src/notification_service.rs b/crates/bcr-ebill-transport/src/notification_service.rs index de3b3e86..2155dc8f 100644 --- a/crates/bcr-ebill-transport/src/notification_service.rs +++ b/crates/bcr-ebill-transport/src/notification_service.rs @@ -7,13 +7,13 @@ use crate::{ transport::NostrContactData, }; use async_trait::async_trait; -use bcr_ebill_core::ServiceTraitBounds; use bcr_ebill_core::{ NodeId, bill::{BillId, BitcreditBill}, contact::{BillIdentParticipant, BillParticipant}, notification::{ActionType, Notification}, }; +use bcr_ebill_core::{ServiceTraitBounds, company::Company, util::BcrKeys}; use bcr_ebill_persistence::notification::NotificationFilter; #[cfg(test)] use mockall::automock; @@ -28,6 +28,8 @@ impl ServiceTraitBounds for MockNotificationServiceApi {} #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NotificationServiceApi: ServiceTraitBounds { + /// Adds a new transport client for a company if it does not already exist + async fn add_company_transport(&self, company: &Company, keys: &BcrKeys) -> Result<()>; /// Sent when an identity chain is created or updated async fn send_identity_chain_events(&self, events: IdentityChainEvent) -> Result<()>; /// Sent when a company chain is created or updated From 1cd581f2ed12ee3f25b76d6ae8c4d290cfdfecd6 Mon Sep 17 00:00:00 2001 From: tompro Date: Tue, 8 Jul 2025 12:17:13 +0200 Subject: [PATCH 08/11] Company invite event generator --- .../src/service/company_service.rs | 24 ++++++++++++++----- .../src/event/bill_events.rs | 4 ++-- .../src/event/company_events.rs | 17 ++++++++++++- crates/bcr-ebill-transport/src/event/mod.rs | 9 +++++-- .../src/handler/bill_invite_handler.rs | 8 +++---- 5 files changed, 47 insertions(+), 15 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/company_service.rs b/crates/bcr-ebill-api/src/service/company_service.rs index ef6cbb99..4692f816 100644 --- a/crates/bcr-ebill-api/src/service/company_service.rs +++ b/crates/bcr-ebill-api/src/service/company_service.rs @@ -190,9 +190,16 @@ impl CompanyService { company: &Company, chain: &CompanyBlockchain, keys: &CompanyKeys, + new_signatory: Option, ) -> Result<()> { self.notification_service - .send_company_chain_events(CompanyChainEvent::new(company, chain, keys, true)) + .send_company_chain_events(CompanyChainEvent::new( + company, + chain, + keys, + new_signatory, + true, + )) .await?; Ok(()) } @@ -354,7 +361,7 @@ impl CompanyServiceApi for CompanyService { .await?; let company_chain = self.company_blockchain_store.get_chain(&id).await?; - self.populate_block(&company, &company_chain, &company_keys) + self.populate_block(&company, &company_chain, &company_keys, None) .await?; self.identity_blockchain_store.add_block(&new_block).await?; @@ -545,7 +552,7 @@ impl CompanyServiceApi for CompanyService { .add_block(id, &new_block) .await?; let company_chain = self.company_blockchain_store.get_chain(id).await?; - self.populate_block(&company, &company_chain, &company_keys) + self.populate_block(&company, &company_chain, &company_keys, None) .await?; debug!("company with id {id} updated"); @@ -635,8 +642,13 @@ impl CompanyServiceApi for CompanyService { .add_block(id, &new_block) .await?; let company_chain = self.company_blockchain_store.get_chain(id).await?; - self.populate_block(&company, &company_chain, &company_keys) - .await?; + self.populate_block( + &company, + &company_chain, + &company_keys, + Some(signatory_node_id.clone()), + ) + .await?; self.identity_blockchain_store .add_block(&new_identity_block) @@ -733,7 +745,7 @@ impl CompanyServiceApi for CompanyService { .add_block(id, &new_block) .await?; let company_chain = self.company_blockchain_store.get_chain(id).await?; - self.populate_block(&company, &company_chain, &company_keys) + self.populate_block(&company, &company_chain, &company_keys, None) .await?; self.identity_blockchain_store diff --git a/crates/bcr-ebill-transport/src/event/bill_events.rs b/crates/bcr-ebill-transport/src/event/bill_events.rs index 6fea30d7..1792746f 100644 --- a/crates/bcr-ebill-transport/src/event/bill_events.rs +++ b/crates/bcr-ebill-transport/src/event/bill_events.rs @@ -119,7 +119,7 @@ impl BillChainEvent { if !self.new_blocks { return None; } - Some(Event::new_chain(BillBlockEvent { + Some(Event::new_bill_chain(BillBlockEvent { bill_id: self.bill.id.to_owned(), block_height: self.block_height(), block: self.latest_block(), @@ -130,7 +130,7 @@ impl BillChainEvent { let invite = ChainInvite::bill(self.bill.id.to_string(), self.bill_keys.clone()); self.new_participants() .keys() - .map(|node_id| (node_id.to_owned(), Event::new_invite(invite.clone()))) + .map(|node_id| (node_id.to_owned(), Event::new_bill_invite(invite.clone()))) .collect() } } diff --git a/crates/bcr-ebill-transport/src/event/company_events.rs b/crates/bcr-ebill-transport/src/event/company_events.rs index d4a81d35..ce63ff54 100644 --- a/crates/bcr-ebill-transport/src/event/company_events.rs +++ b/crates/bcr-ebill-transport/src/event/company_events.rs @@ -7,13 +7,17 @@ use bcr_ebill_core::{ company::{Company, CompanyKeys}, }; -use super::{Event, blockchain_event::CompanyBlockEvent}; +use super::{ + Event, + blockchain_event::{ChainInvite, CompanyBlockEvent}, +}; #[derive(Clone, Debug)] pub struct CompanyChainEvent { pub company: Company, chain: CompanyBlockchain, pub keys: CompanyKeys, + new_signatory: Option, new_blocks: bool, sender_node_id: NodeId, } @@ -25,12 +29,14 @@ impl CompanyChainEvent { company: &Company, chain: &CompanyBlockchain, keys: &CompanyKeys, + new_signatory: Option, new_blocks: bool, ) -> Self { Self { company: company.clone(), chain: chain.clone(), keys: keys.clone(), + new_signatory, new_blocks, sender_node_id: company.id.to_owned(), } @@ -60,4 +66,13 @@ impl CompanyChainEvent { block: self.latest_block(), })) } + + pub fn generate_company_invite_message(&self) -> Option<(NodeId, Event)> { + if let Some(node_id) = self.new_signatory.as_ref() { + let invite = ChainInvite::company(self.company.id.to_string(), self.keys.clone()); + Some((node_id.clone(), Event::new_company_invite(invite))) + } else { + None + } + } } diff --git a/crates/bcr-ebill-transport/src/event/mod.rs b/crates/bcr-ebill-transport/src/event/mod.rs index 5cc75051..5572d2e2 100644 --- a/crates/bcr-ebill-transport/src/event/mod.rs +++ b/crates/bcr-ebill-transport/src/event/mod.rs @@ -20,6 +20,8 @@ pub enum EventType { IdentityChain, /// Public company chain events CompanyChain, + /// Private company invites with keys + CompanyChainInvite, } impl EventType { @@ -58,7 +60,7 @@ impl Event { Self::new(EventType::Bill, data) } - pub fn new_chain(data: T) -> Self { + pub fn new_bill_chain(data: T) -> Self { Self::new(EventType::BillChain, data) } @@ -70,9 +72,12 @@ impl Event { Self::new(EventType::CompanyChain, data) } - pub fn new_invite(data: T) -> Self { + pub fn new_bill_invite(data: T) -> Self { Self::new(EventType::BillChainInvite, data) } + pub fn new_company_invite(data: T) -> Self { + Self::new(EventType::CompanyChainInvite, data) + } } /// The event version that is used for all events if no specific version diff --git a/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs b/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs index 1bfb2ae3..a8249097 100644 --- a/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs +++ b/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs @@ -446,7 +446,7 @@ mod tests { .times(1); let event = generate_test_event(&BcrKeys::new(), None, None, 1); - let invite = Event::new_invite(ChainInvite::bill( + let invite = Event::new_bill_invite(ChainInvite::bill( bill_id_test().to_string(), get_bill_keys(), )) @@ -495,7 +495,7 @@ mod tests { .times(3); let event = generate_test_event(&BcrKeys::new(), None, None, 1); - let invite = Event::new_invite(ChainInvite::bill( + let invite = Event::new_bill_invite(ChainInvite::bill( bill_id_test().to_string(), get_bill_keys(), )) @@ -552,7 +552,7 @@ mod tests { .times(1); let event = generate_test_event(&BcrKeys::new(), None, None, 1); - let invite = Event::new_invite(ChainInvite::bill( + let invite = Event::new_bill_invite(ChainInvite::bill( bill_id_test().to_string(), get_bill_keys(), )) @@ -653,7 +653,7 @@ mod tests { .expect("could not get block") .clone(); - Event::new_chain(BillBlockEvent { + Event::new_bill_chain(BillBlockEvent { bill_id: bill_id_test(), block: block.clone(), block_height, From 62d800ff05ef8f7a9d7060834c6030611279c70f Mon Sep 17 00:00:00 2001 From: tompro Date: Tue, 8 Jul 2025 13:10:36 +0200 Subject: [PATCH 09/11] Send company invite --- .../src/service/notification_service/default_service.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index 43dfe40a..208cd2af 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -424,6 +424,14 @@ impl NotificationServiceApi for DefaultNotificationService { ) .await?; } + + // handle potential invite for new signatory + if let Some((recipient, invite)) = events.generate_company_invite_message() { + if let Some(identity) = self.resolve_identity(&recipient).await { + node.send_private_event(&identity, invite.try_into()?) + .await?; + } + } } else { error!( "could not find transport instance for sender node {}", From ae6e05e91c29965b5a049f679a2363421d269df4 Mon Sep 17 00:00:00 2001 From: tompro Date: Tue, 8 Jul 2025 13:39:14 +0200 Subject: [PATCH 10/11] Less verbose logging --- .../bcr-ebill-api/src/service/notification_service/nostr.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/notification_service/nostr.rs b/crates/bcr-ebill-api/src/service/notification_service/nostr.rs index 91ea8836..02b5d5d3 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/nostr.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/nostr.rs @@ -9,7 +9,7 @@ use bcr_ebill_transport::{ decrypt_public_chain_event, unwrap_direct_message, unwrap_public_chain_event, }, }; -use log::{error, info, trace, warn}; +use log::{debug, error, info, trace, warn}; use nostr::signer::NostrSigner; use nostr_sdk::{ Alphabet, Client, Event, EventBuilder, EventId, Filter, Kind, Metadata, Options, PublicKey, @@ -538,12 +538,12 @@ impl NostrConsumer { } Kind::RelayList => { // we have not subscribed to relaylist events yet - info!("Received relay list: {event:?}"); + debug!("Received relay list from: {}", event.pubkey); (true, 0u64) } Kind::Metadata => { // we have not subscribed to metadata events yet - info!("Received metadata: {event:?}"); + debug!("Received metadata from: {}", event.pubkey); (true, 0u64) } _ => (true, 0u64), From fe7d55b67c4e0a2bed7c30f0874462efef3b90a1 Mon Sep 17 00:00:00 2001 From: tompro Date: Tue, 8 Jul 2025 15:48:27 +0200 Subject: [PATCH 11/11] Review fixes --- .../src/service/notification_service/default_service.rs | 4 ++-- crates/bcr-ebill-transport/src/event/company_events.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs index 208cd2af..d31476d6 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/default_service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/default_service.rs @@ -125,7 +125,7 @@ impl DefaultNotificationService { } } - async fn add_compay_client(&self, company: &Company, keys: &BcrKeys) -> Result<()> { + async fn add_company_client(&self, company: &Company, keys: &BcrKeys) -> Result<()> { let config = get_config(); let node_id = NodeId::new(keys.pub_key(), get_config().bitcoin_network()); @@ -333,7 +333,7 @@ impl DefaultNotificationService { impl NotificationServiceApi for DefaultNotificationService { /// Adds a new transport client for a company if it does not already exist async fn add_company_transport(&self, company: &Company, keys: &BcrKeys) -> Result<()> { - self.add_compay_client(company, keys).await + self.add_company_client(company, keys).await } /// Sent when an identity chain is created or updated diff --git a/crates/bcr-ebill-transport/src/event/company_events.rs b/crates/bcr-ebill-transport/src/event/company_events.rs index ce63ff54..5ff95862 100644 --- a/crates/bcr-ebill-transport/src/event/company_events.rs +++ b/crates/bcr-ebill-transport/src/event/company_events.rs @@ -60,7 +60,7 @@ impl CompanyChainEvent { if !self.new_blocks { return None; } - Some(Event::new_identity_chain(CompanyBlockEvent { + Some(Event::new_company_chain(CompanyBlockEvent { node_id: self.company.id.clone(), block_height: self.block_height(), block: self.latest_block(),