Skip to content

Commit 905c61f

Browse files
authored
Single nostr client (#755)
* refactor: update NostrClient to support multiple identities * fix: address critical issues from Task 1 code review - Replace panic() in get_sender_node_id() and get_sender_keys() with backward-compatible implementations returning first identity - Store BcrKeys HashMap alongside signers to enable get_sender_keys() implementation - Make signers field private (was pub(crate)) - Remove async from add_identity() (no async work needed) - Revert signer selection in send_nip04_message() to use get_default_signer() - Revert NostrConsumer changes to use get_node_id() instead of accessing signers directly - Add backward-compatible helper methods: get_node_id(), get_default_signer(), get_nostr_keys() - Update direct_message_event_processor to use public methods instead of private field access All tests pass (93 tests in bcr-ebill-transport). * refactor: add sender_node_id parameter to TransportClientApi * fix: address NIP-17 and key validation issues from Task 2 review * refactor: update NostrConsumer to use single shared client - Change NostrConsumer from HashMap of clients to single Arc<NostrClient> - Implement determine_recipient() to route events to correct identity - Create single subscription with all local pubkeys - Use earliest offset across all identities for subscription - Update create_nostr_consumer() to accept single client parameter - Add test for multi-identity consumer with single client - Add temporary workaround in WASM context (to be fixed in Task 10) Part of Task 3: Nostr single relay pool refactoring All 95 tests passing * feat: enable NIP-17 for multi-identity clients via manual gift wrap - Implement manual NIP-17 gift wrap construction using nostr::nips::nip59 - Use EventBuilder to create rumor, seal, and gift wrap with explicit signer - Remove multi-identity limitation check that blocked NIP-17 - Update test to expect success instead of failure for NIP-17 multi-identity - Improves privacy by enabling proper NIP-17 encryption for all identities All 95 tests passing * refactor: update NostrTransportService to use single shared client Replace HashMap<NodeId, Arc<dyn TransportClientApi>> with single Arc<dyn TransportClientApi> in NostrTransportService and update all callers: - Update NostrTransportService struct to hold single nostr_client field - Convert get_node_transport() and get_first_transport() to return Arc directly (no longer async) - Simplify add_company_client() to no-op (handled by multi-identity client) - Update connect() method to use single client - Remove all .await calls from get_node_transport()/get_first_transport() - Update all service callers: block_transport, contact_transport, notification_transport, transport_service - Fix test expectations to match new single-client architecture - Clean up unused imports (error, NostrClient, NostrConfig, tokio, get_config) All 95 tests passing. * Finalize * Cleanup old methods * Multi signer for dm handler * Fix sender NodeId on public chain events * Allow dynamically adding identities * Update subscriptions on identity add * Cleanup BcrKeys * Fix publish relay list and metadata * Review fixes * Use event ref * Review fixes
1 parent 81620b8 commit 905c61f

File tree

16 files changed

+887
-723
lines changed

16 files changed

+887
-723
lines changed

crates/bcr-ebill-api/src/service/transport_service/transport_client.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@ impl ServiceTraitBounds for MockTransportClientApi {}
2020
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
2121
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
2222
pub trait TransportClientApi: ServiceTraitBounds {
23-
/// Returns the senders node id for this instance.
24-
fn get_sender_node_id(&self) -> NodeId;
25-
/// Returns the senders keys for this instance.
26-
fn get_sender_keys(&self) -> BcrKeys;
2723
/// Sends a private json event to the given recipient.
2824
async fn send_private_event(
2925
&self,
26+
sender_node_id: &NodeId,
3027
recipient: &BillParticipant,
3128
event: EventEnvelope,
3229
) -> Result<()>;
@@ -35,6 +32,7 @@ pub trait TransportClientApi: ServiceTraitBounds {
3532
/// event. This will return the sent event so we can add it to the local store.
3633
async fn send_public_chain_event(
3734
&self,
35+
sender_node_id: &NodeId,
3836
id: &str,
3937
blockchain: BlockchainType,
4038
block_time: Timestamp,
@@ -56,13 +54,24 @@ pub trait TransportClientApi: ServiceTraitBounds {
5654
/// Resolves all private messages matching the filter
5755
async fn resolve_private_events(&self, filter: Filter) -> Result<Vec<Event>>;
5856

59-
/// Publishes the metadata (contact info) via the Nostr client
60-
async fn publish_metadata(&self, data: &nostr::nips::nip01::Metadata) -> Result<()>;
57+
/// Publishes the metadata (contact info) via the Nostr client for the specified identity
58+
async fn publish_metadata(
59+
&self,
60+
node_id: &NodeId,
61+
data: &nostr::nips::nip01::Metadata,
62+
) -> Result<()>;
6163

62-
/// Publishes the relay list via the Nostr client
63-
async fn publish_relay_list(&self, relays: Vec<RelayUrl>) -> Result<()>;
64+
/// Publishes the relay list via the Nostr client for the specified identity
65+
async fn publish_relay_list(&self, node_id: &NodeId, relays: Vec<RelayUrl>) -> Result<()>;
6466

6567
/// Opens the connection(s) to the underlying network. This can be called multiple times and
6668
/// will only open the connection once.
6769
async fn connect(&self) -> Result<()>;
70+
71+
/// Adds a new identity (company keys) to the multi-identity client
72+
/// This will also add a subscription for direct messages to this identity
73+
async fn add_identity(&self, node_id: NodeId, keys: BcrKeys) -> Result<()>;
74+
75+
/// Check if this client has a local signer for the given node_id
76+
fn has_local_signer(&self, node_id: &NodeId) -> bool;
6877
}

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

Lines changed: 125 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use bcr_ebill_core::application::company::Company;
1212
use bcr_ebill_core::protocol::blockchain::BlockchainType;
1313
use bcr_ebill_core::protocol::crypto::BcrKeys;
1414
use bcr_ebill_core::protocol::event::{BillChainEvent, CompanyChainEvent, IdentityChainEvent};
15-
use log::{debug, error};
15+
use log::debug;
1616

1717
use bcr_ebill_api::service::transport_service::Result;
1818

@@ -51,50 +51,42 @@ impl BlockTransportServiceApi for BlockTransportService {
5151
"sending identity chain events for node: {}",
5252
events.identity_id
5353
);
54-
if let Some(node) = self
55-
.nostr_transport
56-
.get_node_transport(&events.sender())
57-
.await
58-
{
59-
if let Some(event) = events.generate_blockchain_message() {
60-
let (previous_event, root_event) = self
61-
.nostr_transport
62-
.find_root_and_previous_event(
63-
&event.data.block.previous_hash,
64-
&event.data.node_id.to_string(),
65-
BlockchainType::Identity,
66-
)
67-
.await?;
68-
// send the event
69-
let nostr_event = node
70-
.send_public_chain_event(
71-
&event.data.node_id.to_string(),
72-
BlockchainType::Identity,
73-
event.data.block.timestamp,
74-
events.keys.clone(),
75-
event.clone().try_into()?,
76-
previous_event.clone().map(|e| e.payload),
77-
root_event.clone().map(|e| e.payload),
78-
)
79-
.await?;
80-
// and store the event locally
81-
self.nostr_transport
82-
.add_chain_event(
83-
&nostr_event,
84-
&root_event,
85-
&previous_event,
86-
&event.data.node_id.to_string(),
87-
BlockchainType::Identity,
88-
event.data.block.id.inner() as usize,
89-
&event.data.block.hash,
90-
)
91-
.await?;
92-
}
93-
} else {
94-
error!(
95-
"could not find transport instance for sender node {}",
96-
events.sender()
97-
);
54+
let node = self.nostr_transport.get_node_transport(&events.sender());
55+
56+
if let Some(event) = events.generate_blockchain_message() {
57+
let (previous_event, root_event) = self
58+
.nostr_transport
59+
.find_root_and_previous_event(
60+
&event.data.block.previous_hash,
61+
&event.data.node_id.to_string(),
62+
BlockchainType::Identity,
63+
)
64+
.await?;
65+
// send the event
66+
let nostr_event = node
67+
.send_public_chain_event(
68+
&events.sender(),
69+
&event.data.node_id.to_string(),
70+
BlockchainType::Identity,
71+
event.data.block.timestamp,
72+
events.keys.clone(),
73+
event.clone().try_into()?,
74+
previous_event.clone().map(|e| e.payload),
75+
root_event.clone().map(|e| e.payload),
76+
)
77+
.await?;
78+
// and store the event locally
79+
self.nostr_transport
80+
.add_chain_event(
81+
&nostr_event,
82+
&root_event,
83+
&previous_event,
84+
&event.data.node_id.to_string(),
85+
BlockchainType::Identity,
86+
event.data.block.id.inner() as usize,
87+
&event.data.block.hash,
88+
)
89+
.await?;
9890
}
9991

10092
Ok(())
@@ -106,114 +98,102 @@ impl BlockTransportServiceApi for BlockTransportService {
10698
"sending company chain events for company id: {}",
10799
events.company_id
108100
);
109-
if let Some(node) = self
110-
.nostr_transport
111-
.get_node_transport(&events.sender())
112-
.await
113-
{
114-
if let Some(event) = events.generate_blockchain_message() {
115-
let (previous_event, root_event) = self
116-
.nostr_transport
117-
.find_root_and_previous_event(
118-
&event.data.block.previous_hash,
119-
&event.data.node_id.to_string(),
120-
BlockchainType::Company,
121-
)
122-
.await?;
123-
// send the event
124-
let nostr_event = node
125-
.send_public_chain_event(
126-
&event.data.node_id.to_string(),
127-
BlockchainType::Company,
128-
event.data.block.timestamp,
129-
events.keys.clone(),
130-
event.clone().try_into()?,
131-
previous_event.clone().map(|e| e.payload),
132-
root_event.clone().map(|e| e.payload),
133-
)
134-
.await?;
135-
// and store the event locally
136-
self.nostr_transport
137-
.add_chain_event(
138-
&nostr_event,
139-
&root_event,
140-
&previous_event,
141-
&event.data.node_id.to_string(),
142-
BlockchainType::Company,
143-
event.data.block.id.inner() as usize,
144-
&event.data.block.hash,
145-
)
146-
.await?;
147-
}
101+
let node = self.nostr_transport.get_node_transport(&events.sender());
102+
103+
if let Some(event) = events.generate_blockchain_message() {
104+
let (previous_event, root_event) = self
105+
.nostr_transport
106+
.find_root_and_previous_event(
107+
&event.data.block.previous_hash,
108+
&event.data.node_id.to_string(),
109+
BlockchainType::Company,
110+
)
111+
.await?;
112+
// send the event
113+
let nostr_event = node
114+
.send_public_chain_event(
115+
&events.sender(),
116+
&event.data.node_id.to_string(),
117+
BlockchainType::Company,
118+
event.data.block.timestamp,
119+
events.keys.clone(),
120+
event.clone().try_into()?,
121+
previous_event.clone().map(|e| e.payload),
122+
root_event.clone().map(|e| e.payload),
123+
)
124+
.await?;
125+
// and store the event locally
126+
self.nostr_transport
127+
.add_chain_event(
128+
&nostr_event,
129+
&root_event,
130+
&previous_event,
131+
&event.data.node_id.to_string(),
132+
BlockchainType::Company,
133+
event.data.block.id.inner() as usize,
134+
&event.data.block.hash,
135+
)
136+
.await?;
137+
}
148138

149-
// handle potential invite for new signatory
150-
if let Some((recipient, invite)) = events.generate_company_invite_message()
151-
&& let Some(identity) = self.nostr_transport.resolve_identity(&recipient).await
152-
{
153-
node.send_private_event(&identity, invite.try_into()?)
154-
.await?;
155-
}
156-
} else {
157-
error!(
158-
"could not find transport instance for sender node {}",
159-
events.sender()
160-
);
139+
// handle potential invite for new signatory
140+
if let Some((recipient, invite)) = events.generate_company_invite_message()
141+
&& let Some(identity) = self.nostr_transport.resolve_identity(&recipient).await
142+
{
143+
node.send_private_event(&events.sender(), &identity, invite.try_into()?)
144+
.await?;
161145
}
162146

163147
Ok(())
164148
}
165149

166150
/// Sent when: A bill chain is created or updated
167151
async fn send_bill_chain_events(&self, events: BillChainEvent) -> Result<()> {
168-
if let Some(node) = self
169-
.nostr_transport
170-
.get_node_transport(&events.sender())
171-
.await
172-
{
173-
if let Some(block_event) = events.generate_blockchain_message() {
174-
let (previous_event, root_event) = self
175-
.nostr_transport
176-
.find_root_and_previous_event(
177-
&block_event.data.block.previous_hash,
178-
&block_event.data.bill_id.to_string(),
179-
BlockchainType::Bill,
180-
)
181-
.await?;
182-
183-
// now send the event
184-
let event = node
185-
.send_public_chain_event(
186-
&block_event.data.bill_id.to_string(),
187-
BlockchainType::Bill,
188-
block_event.data.block.timestamp,
189-
events.bill_keys.clone(),
190-
block_event.clone().try_into()?,
191-
previous_event.clone().map(|e| e.payload),
192-
root_event.clone().map(|e| e.payload),
193-
)
194-
.await?;
195-
196-
self.nostr_transport
197-
.add_chain_event(
198-
&event,
199-
&root_event,
200-
&previous_event,
201-
&block_event.data.bill_id.to_string(),
202-
BlockchainType::Bill,
203-
block_event.data.block.id.inner() as usize,
204-
&block_event.data.block.hash,
205-
)
206-
.await?;
207-
}
152+
let node = self.nostr_transport.get_node_transport(&events.sender());
153+
154+
if let Some(block_event) = events.generate_blockchain_message() {
155+
let (previous_event, root_event) = self
156+
.nostr_transport
157+
.find_root_and_previous_event(
158+
&block_event.data.block.previous_hash,
159+
&block_event.data.bill_id.to_string(),
160+
BlockchainType::Bill,
161+
)
162+
.await?;
163+
164+
// now send the event
165+
let event = node
166+
.send_public_chain_event(
167+
&events.sender(),
168+
&block_event.data.bill_id.to_string(),
169+
BlockchainType::Bill,
170+
block_event.data.block.timestamp,
171+
events.bill_keys.clone(),
172+
block_event.clone().try_into()?,
173+
previous_event.clone().map(|e| e.payload),
174+
root_event.clone().map(|e| e.payload),
175+
)
176+
.await?;
177+
178+
self.nostr_transport
179+
.add_chain_event(
180+
&event,
181+
&root_event,
182+
&previous_event,
183+
&block_event.data.bill_id.to_string(),
184+
BlockchainType::Bill,
185+
block_event.data.block.id.inner() as usize,
186+
&block_event.data.block.hash,
187+
)
188+
.await?;
189+
}
208190

209-
let invites = events.generate_bill_invite_events();
210-
if !invites.is_empty() {
211-
for (recipient, event) in invites {
212-
if let Some(identity) = self.nostr_transport.resolve_identity(&recipient).await
213-
{
214-
node.send_private_event(&identity, event.try_into()?)
215-
.await?;
216-
}
191+
let invites = events.generate_bill_invite_events();
192+
if !invites.is_empty() {
193+
for (recipient, event) in invites {
194+
if let Some(identity) = self.nostr_transport.resolve_identity(&recipient).await {
195+
node.send_private_event(&events.sender(), &identity, event.try_into()?)
196+
.await?;
217197
}
218198
}
219199
}
@@ -244,6 +224,6 @@ impl BlockTransportServiceApi for BlockTransportService {
244224

245225
/// Adds a new transport client for a company if it does not already exist
246226
async fn add_company_transport(&self, company: &Company, keys: &BcrKeys) -> Result<()> {
247-
self.nostr_transport.add_company_client(company, keys).await
227+
self.nostr_transport.add_company_keys(company, keys).await
248228
}
249229
}

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,19 @@ impl ContactTransportServiceApi for ContactTransportService {
4747
async fn resolve_contact(&self, node_id: &NodeId) -> Result<Option<NostrContactData>> {
4848
validate_node_id_network(node_id)?;
4949
// take any transport - doesn't matter
50-
if let Some(transport) = self.nostr_transport.get_first_transport().await {
51-
let res = transport.resolve_contact(node_id).await?;
52-
Ok(res)
53-
} else {
54-
Ok(None)
55-
}
50+
let transport = self.nostr_transport.get_first_transport();
51+
let res = transport.resolve_contact(node_id).await?;
52+
Ok(res)
5653
}
5754

5855
/// Publish contact data for NodeId to nostr. Will only publish if the NodeId points to a
5956
/// registered nostr client and therefore is our own.
6057
async fn publish_contact(&self, node_id: &NodeId, data: &NostrContactData) -> Result<()> {
61-
if let Some(transport) = self.nostr_transport.get_node_transport(node_id).await {
62-
transport.publish_metadata(&data.metadata).await?;
63-
transport.publish_relay_list(data.relays.clone()).await?;
64-
}
58+
let transport = self.nostr_transport.get_node_transport(node_id);
59+
transport.publish_metadata(node_id, &data.metadata).await?;
60+
transport
61+
.publish_relay_list(node_id, data.relays.clone())
62+
.await?;
6563
Ok(())
6664
}
6765

0 commit comments

Comments
 (0)