diff --git a/crates/bcr-ebill-api/src/service/bill_service/mod.rs b/crates/bcr-ebill-api/src/service/bill_service/mod.rs index 43422ed3..a6d6d586 100644 --- a/crates/bcr-ebill-api/src/service/bill_service/mod.rs +++ b/crates/bcr-ebill-api/src/service/bill_service/mod.rs @@ -3729,8 +3729,10 @@ pub mod tests { .expect_send_bill_is_sold_event() .returning(|_, _| Ok(())); - // Populates identity block - expect_populates_identity_block(&mut ctx); + // Populates identity block and company block + ctx.notification_service + .expect_send_identity_chain_events() + .returning(|_| Ok(())); let service = get_service(ctx); @@ -3785,7 +3787,9 @@ pub mod tests { .returning(|_, _| Ok(())); // Populates identity block and company block - expect_populates_identity_block(&mut ctx); + ctx.notification_service + .expect_send_identity_chain_events() + .returning(|_| Ok(())); let service = get_service(ctx); diff --git a/crates/bcr-ebill-api/src/service/notification_service/service.rs b/crates/bcr-ebill-api/src/service/notification_service/service.rs index 7fffd2b9..06f9404b 100644 --- a/crates/bcr-ebill-api/src/service/notification_service/service.rs +++ b/crates/bcr-ebill-api/src/service/notification_service/service.rs @@ -199,4 +199,7 @@ pub trait NotificationServiceApi: ServiceTraitBounds { /// Fetch email notifications preferences link for the currently selected identity async fn get_email_notifications_preferences_link(&self, node_id: &NodeId) -> Result; + + /// Resync bill chain + async fn resync_bill_chain(&self, bill_id: &BillId) -> Result<()>; } diff --git a/crates/bcr-ebill-transport/src/handler/bill_chain_event_processor.rs b/crates/bcr-ebill-transport/src/handler/bill_chain_event_processor.rs index bc9070dc..3b40dd45 100644 --- a/crates/bcr-ebill-transport/src/handler/bill_chain_event_processor.rs +++ b/crates/bcr-ebill-transport/src/handler/bill_chain_event_processor.rs @@ -1,5 +1,8 @@ +use crate::handler::public_chain_helpers::{BlockData, EventContainer, resolve_event_chains}; use crate::{Error, Result}; use async_trait::async_trait; +use bcr_ebill_api::service::notification_service::transport::NotificationJsonTransportApi; +use bcr_ebill_api::util::BcrKeys; use bcr_ebill_core::ServiceTraitBounds; use bcr_ebill_core::Validate; use bcr_ebill_core::bill::BillValidateActionData; @@ -7,7 +10,7 @@ use bcr_ebill_core::bill::{BillId, BillKeys}; use bcr_ebill_core::blockchain::bill::BillOpCode; use bcr_ebill_core::blockchain::bill::block::BillIssueBlockData; use bcr_ebill_core::blockchain::bill::{BillBlock, BillBlockchain}; -use bcr_ebill_core::blockchain::{Block, Blockchain}; +use bcr_ebill_core::blockchain::{Block, Blockchain, BlockchainType}; use bcr_ebill_persistence::bill::BillChainStoreApi; use bcr_ebill_persistence::bill::BillStoreApi; use log::{debug, error, info, warn}; @@ -70,6 +73,73 @@ impl BillChainEventProcessorApi for BillChainEventProcessor { Ok(false) } } + + async fn resolve_chain( + &self, + bill_id: &BillId, + bill_keys: &BillKeys, + ) -> Result>> { + let bcr_keys = BcrKeys::from_private_key(&bill_keys.private_key)?; + resolve_event_chains( + self.transport.clone(), + &bill_id.to_string(), + BlockchainType::Bill, + &bcr_keys, + ) + .await + } + + async fn resync_chain(&self, bill_id: &BillId) -> Result<()> { + match ( + self.bill_blockchain_store.get_chain(bill_id).await, + self.bill_store.get_keys(bill_id).await, + ) { + (Ok(existing_chain), Ok(bill_keys)) => { + debug!("starting bill chain resync for {bill_id}"); + let bcr_keys = BcrKeys::from_private_key(&bill_keys.private_key)?; + if let Ok(chain_data) = resolve_event_chains( + self.transport.clone(), + &bill_id.to_string(), + BlockchainType::Bill, + &bcr_keys, + ) + .await + { + for data in chain_data.iter() { + let blocks: Vec = data + .iter() + .filter_map(|d| match d.block.clone() { + BlockData::Bill(block) => Some(block), + _ => None, + }) + .collect(); + if !data.is_empty() + && self + .add_bill_blocks(bill_id, existing_chain.clone(), blocks) + .await + .is_ok() + { + debug!("resynced bill {bill_id} with {} remote events", data.len()); + break; + } + } + debug!("finished bill chain resync for {bill_id}"); + Ok(()) + } else { + let message = format!("Could not refetch chain data from Nostr for {bill_id}"); + error!("{message}"); + Err(Error::Network(message)) + } + } + _ => { + let message = format!( + "Could not refetch chain for {bill_id} because the bill keys or chain could not be fetched" + ); + error!("{message}"); + Err(Error::Persistence(message)) + } + } + } } #[derive(Clone)] @@ -77,6 +147,7 @@ pub struct BillChainEventProcessor { bill_blockchain_store: Arc, bill_store: Arc, nostr_contact_processor: Arc, + transport: Arc, bitcoin_network: bitcoin::Network, } @@ -85,12 +156,14 @@ impl BillChainEventProcessor { bill_blockchain_store: Arc, bill_store: Arc, nostr_contact_processor: Arc, + transport: Arc, bitcoin_network: bitcoin::Network, ) -> Self { Self { bill_blockchain_store, bill_store, nostr_contact_processor, + transport, bitcoin_network, } } @@ -117,17 +190,34 @@ impl BillChainEventProcessor { })?; debug!("adding {} bill blocks for bill {bill_id}", blocks.len()); - for block in blocks { - block_added = self + for block in blocks.iter() { + block_added = match self .validate_and_save_block( bill_id, &mut chain, &bill_first_version, &bill_keys, - block, + block.clone(), is_paid, ) - .await?; + .await + { + Ok(added) => Ok(added), + Err(e) => { + // if we received a single block (normal block populate) and we are missing blocks, we try to resync + if blocks.len() == 1 && chain.get_latest_block().id + 1 < block.id { + info!( + "Received invalid block {} for bill {bill_id} - missing blocks - try to resync", + block.id + ); + self.resync_chain(bill_id).await?; + break; + } else { + println!("Error adding block for bill {bill_id}: {e}"); + Err(e) + } + } + }?; } // if the bill was changed, we invalidate the cache if block_added { @@ -376,34 +466,43 @@ impl BillChainEventProcessor { #[cfg(test)] mod tests { + use bcr_ebill_api::service::notification_service::event::{ + BillBlockEvent, Event, EventEnvelope, + }; use bcr_ebill_core::{ NodeId, blockchain::bill::block::{ - BillEndorseBlockData, BillParticipantBlockData, BillRejectBlockData, + BillAcceptBlockData, BillEndorseBlockData, BillParticipantBlockData, + BillRejectBlockData, BillRequestToAcceptBlockData, }, contact::BillIdentParticipant, util::BcrKeys, }; use mockall::predicate::{always, eq}; - use crate::handler::{ - MockNostrContactProcessorApi, - test_utils::{ - MockBillChainStore, MockBillStore, bill_id_test, empty_address, get_baseline_bill, - get_baseline_identity, get_bill_keys, get_genesis_chain, get_test_bitcredit_bill, - node_id_test, node_id_test_other, private_key_test, + use crate::{ + handler::{ + MockNostrContactProcessorApi, + test_utils::{ + MockBillChainStore, MockBillStore, bill_id_test, empty_address, get_baseline_bill, + get_baseline_identity, get_bill_keys, get_genesis_chain, get_test_bitcredit_bill, + node_id_test, node_id_test_other, private_key_test, + }, }, + test_utils::MockNotificationJsonTransport, + transport::create_public_chain_event, }; use super::*; #[tokio::test] async fn test_create_event_handler() { - let (bill_chain_store, bill_store, contact) = create_mocks(); + let (bill_chain_store, bill_store, contact, transport) = create_mocks(); BillChainEventProcessor::new( Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); } @@ -411,7 +510,7 @@ mod tests { #[tokio::test] async fn test_validate_chain_event_and_sender_invalid_on_no_keys_or_chain() { let keys = BcrKeys::new().get_nostr_keys(); - let (mut bill_chain_store, mut bill_store, contact) = create_mocks(); + let (mut bill_chain_store, mut bill_store, contact, transport) = create_mocks(); bill_store .expect_get_keys() @@ -427,6 +526,7 @@ mod tests { Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); @@ -443,7 +543,7 @@ mod tests { let node_id = node_id_test(); let npub = node_id.npub(); - let (mut bill_chain_store, mut bill_store, contact) = create_mocks(); + let (mut bill_chain_store, mut bill_store, contact, transport) = create_mocks(); bill_store .expect_get_keys() @@ -460,6 +560,7 @@ mod tests { Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); @@ -481,7 +582,7 @@ mod tests { let chain = get_genesis_chain(Some(bill.clone())); let keys = get_bill_keys(); - let (mut bill_chain_store, mut bill_store, mut contact) = create_mocks(); + let (mut bill_chain_store, mut bill_store, mut contact, transport) = create_mocks(); bill_chain_store .expect_get_chain() @@ -507,6 +608,7 @@ mod tests { Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); @@ -516,6 +618,236 @@ mod tests { .expect("Event should be handled"); } + #[tokio::test] + async fn test_adds_block_for_existing_chain_event() { + let payer = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); + let payee = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); + let mut endorsee = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); + endorsee.node_id = node_id_test_other(); + let bill = get_test_bitcredit_bill(&bill_id_test(), &payer, &payee, None, None); + let chain = get_genesis_chain(Some(bill.clone())); + let block = BillBlock::create_block_for_endorse( + bill_id_test(), + chain.get_latest_block(), + &BillEndorseBlockData { + endorsee: BillParticipantBlockData::Ident(endorsee.clone().into()), + // endorsed by payee + endorser: BillParticipantBlockData::Ident( + BillIdentParticipant::new(get_baseline_identity().identity) + .unwrap() + .into(), + ), + signatory: None, + signing_timestamp: chain.get_latest_block().timestamp + 1000, + signing_address: Some(empty_address()), + }, + &BcrKeys::from_private_key(&private_key_test()).unwrap(), + None, + &BcrKeys::from_private_key(&private_key_test()).unwrap(), + chain.get_latest_block().timestamp + 1000, + ) + .unwrap(); + + let (mut bill_chain_store, mut bill_store, mut contact, transport) = create_mocks(); + + let chain_clone = chain.clone(); + bill_store + .expect_invalidate_bill_in_cache() + .returning(|_| Ok(())); + bill_store.expect_is_paid().returning(|_| Ok(false)); + bill_store.expect_get_keys().returning(|_| { + Ok(BillKeys { + private_key: private_key_test().to_owned(), + public_key: node_id_test().pub_key(), + }) + }); + bill_chain_store + .expect_get_chain() + .with(eq(bill_id_test())) + .times(1) + .returning(move |_| Ok(chain_clone.clone())); + + bill_chain_store + .expect_add_block() + .with(eq(bill_id_test()), eq(block.clone())) + .times(1) + .returning(move |_, _| Ok(())); + + contact.expect_ensure_nostr_contact().returning(|_| ()); + + let handler = BillChainEventProcessor::new( + Arc::new(bill_chain_store), + Arc::new(bill_store), + Arc::new(contact), + Arc::new(transport), + bitcoin::Network::Testnet, + ); + + handler + .process_chain_data(&bill_id_test(), vec![block.clone()], None) + .await + .expect("Event should be handled"); + } + + #[tokio::test] + async fn test_recovers_chain_on_missing_blocks() { + let payer = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); + let payee = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); + let mut endorsee = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); + endorsee.node_id = node_id_test_other(); + let bcr_keys = BcrKeys::from_private_key(&private_key_test()).unwrap(); + let bill_id = BillId::new(bcr_keys.pub_key(), bitcoin::Network::Testnet); + let bill = get_test_bitcredit_bill(&bill_id, &payer, &payee, None, None); + let chain = get_genesis_chain(Some(bill.clone())); + + let block1 = BillBlock::create_block_for_request_to_accept( + bill_id.clone(), + chain.get_latest_block(), + &BillRequestToAcceptBlockData { + requester: BillParticipantBlockData::Ident(payer.clone().into()), + signatory: None, + signing_timestamp: chain.get_latest_block().timestamp + 1000, + signing_address: Some(empty_address()), + }, + &bcr_keys, + None, + &bcr_keys, + chain.get_latest_block().timestamp + 1000, + ) + .unwrap(); + + let block2 = BillBlock::create_block_for_accept( + bill_id.clone(), + &block1, + &BillAcceptBlockData { + accepter: payer.clone().into(), + signatory: None, + signing_timestamp: block1.timestamp + 1000, + signing_address: empty_address(), + }, + &bcr_keys, + None, + &bcr_keys, + block1.timestamp + 1000, + ) + .unwrap(); + + let event1 = generate_test_event( + &bcr_keys, + None, + None, + as_event_payload(&bill_id_test(), chain.get_latest_block()), + &bill_id, + ); + + let event2 = generate_test_event( + &bcr_keys, + Some(event1.clone()), + Some(event1.clone()), + as_event_payload(&bill_id_test(), &block1), + &bill_id, + ); + + let event3 = generate_test_event( + &bcr_keys, + Some(event2.clone()), + Some(event1.clone()), + as_event_payload(&bill_id_test(), &block2), + &bill_id, + ); + + // mock the nostr chain + let nostr_chain = vec![event1.clone(), event2.clone(), event3.clone()]; + + let (mut bill_chain_store, mut bill_store, mut contact, mut transport) = create_mocks(); + + let chain_clone = chain.clone(); + bill_store + .expect_invalidate_bill_in_cache() + .returning(|_| Ok(())); + bill_store.expect_is_paid().returning(|_| Ok(false)); + bill_store.expect_get_keys().returning(move |_| { + Ok(BillKeys { + private_key: bcr_keys.get_private_key().to_owned(), + public_key: bcr_keys.pub_key(), + }) + }); + + let expected_bill_id = bill_id.clone(); + // on chain recovery we ask for the existing chain two times + bill_chain_store + .expect_get_chain() + .with(eq(expected_bill_id)) + .times(2) + .returning(move |_| Ok(chain_clone.clone())); + + transport + .expect_resolve_public_chain() + .with(eq(bill_id.to_string()), eq(BlockchainType::Bill)) + .returning(move |_, _| Ok(nostr_chain.clone())) + .once(); + + // recovery adds the missing block + bill_chain_store + .expect_add_block() + .with(eq(bill_id.clone()), eq(block1.clone())) + .times(1) + .returning(move |_, _| Ok(())); + + // and then the received block + bill_chain_store + .expect_add_block() + .with(eq(bill_id.clone()), eq(block2.clone())) + .times(1) + .returning(move |_, _| Ok(())); + + contact.expect_ensure_nostr_contact().returning(|_| ()); + + let handler = BillChainEventProcessor::new( + Arc::new(bill_chain_store), + Arc::new(bill_store), + Arc::new(contact), + Arc::new(transport), + bitcoin::Network::Testnet, + ); + + handler + .process_chain_data(&bill_id, vec![block2.clone()], None) + .await + .expect("Event should be handled"); + } + + fn as_event_payload(id: &BillId, block: &BillBlock) -> EventEnvelope { + Event::new_bill(BillBlockEvent { + bill_id: id.clone(), + block_height: block.id as usize, + block: block.clone(), + }) + .try_into() + .expect("could not create envelope") + } + + fn generate_test_event( + keys: &BcrKeys, + previous: Option, + root: Option, + data: EventEnvelope, + bill_id: &BillId, + ) -> nostr::Event { + create_public_chain_event( + &bill_id.to_string(), + data, + 1000, + BlockchainType::Bill, + keys.clone(), + previous, + root, + ) + .expect("could not create chain event") + .sign_with_keys(&keys.get_nostr_keys()) + .expect("could not sign event") + } + #[tokio::test] async fn test_fails_to_create_new_chain_for_new_chain_event_if_block_validation_fails() { let payer = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); @@ -544,7 +876,7 @@ mod tests { .unwrap(); assert!(chain.try_add_block(block)); - let (mut bill_chain_store, mut bill_store, contact) = create_mocks(); + let (mut bill_chain_store, mut bill_store, contact, transport) = create_mocks(); bill_chain_store .expect_get_chain() @@ -567,6 +899,7 @@ mod tests { Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); @@ -588,7 +921,7 @@ mod tests { let chain = get_genesis_chain(Some(bill.clone())); let keys = get_bill_keys(); - let (mut bill_chain_store, mut bill_store, contact) = create_mocks(); + let (mut bill_chain_store, mut bill_store, contact, transport) = create_mocks(); bill_chain_store .expect_get_chain() @@ -609,6 +942,7 @@ mod tests { Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); @@ -618,76 +952,6 @@ mod tests { assert!(result.is_err()); } - #[tokio::test] - async fn test_adds_block_for_existing_chain_event() { - let payer = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); - let payee = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); - let mut endorsee = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); - endorsee.node_id = node_id_test_other(); - let bill = get_test_bitcredit_bill(&bill_id_test(), &payer, &payee, None, None); - let chain = get_genesis_chain(Some(bill.clone())); - let block = BillBlock::create_block_for_endorse( - bill_id_test(), - chain.get_latest_block(), - &BillEndorseBlockData { - endorsee: BillParticipantBlockData::Ident(endorsee.clone().into()), - // endorsed by payee - endorser: BillParticipantBlockData::Ident( - BillIdentParticipant::new(get_baseline_identity().identity) - .unwrap() - .into(), - ), - signatory: None, - signing_timestamp: chain.get_latest_block().timestamp + 1000, - signing_address: Some(empty_address()), - }, - &BcrKeys::from_private_key(&private_key_test()).unwrap(), - None, - &BcrKeys::from_private_key(&private_key_test()).unwrap(), - chain.get_latest_block().timestamp + 1000, - ) - .unwrap(); - - let (mut bill_chain_store, mut bill_store, mut contact) = create_mocks(); - - let chain_clone = chain.clone(); - bill_store - .expect_invalidate_bill_in_cache() - .returning(|_| Ok(())); - bill_store.expect_is_paid().returning(|_| Ok(false)); - bill_store.expect_get_keys().returning(|_| { - Ok(BillKeys { - private_key: private_key_test().to_owned(), - public_key: node_id_test().pub_key(), - }) - }); - bill_chain_store - .expect_get_chain() - .with(eq(bill_id_test())) - .times(1) - .returning(move |_| Ok(chain_clone.clone())); - - bill_chain_store - .expect_add_block() - .with(eq(bill_id_test()), eq(block.clone())) - .times(1) - .returning(move |_, _| Ok(())); - - contact.expect_ensure_nostr_contact().returning(|_| ()); - - let handler = BillChainEventProcessor::new( - Arc::new(bill_chain_store), - Arc::new(bill_store), - Arc::new(contact), - bitcoin::Network::Testnet, - ); - - handler - .process_chain_data(&bill_id_test(), vec![block.clone()], None) - .await - .expect("Event should be handled"); - } - #[tokio::test] async fn test_fails_to_add_block_for_invalid_bill_action() { let payer = BillIdentParticipant::new(get_baseline_identity().identity).unwrap(); @@ -712,7 +976,7 @@ mod tests { ) .unwrap(); - let (mut bill_chain_store, mut bill_store, contact) = create_mocks(); + let (mut bill_chain_store, mut bill_store, contact, transport) = create_mocks(); let chain_clone = chain.clone(); bill_store.expect_get_keys().returning(|_| { @@ -736,13 +1000,13 @@ mod tests { Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); let result = handler .process_chain_data(&bill_id_test(), vec![block.clone()], None) .await; - assert!(result.is_err()); } @@ -775,7 +1039,7 @@ mod tests { ) .unwrap(); - let (mut bill_chain_store, mut bill_store, contact) = create_mocks(); + let (mut bill_chain_store, mut bill_store, contact, transport) = create_mocks(); let chain_clone = chain.clone(); bill_store.expect_get_keys().returning(|_| { @@ -799,13 +1063,13 @@ mod tests { Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); let result = handler .process_chain_data(&bill_id_test(), vec![block.clone()], None) .await; - assert!(result.is_err()); } @@ -839,7 +1103,7 @@ mod tests { ) .unwrap(); - let (mut bill_chain_store, bill_store, contact) = create_mocks(); + let (mut bill_chain_store, bill_store, contact, transport) = create_mocks(); bill_chain_store .expect_get_chain() @@ -853,6 +1117,7 @@ mod tests { Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); @@ -865,11 +1130,12 @@ mod tests { #[tokio::test] async fn test_fails_to_add_block_for_bill_id_from_different_network() { - let (bill_chain_store, bill_store, contact) = create_mocks(); + let (bill_chain_store, bill_store, contact, transport) = create_mocks(); let handler = BillChainEventProcessor::new( Arc::new(bill_chain_store), Arc::new(bill_store), Arc::new(contact), + Arc::new(transport), bitcoin::Network::Testnet, ); let mainnet_bill_id = BillId::new(BcrKeys::new().pub_key(), bitcoin::Network::Bitcoin); @@ -886,11 +1152,13 @@ mod tests { MockBillChainStore, MockBillStore, MockNostrContactProcessorApi, + MockNotificationJsonTransport, ) { ( MockBillChainStore::new(), MockBillStore::new(), MockNostrContactProcessorApi::new(), + MockNotificationJsonTransport::new(), ) } } 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 04947b50..717374fe 100644 --- a/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs +++ b/crates/bcr-ebill-transport/src/handler/bill_invite_handler.rs @@ -1,15 +1,11 @@ -use std::{cmp::Reverse, collections::HashMap, str::FromStr, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; use async_trait::async_trait; -use bcr_ebill_api::service::notification_service::{ - event::{ChainInvite, ChainKeys}, - transport::NotificationJsonTransportApi, -}; +use bcr_ebill_api::service::notification_service::event::ChainInvite; use bcr_ebill_core::{ NodeId, ServiceTraitBounds, bill::{BillId, BillKeys}, blockchain::BlockchainType, - util::BcrKeys, }; use bcr_ebill_persistence::{NostrChainEventStoreApi, nostr::NostrChainEvent}; use log::{debug, error, warn}; @@ -21,13 +17,10 @@ use crate::{ use bcr_ebill_api::service::notification_service::event::Event; use bcr_ebill_api::service::notification_service::{Result, event::EventEnvelope}; -use super::{ - BillChainEventProcessorApi, NotificationHandlerApi, public_chain_helpers::collect_event_chains, -}; +use super::{BillChainEventProcessorApi, NotificationHandlerApi}; #[derive(Clone)] pub struct BillInviteEventHandler { - transport: Arc, processor: Arc, chain_event_store: Arc, } @@ -47,21 +40,14 @@ impl NotificationHandlerApi for BillInviteEventHandler { ) -> Result<()> { debug!("incoming bill chain invite for {node_id}"); if let Ok(decoded) = Event::::try_from(event.clone()) { - let events = self - .transport - .resolve_public_chain(&decoded.data.chain_id, decoded.data.chain_type) - .await?; + let keys = BillKeys { + private_key: decoded.data.keys.private_key.to_owned(), + public_key: decoded.data.keys.public_key.to_owned(), + }; + let chain_id = BillId::from_str(&decoded.data.chain_id)?; let mut inserted_chain: Vec = Vec::new(); - if let Ok(chain_data) = self - .resolve_chain_data( - &decoded.data.keys, - &decoded.data.chain_id, - decoded.data.chain_type, - &events, - ) - .await - { + if let Ok(chain_data) = self.processor.resolve_chain(&chain_id, &keys).await { // We try to add shorter and shorter chains until we have a success for data in chain_data.iter() { let blocks = data @@ -115,33 +101,15 @@ impl ServiceTraitBounds for BillInviteEventHandler {} impl BillInviteEventHandler { pub fn new( - transport: Arc, processor: Arc, chain_event_store: Arc, ) -> Self { Self { - transport, processor, chain_event_store, } } - /// Parses chain keys, resolves all Nostr events and builds Nostr chains from it. - /// Then decrypts the payloads and parses the block contents. Returns all found chains - /// in descending order by chain length. - async fn resolve_chain_data( - &self, - keys: &ChainKeys, - chain_id: &str, - chain_type: BlockchainType, - events: &[nostr_sdk::Event], - ) -> Result>> { - let keys = BcrKeys::from_private_key(&keys.private_key)?; - let mut chains = collect_event_chains(events, chain_id, chain_type, &keys); - chains.sort_by_key(|v| Reverse(v.len())); - Ok(chains) - } - async fn store_events( &self, chain_id: &str, @@ -187,14 +155,13 @@ mod tests { node_id_test, private_key_test, }, }, - test_utils::MockNotificationJsonTransport, transport::create_public_chain_event, }; use super::*; use bcr_ebill_api::service::notification_service::event::BillBlockEvent; use bcr_ebill_core::{blockchain::Blockchain, util::crypto::BcrKeys}; - use mockall::predicate::eq; + use mockall::predicate::{always, eq}; #[test] fn test_single_block() { @@ -243,16 +210,22 @@ mod tests { #[tokio::test] async fn test_process_single_event_chain_invite() { - let (mut transport, mut processor, mut chain_event_store) = get_mocks(); + let (mut processor, mut chain_event_store) = get_mocks(); let node_id = node_id_test(); - let (_, chain) = generate_test_chain(1, false); + let (bcr_keys, chain) = generate_test_chain(1, false); + let chains = collect_event_chains( + &chain, + &bill_id_test().to_string(), + BlockchainType::Bill, + &bcr_keys, + ); // get events from nostr - transport - .expect_resolve_public_chain() - .with(eq(bill_id_test().to_string()), eq(BlockchainType::Bill)) - .returning(move |_, _| Ok(chain.clone())); + processor + .expect_resolve_chain() + .with(eq(bill_id_test()), always()) + .returning(move |_, _| Ok(chains.clone())); // process blocks processor @@ -279,11 +252,7 @@ mod tests { .try_into() .expect("failed to create envelope"); - let handler = BillInviteEventHandler::new( - Arc::new(transport), - Arc::new(processor), - Arc::new(chain_event_store), - ); + let handler = BillInviteEventHandler::new(Arc::new(processor), Arc::new(chain_event_store)); handler .handle_event(invite, &node_id, Some(Box::new(event.clone()))) .await @@ -292,16 +261,22 @@ mod tests { #[tokio::test] async fn test_process_single_chain_invite() { - let (mut transport, mut processor, mut chain_event_store) = get_mocks(); + let (mut processor, mut chain_event_store) = get_mocks(); let node_id = node_id_test(); - let (_, chain) = generate_test_chain(3, false); + let (bcr_keys, chain) = generate_test_chain(3, false); + let chains = collect_event_chains( + &chain, + &bill_id_test().to_string(), + BlockchainType::Bill, + &bcr_keys, + ); // get events from nostr - transport - .expect_resolve_public_chain() - .with(eq(bill_id_test().to_string()), eq(BlockchainType::Bill)) - .returning(move |_, _| Ok(chain.clone())); + processor + .expect_resolve_chain() + .with(eq(bill_id_test()), always()) + .returning(move |_, _| Ok(chains.clone())); // process blocks processor @@ -328,11 +303,7 @@ mod tests { .try_into() .expect("failed to create envelope"); - let handler = BillInviteEventHandler::new( - Arc::new(transport), - Arc::new(processor), - Arc::new(chain_event_store), - ); + let handler = BillInviteEventHandler::new(Arc::new(processor), Arc::new(chain_event_store)); handler .handle_event(invite, &node_id, Some(Box::new(event.clone()))) .await @@ -341,16 +312,22 @@ mod tests { #[tokio::test] async fn test_process_multiple_chains_invite() { - let (mut transport, mut processor, mut chain_event_store) = get_mocks(); + let (mut processor, mut chain_event_store) = get_mocks(); let node_id = node_id_test(); - let (_, chain) = generate_test_chain(3, true); + let (bcr_keys, chain) = generate_test_chain(3, true); + let chains = collect_event_chains( + &chain, + &bill_id_test().to_string(), + BlockchainType::Bill, + &bcr_keys, + ); // get events from nostr - transport - .expect_resolve_public_chain() - .with(eq(bill_id_test().to_string()), eq(BlockchainType::Bill)) - .returning(move |_, _| Ok(chain.clone())); + processor + .expect_resolve_chain() + .with(eq(bill_id_test()), always()) + .returning(move |_, _| Ok(chains.clone())); // process blocks processor @@ -385,24 +362,15 @@ mod tests { .try_into() .expect("failed to create envelope"); - let handler = BillInviteEventHandler::new( - Arc::new(transport), - Arc::new(processor), - Arc::new(chain_event_store), - ); + let handler = BillInviteEventHandler::new(Arc::new(processor), Arc::new(chain_event_store)); handler .handle_event(invite, &node_id, Some(Box::new(event.clone()))) .await .expect("failed to process chain invite event"); } - fn get_mocks() -> ( - MockNotificationJsonTransport, - MockBillChainEventProcessorApi, - MockNostrChainEventStore, - ) { + fn get_mocks() -> (MockBillChainEventProcessorApi, MockNostrChainEventStore) { ( - MockNotificationJsonTransport::new(), MockBillChainEventProcessorApi::new(), MockNostrChainEventStore::new(), ) diff --git a/crates/bcr-ebill-transport/src/handler/mod.rs b/crates/bcr-ebill-transport/src/handler/mod.rs index e98a4647..34abb25c 100644 --- a/crates/bcr-ebill-transport/src/handler/mod.rs +++ b/crates/bcr-ebill-transport/src/handler/mod.rs @@ -1,4 +1,4 @@ -use crate::Result; +use crate::{Result, handler::public_chain_helpers::EventContainer}; use async_trait::async_trait; use bcr_ebill_api::{service::notification_service::event::EventEnvelope, util::BcrKeys}; use bcr_ebill_core::{ @@ -85,6 +85,18 @@ pub trait BillChainEventProcessorApi: ServiceTraitBounds { bill_id: &BillId, sender: nostr::PublicKey, ) -> Result; + + /// Resolves the Bill chain blocks from Nostr for the given bill id. + async fn resolve_chain( + &self, + bill_id: &BillId, + bill_keys: &BillKeys, + ) -> Result>>; + + /// Tries to resync the chain for the given bill id. This will try to find the bill keys and + /// then try to find the chain data for the given bill id. Will add all potentially missing + /// blocks to the chain. + async fn resync_chain(&self, bill_id: &BillId) -> Result<()>; } #[cfg(test)] diff --git a/crates/bcr-ebill-transport/src/lib.rs b/crates/bcr-ebill-transport/src/lib.rs index 6fbb6ac8..ad476ad2 100644 --- a/crates/bcr-ebill-transport/src/lib.rs +++ b/crates/bcr-ebill-transport/src/lib.rs @@ -7,18 +7,14 @@ use bcr_ebill_api::{ service::{ contact_service::ContactServiceApi, notification_service::{ - Error, NostrConfig, NotificationServiceApi, Result, event::EventType, + NostrConfig, NotificationServiceApi, event::EventType, transport::NotificationJsonTransportApi, }, }, util::BcrKeys, }; use bcr_ebill_core::NodeId; -use bcr_ebill_persistence::{ - NostrChainEventStoreApi, NotificationStoreApi, company::CompanyStoreApi, - identity::IdentityStoreApi, nostr::NostrQueuedMessageStoreApi, - notification::EmailNotificationStoreApi, -}; +use bcr_ebill_persistence::{company::CompanyStoreApi, identity::IdentityStoreApi}; use chain_keys::ChainKeyServiceApi; use handler::{ BillActionEventHandler, BillChainEventHandler, BillChainEventProcessor, BillInviteEventHandler, @@ -38,6 +34,7 @@ pub mod test_utils; pub mod transport; pub use async_broadcast::Receiver; +pub use bcr_ebill_api::service::notification_service::{Error, Result}; pub use handler::RestoreAccountService; pub use nostr::{NostrClient, NostrConsumer}; use notification_service::NotificationService; @@ -46,7 +43,7 @@ pub use transport::bcr_nostr_tag; use crate::handler::DirectMessageEventProcessor; -/// Creates a new nostr client configured with the current identity user. +/// Creates new nostr clients configured with the current identity user and all local companies. pub async fn create_nostr_clients( config: &Config, identity_store: Arc, @@ -106,26 +103,42 @@ pub async fn create_nostr_clients( /// Creates a new notification service that will send events via the given Nostr json transport. pub async fn create_notification_service( clients: Vec>, - notification_store: Arc, - email_notification_store: Arc, + db_context: DbContext, contact_service: Arc, - queued_message_store: Arc, - chain_event_store: Arc, email_client: Arc, nostr_relays: Vec, ) -> Result> { + let transport = match clients.iter().find(|c| c.is_primary()) { + Some(client) => client.clone(), + None => panic!("Cant create Nostr consumer as there is no nostr client available"), + }; + + let nostr_contact_processor = Arc::new(NostrContactProcessor::new( + transport.clone(), + db_context.nostr_contact_store.clone(), + get_config().bitcoin_network(), + )); + let bill_processor = Arc::new(BillChainEventProcessor::new( + db_context.bill_blockchain_store.clone(), + db_context.bill_store.clone(), + nostr_contact_processor.clone(), + transport.clone(), + get_config().bitcoin_network(), + )); + #[allow(clippy::arc_with_non_send_sync)] Ok(Arc::new(NotificationService::new( clients .iter() .map(|c| c.clone() as Arc) .collect(), - notification_store, - email_notification_store, + db_context.notification_store.clone(), + db_context.email_notification_store.clone(), contact_service, - queued_message_store, - chain_event_store, + db_context.queued_message_store.clone(), + db_context.nostr_chain_event_store.clone(), email_client, + bill_processor, nostr_relays, ))) } @@ -156,11 +169,11 @@ pub async fn create_nostr_consumer( db_context.bill_blockchain_store.clone(), db_context.bill_store.clone(), nostr_contact_processor.clone(), + transport.clone(), get_config().bitcoin_network(), )); let bill_invite_handler = Arc::new(BillInviteEventHandler::new( - transport.clone(), bill_processor.clone(), db_context.nostr_chain_event_store.clone(), )); @@ -264,11 +277,11 @@ pub async fn create_restore_account_service( db_context.bill_blockchain_store.clone(), db_context.bill_store.clone(), nostr_contact_processor.clone(), + nostr_client.clone(), get_config().bitcoin_network(), )); let bill_invite_handler = Arc::new(BillInviteEventHandler::new( - nostr_client.clone(), bill_processor.clone(), db_context.nostr_chain_event_store.clone(), )); diff --git a/crates/bcr-ebill-transport/src/notification_service.rs b/crates/bcr-ebill-transport/src/notification_service.rs index ec069a9d..5393a466 100644 --- a/crates/bcr-ebill-transport/src/notification_service.rs +++ b/crates/bcr-ebill-transport/src/notification_service.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; +use crate::handler::BillChainEventProcessorApi; use crate::nostr::NostrClient; use async_trait::async_trait; use bcr_ebill_api::external::email::EmailClientApi; @@ -50,6 +51,7 @@ pub struct NotificationService { queued_message_store: Arc, chain_event_store: Arc, email_client: Arc, + bill_chain_event_processor: Arc, nostr_relays: Vec, } @@ -67,6 +69,7 @@ impl NotificationService { queued_message_store: Arc, chain_event_store: Arc, email_client: Arc, + bill_chain_event_processor: Arc, nostr_relays: Vec, ) -> Self { let transports: Mutex>> = Mutex::new( @@ -83,6 +86,7 @@ impl NotificationService { queued_message_store, chain_event_store, email_client, + bill_chain_event_processor, nostr_relays, } } @@ -1043,6 +1047,13 @@ impl NotificationServiceApi for NotificationService { Err(e) => Err(Error::Persistence(e.to_string())), } } + + async fn resync_bill_chain(&self, bill_id: &BillId) -> Result<()> { + self.bill_chain_event_processor + .resync_chain(bill_id) + .await?; + Ok(()) + } } #[cfg(test)] @@ -1063,6 +1074,7 @@ mod tests { use reqwest::Url; use std::sync::Arc; + use crate::handler::MockBillChainEventProcessorApi; use crate::test_utils::{ MockContactService, MockEmailClient, MockEmailNotificationStore, MockNostrChainEventStore, MockNostrQueuedMessageStore, MockNotificationJsonTransport, MockNotificationStore, @@ -1194,6 +1206,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -1282,6 +1295,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -1338,6 +1352,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -1404,6 +1419,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -1528,6 +1544,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(event_store), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -1606,6 +1623,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -1677,6 +1695,7 @@ mod tests { Arc::new(queue_mock), Arc::new(mock_event_store), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -1791,6 +1810,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(mock_event_store), Arc::new(mock_email_client), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2263,6 +2283,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2316,6 +2337,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2362,6 +2384,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2429,6 +2452,7 @@ mod tests { Arc::new(mock_queue), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2473,6 +2497,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(mock_email_client), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); let event = Event::new( @@ -2549,6 +2574,7 @@ mod tests { Arc::new(mock_queue), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2657,6 +2683,7 @@ mod tests { Arc::new(mock_queue), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2704,6 +2731,7 @@ mod tests { Arc::new(mock_queue), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2776,6 +2804,7 @@ mod tests { Arc::new(mock_queue), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2848,6 +2877,7 @@ mod tests { Arc::new(mock_queue), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2876,6 +2906,7 @@ mod tests { Arc::new(mock_queue), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2910,6 +2941,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(mock_email_client), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2944,6 +2976,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); @@ -2977,6 +3010,7 @@ mod tests { Arc::new(MockNostrQueuedMessageStore::new()), Arc::new(MockNostrChainEventStore::new()), Arc::new(MockEmailClient::new()), + Arc::new(MockBillChainEventProcessorApi::new()), vec!["ws://test.relay".into()], ); let result = service diff --git a/crates/bcr-ebill-transport/src/test_utils.rs b/crates/bcr-ebill-transport/src/test_utils.rs index 9927b045..08316b0d 100644 --- a/crates/bcr-ebill-transport/src/test_utils.rs +++ b/crates/bcr-ebill-transport/src/test_utils.rs @@ -404,6 +404,8 @@ mockall::mock! { async fn resolve_contact(&self, node_id: &NodeId) -> Result>; async fn register_email_notifications(&self, relay_url: &str, email: &str, node_id: &NodeId, caller_keys: &BcrKeys) -> Result<()>; async fn get_email_notifications_preferences_link(&self, node_id: &NodeId) -> Result; + async fn resync_bill_chain(&self, bill_id: &BillId) -> Result<()>; + } } diff --git a/crates/bcr-ebill-wasm/index.html b/crates/bcr-ebill-wasm/index.html index cf18da49..ef7395bf 100644 --- a/crates/bcr-ebill-wasm/index.html +++ b/crates/bcr-ebill-wasm/index.html @@ -115,6 +115,7 @@

Bill Testing

+

Bill Performance

@@ -127,6 +128,7 @@

Bill Performance

+
Execution Time:
Results:
diff --git a/crates/bcr-ebill-wasm/main.js b/crates/bcr-ebill-wasm/main.js index 6fab62b0..ea368ce2 100644 --- a/crates/bcr-ebill-wasm/main.js +++ b/crates/bcr-ebill-wasm/main.js @@ -52,6 +52,7 @@ document.getElementById("bill_test_self_drafted").addEventListener("click", trig document.getElementById("bill_test_promissory").addEventListener("click", triggerBill.bind(null, 0, false)); document.getElementById("bill_test_promissory_blank").addEventListener("click", triggerBill.bind(null, 0, true)); document.getElementById("clear_bill_cache").addEventListener("click", clearBillCache); +document.getElementById("sync_bill_chain").addEventListener("click", syncBillChain); // companies document.getElementById("company_create").addEventListener("click", createCompany); @@ -636,6 +637,15 @@ async function clearBillCache() { await measured(); } +async function syncBillChain() { + let bill_id = document.getElementById("bill_id").value; + console.log("syncBillChain", bill_id); + let measured = measure(async () => { + return await billApi.sync_bill_chain({ bill_id: bill_id }); + }); + await measured(); +} + function measure(promiseFunction) { return async function (...args) { const startTime = performance.now(); diff --git a/crates/bcr-ebill-wasm/src/api/bill.rs b/crates/bcr-ebill-wasm/src/api/bill.rs index 82f2bd94..b7a9cb9e 100644 --- a/crates/bcr-ebill-wasm/src/api/bill.rs +++ b/crates/bcr-ebill-wasm/src/api/bill.rs @@ -35,7 +35,7 @@ use crate::{ PastEndorseesResponse, PastPaymentsResponse, RejectActionBillPayload, RequestRecourseForAcceptancePayload, RequestRecourseForPaymentPayload, RequestToAcceptBitcreditBillPayload, RequestToMintBitcreditBillPayload, - RequestToPayBitcreditBillPayload, + RequestToPayBitcreditBillPayload, ResyncBillPayload, }, mint::MintRequestStateResponse, }, @@ -846,6 +846,20 @@ impl Bill { get_ctx().bill_service.clear_bill_cache().await?; Ok(()) } + + /// Given a bill id, resync the chain via block transport + #[wasm_bindgen] + pub async fn sync_bill_chain( + &self, + #[wasm_bindgen(unchecked_param_type = "ResyncBillPayload")] payload: JsValue, + ) -> Result<()> { + let payload: ResyncBillPayload = serde_wasm_bindgen::from_value(payload)?; + get_ctx() + .notification_service + .resync_bill_chain(&payload.bill_id) + .await?; + Ok(()) + } } async fn request_recourse( diff --git a/crates/bcr-ebill-wasm/src/context.rs b/crates/bcr-ebill-wasm/src/context.rs index 759a1582..e9f93a0b 100644 --- a/crates/bcr-ebill-wasm/src/context.rs +++ b/crates/bcr-ebill-wasm/src/context.rs @@ -59,11 +59,8 @@ impl Context { create_nostr_clients(&cfg, db.identity_store.clone(), db.company_store.clone()).await?; let notification_service = create_notification_service( nostr_clients.clone(), - db.notification_store.clone(), - db.email_notification_store.clone(), + db.clone(), contact_service.clone(), - db.queued_message_store.clone(), - db.nostr_chain_event_store.clone(), email_client, cfg.nostr_config.relays.to_owned(), ) diff --git a/crates/bcr-ebill-wasm/src/data/bill.rs b/crates/bcr-ebill-wasm/src/data/bill.rs index 196e08a0..64378dd1 100644 --- a/crates/bcr-ebill-wasm/src/data/bill.rs +++ b/crates/bcr-ebill-wasm/src/data/bill.rs @@ -133,6 +133,13 @@ pub struct BillCombinedBitcoinKeyWeb { pub private_descriptor: String, } +#[derive(Tsify, Debug, Clone, Deserialize)] +#[tsify(from_wasm_abi)] +pub struct ResyncBillPayload { + #[tsify(type = "string")] + pub bill_id: BillId, +} + impl From for BillCombinedBitcoinKeyWeb { fn from(val: BillCombinedBitcoinKey) -> Self { BillCombinedBitcoinKeyWeb {