diff --git a/crates/bcr-ebill-api/src/service/company_service.rs b/crates/bcr-ebill-api/src/service/company_service.rs index 30d61e04..4692f816 100644 --- a/crates/bcr-ebill-api/src/service/company_service.rs +++ b/crates/bcr-ebill-api/src/service/company_service.rs @@ -28,6 +28,9 @@ 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 bcr_ebill_transport::event::company_events::CompanyChainEvent; +use bcr_ebill_transport::event::identity_events::IdentityChainEvent; use log::{debug, error, info}; use std::sync::Arc; @@ -115,6 +118,7 @@ pub struct CompanyService { contact_store: Arc, identity_blockchain_store: Arc, company_blockchain_store: Arc, + notification_service: Arc, } impl CompanyService { @@ -126,6 +130,7 @@ impl CompanyService { contact_store: Arc, identity_blockchain_store: Arc, company_blockchain_store: Arc, + notification_service: Arc, ) -> Self { Self { store, @@ -135,6 +140,7 @@ impl CompanyService { contact_store, identity_blockchain_store, company_blockchain_store, + notification_service, } } @@ -178,6 +184,25 @@ impl CompanyService { nostr_hash: nostr_hash.to_string(), }) } + + async fn populate_block( + &self, + company: &Company, + chain: &CompanyBlockchain, + keys: &CompanyKeys, + new_signatory: Option, + ) -> Result<()> { + self.notification_service + .send_company_chain_events(CompanyChainEvent::new( + company, + chain, + keys, + new_signatory, + true, + )) + .await?; + Ok(()) + } } impl ServiceTraitBounds for CompanyService {} @@ -329,7 +354,25 @@ impl CompanyServiceApi for CompanyService { self.company_blockchain_store .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, None) + .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 @@ -508,6 +551,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, None) + .await?; + debug!("company with id {id} updated"); if let Some(upload_id) = logo_file_upload_id { @@ -594,9 +641,26 @@ 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, + Some(signatory_node_id.clone()), + ) + .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 @@ -680,9 +744,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, None) + .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 @@ -757,8 +832,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 +847,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 +857,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 +869,7 @@ pub mod tests { MockContactStoreApiMock, MockIdentityChainStoreApiMock, MockCompanyChainStoreApiMock, + MockNotificationService, ) { ( MockCompanyStoreApiMock::new(), @@ -801,6 +879,7 @@ pub mod tests { MockContactStoreApiMock::new(), MockIdentityChainStoreApiMock::new(), MockCompanyChainStoreApiMock::new(), + MockNotificationService::new(), ) } @@ -830,8 +909,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(), @@ -839,8 +921,6 @@ pub mod tests { 1731593928, ) .unwrap() - .get_latest_block() - .to_owned() } #[tokio::test] @@ -853,6 +933,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 +949,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service.get_list_of_companies().await; @@ -886,6 +968,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 +983,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 +999,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); storage.expect_exists().returning(|_| true); storage @@ -931,6 +1016,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 +1034,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 +1045,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 +1061,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 +1077,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 +1093,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, + mut notification, ) = get_storages(); company_chain_store .expect_add_block() @@ -1043,6 +1134,25 @@ 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(); + // adds company client + notification + .expect_add_company_transport() + .returning(|_, _| Ok(())) + .once(); + // sends company block + notification + .expect_send_company_chain_events() + .returning(|_| Ok(())) + .once(); let service = get_service( storage, @@ -1052,6 +1162,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service @@ -1095,6 +1206,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 +1229,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .create_company( @@ -1147,6 +1260,7 @@ pub mod tests { contact_store, identity_chain_store, mut company_chain_store, + mut notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1154,6 +1268,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; @@ -1193,6 +1317,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .edit_company( @@ -1222,6 +1347,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 +1358,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .edit_company( @@ -1261,6 +1388,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 +1411,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .edit_company( @@ -1312,6 +1441,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 +1476,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .edit_company( @@ -1375,6 +1506,7 @@ pub mod tests { mut contact_store, mut identity_chain_store, mut company_chain_store, + mut notification, ) = get_storages(); let signatory_node_id = NodeId::new(BcrKeys::new().pub_key(), bitcoin::Network::Testnet); storage.expect_exists().returning(|_| true); @@ -1388,6 +1520,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(); @@ -1422,6 +1563,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, @@ -1430,6 +1576,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 +1594,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 +1622,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 +1640,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 +1661,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 +1679,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 +1697,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 +1715,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 +1747,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 +1765,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 +1800,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 +1818,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, + mut notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1700,6 +1858,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, @@ -1708,6 +1880,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 +1898,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 +1916,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 +1935,7 @@ pub mod tests { contact_store, mut identity_chain_store, mut company_chain_store, + mut notification, ) = get_storages(); company_chain_store .expect_get_latest_block() @@ -1767,6 +1943,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(); @@ -1811,6 +1996,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, @@ -1819,6 +2009,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service .remove_signatory( @@ -1840,6 +2031,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 +2058,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 +2076,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 +2101,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 +2119,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 +2151,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 +2176,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ) = get_storages(); file_upload_client @@ -2003,6 +2201,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let file = service @@ -2045,6 +2244,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 +2259,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); assert!( @@ -2085,6 +2286,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 +2301,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); assert!( @@ -2124,6 +2327,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 +2352,7 @@ pub mod tests { contact_store, identity_chain_store, company_chain_store, + notification, ); let res = service.list_signatories(&node_id_test()).await; @@ -2165,6 +2370,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 +2381,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..1fa62445 100644 --- a/crates/bcr-ebill-api/src/service/identity_service.rs +++ b/crates/bcr-ebill-api/src/service/identity_service.rs @@ -16,6 +16,8 @@ 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 bcr_ebill_transport::event::identity_events::IdentityChainEvent; use log::{debug, error, info}; use std::sync::Arc; @@ -102,6 +104,7 @@ pub struct IdentityService { file_upload_store: Arc, file_upload_client: Arc, blockchain_store: Arc, + notification_service: Arc, } impl IdentityService { @@ -110,12 +113,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, } } @@ -159,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 {} @@ -315,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(()) } @@ -421,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(()) } @@ -524,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(()) } @@ -627,7 +646,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,18 +657,21 @@ mod tests { Arc::new(MockFileUploadStoreApiMock::new()), Arc::new(MockFileStorageClientApi::new()), Arc::new(MockIdentityChainStoreApiMock::new()), + Arc::new(MockNotificationService::new()), ) } 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(notification), ) } @@ -666,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, @@ -691,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())); @@ -700,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, @@ -749,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, @@ -789,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, @@ -832,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, @@ -880,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()), @@ -957,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 e5a227d4..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 @@ -5,15 +5,21 @@ 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, }; +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::{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, @@ -21,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}; @@ -30,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, @@ -52,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, @@ -65,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, @@ -82,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)) = @@ -107,12 +125,37 @@ impl DefaultNotificationService { } } - async fn send_all_events( + 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()); + + 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 @@ -122,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}"); @@ -146,33 +182,64 @@ 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, + 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(node) = self.get_node_transport(&events.sender()).await { 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 @@ -187,27 +254,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(); @@ -223,13 +279,46 @@ 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, 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?; @@ -242,6 +331,117 @@ 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_company_client(company, keys).await + } + + /// Sent when an identity chain is created or updated + async fn send_identity_chain_events(&self, events: IdentityChainEvent) -> Result<()> { + 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( + &event.data.block.previous_hash, + &event.data.node_id.to_string(), + BlockchainType::Identity, + ) + .await?; + // 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?; + // and store the event locally + self.add_chain_event( + &nostr_event, + &root_event, + &previous_event, + &event.data.node_id.to_string(), + BlockchainType::Identity, + 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<()> { + 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( + &event.data.block.previous_hash, + &event.data.node_id.to_string(), + BlockchainType::Company, + ) + .await?; + // send the event + let nostr_event = node + .send_public_chain_event( + &event.data.node_id.to_string(), + BlockchainType::Company, + 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?; + // and store the event locally + self.add_chain_event( + &nostr_event, + &root_event, + &previous_event, + &event.data.node_id.to_string(), + BlockchainType::Company, + event.data.block.id as usize, + &event.data.block.hash, + ) + .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 {}", + events.sender() + ); + } + + Ok(()) + } + async fn send_bill_is_signed_event(&self, event: &BillChainEvent) -> Result<()> { let event_type = BillEventType::BillSigned; @@ -261,7 +461,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 +476,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 +494,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 +509,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 +524,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 +539,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 +558,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 +577,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 +596,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(()) } @@ -404,7 +613,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(()) @@ -422,7 +631,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(()) } @@ -435,7 +645,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 = @@ -473,7 +683,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(()) } @@ -616,8 +827,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 { @@ -642,7 +854,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-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), diff --git a/crates/bcr-ebill-api/src/tests/mod.rs b/crates/bcr-ebill-api/src/tests/mod.rs index 7c86cd5f..3137d01b 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,9 @@ 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<()>; 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/bill_events.rs b/crates/bcr-ebill-transport/src/event/bill_events.rs index bccaa5a7..1792746f 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 { @@ -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/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..5ff95862 --- /dev/null +++ b/crates/bcr-ebill-transport/src/event/company_events.rs @@ -0,0 +1,78 @@ +use bcr_ebill_core::{ + NodeId, + blockchain::{ + Blockchain, + company::{CompanyBlock, CompanyBlockchain}, + }, + company::{Company, CompanyKeys}, +}; + +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, +} + +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_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(), + } + } + + 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_company_chain(CompanyBlockEvent { + node_id: self.company.id.clone(), + block_height: self.block_height(), + 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/identity_events.rs b/crates/bcr-ebill-transport/src/event/identity_events.rs new file mode 100644 index 00000000..f78ff707 --- /dev/null +++ b/crates/bcr-ebill-transport/src/event/identity_events.rs @@ -0,0 +1,40 @@ +use bcr_ebill_core::{ + NodeId, blockchain::identity::IdentityBlock, identity::Identity, util::BcrKeys, +}; + +use super::{Event, blockchain_event::IdentityBlockEvent}; + +#[derive(Clone, Debug)] +pub struct IdentityChainEvent { + pub identity: Identity, + block: IdentityBlock, + pub keys: BcrKeys, + 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, block: &IdentityBlock, keys: &BcrKeys) -> Self { + Self { + identity: identity.clone(), + block: block.clone(), + keys: keys.clone(), + sender_node_id: identity.node_id.to_owned(), + } + } + + pub fn sender(&self) -> NodeId { + self.sender_node_id.clone() + } + + /// generates the latest block event for the bill. + pub fn generate_blockchain_message(&self) -> Option> { + Some(Event::new_identity_chain(IdentityBlockEvent { + node_id: self.identity.node_id.clone(), + block_height: self.block.id as usize, + block: self.block.clone(), + })) + } +} diff --git a/crates/bcr-ebill-transport/src/event/mod.rs b/crates/bcr-ebill-transport/src/event/mod.rs index 7a140e4d..5572d2e2 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,12 @@ pub enum EventType { BillChain, /// Private Bill invites with keys BillChainInvite, + /// Public identity events + IdentityChain, + /// Public company chain events + CompanyChain, + /// Private company invites with keys + CompanyChainInvite, } impl EventType { @@ -22,6 +30,8 @@ impl EventType { EventType::Bill, EventType::BillChain, EventType::BillChainInvite, + EventType::IdentityChain, + EventType::CompanyChain, ] } } @@ -50,13 +60,24 @@ 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) } - pub fn new_invite(data: T) -> Self { + 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_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_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..a8249097 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}, }; @@ -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, diff --git a/crates/bcr-ebill-transport/src/notification_service.rs b/crates/bcr-ebill-transport/src/notification_service.rs index aa4f73f6..2155dc8f 100644 --- a/crates/bcr-ebill-transport/src/notification_service.rs +++ b/crates/bcr-ebill-transport/src/notification_service.rs @@ -1,12 +1,19 @@ -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::{ 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; @@ -21,6 +28,12 @@ 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 + 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 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);