Skip to content

Commit 4c746fc

Browse files
authored
Nip 04 messages (#462)
* Allow swtiching between NIP04 and NIP17 * Clippy fix * Do not create a notification after handler err * Review fixes
1 parent 354046b commit 4c746fc

File tree

6 files changed

+159
-40
lines changed

6 files changed

+159
-40
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ uuid = { version = "1", default-features = false, features = ["v4", "js"] }
3434
bitcoin = { version = "0.32", default-features = false }
3535
bip39 = { version = "2.1", features = ["rand"] }
3636
ecies = { version = "0.2", default-features = false, features = ["pure"] }
37-
nostr-sdk = { version = "0.40", features = ["nip59"] }
37+
nostr-sdk = { version = "0.40", features = ["nip04", "nip59"] }
3838
getrandom = { version = "0.3.1", features = ["wasm_js"] }
3939
reqwest = { version = "0.12", default-features = false, features = ["json"] }
4040
async-broadcast = "0.7.2"

crates/bcr-ebill-api/src/constants.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
pub const MAX_FILE_SIZE_BYTES: usize = 1_000_000; // ~1 MB
33
pub const MAX_FILE_NAME_CHARACTERS: usize = 200;
44
pub const VALID_FILE_MIME_TYPES: [&str; 3] = ["image/jpeg", "image/png", "application/pdf"];
5+
6+
// When subscribing events we subtract this from the last received event time
7+
pub const NOSTR_EVENT_TIME_SLACK: u64 = 3600; // 1 hour

crates/bcr-ebill-api/src/service/notification_service/nostr.rs

Lines changed: 151 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
use async_trait::async_trait;
2-
use bcr_ebill_core::contact::IdentityPublicData;
2+
use bcr_ebill_core::{contact::IdentityPublicData, util::crypto};
33
use bcr_ebill_transport::event::EventEnvelope;
44
use bcr_ebill_transport::handler::NotificationHandlerApi;
5-
use log::{error, trace, warn};
6-
use nostr_sdk::nips::nip59::UnwrappedGift;
5+
use log::{error, info, trace, warn};
76
use nostr_sdk::{
8-
Client, EventId, Filter, Kind, Metadata, Options, PublicKey, RelayPoolNotification, Timestamp,
9-
ToBech32, UnsignedEvent,
7+
Client, EventBuilder, EventId, Filter, Kind, Metadata, Options, PublicKey,
8+
RelayPoolNotification, SecretKey, Tag, Timestamp, ToBech32, UnsignedEvent,
9+
nips::{nip04, nip59::UnwrappedGift},
1010
};
1111
use std::collections::HashMap;
1212
use std::str::FromStr;
1313
use std::sync::Arc;
1414

15-
use crate::service::contact_service::ContactServiceApi;
16-
use crate::util::{BcrKeys, crypto};
15+
use crate::util::BcrKeys;
16+
use crate::{constants::NOSTR_EVENT_TIME_SLACK, service::contact_service::ContactServiceApi};
1717
use bcr_ebill_core::ServiceTraitBounds;
1818
use bcr_ebill_persistence::{NostrEventOffset, NostrEventOffsetStoreApi};
1919
use bcr_ebill_transport::{Error, NotificationJsonTransportApi, Result};
@@ -47,7 +47,7 @@ impl NostrConfig {
4747
/// A wrapper around nostr_sdk that implements the NotificationJsonTransportApi.
4848
///
4949
/// # Example:
50-
/// ```ignore
50+
/// ```no_run
5151
/// let config = NostrConfig {
5252
/// keys: BcrKeys::new(),
5353
/// relays: vec!["wss://relay.example.com".to_string()],
@@ -95,6 +95,14 @@ impl NostrClient {
9595
self.keys.get_public_key()
9696
}
9797

98+
pub fn get_nostr_keys(&self) -> nostr_sdk::Keys {
99+
self.keys.get_nostr_keys()
100+
}
101+
102+
fn use_nip04(&self) -> bool {
103+
true
104+
}
105+
98106
/// Subscribe to some nostr events with a filter
99107
pub async fn subscribe(&self, subscription: Filter) -> Result<()> {
100108
self.client
@@ -107,10 +115,21 @@ impl NostrClient {
107115
Ok(())
108116
}
109117

110-
/// Unwrap envelope from private direct message
111118
pub async fn unwrap_envelope(
112119
&self,
113120
note: RelayPoolNotification,
121+
) -> Option<(EventEnvelope, PublicKey, EventId, Timestamp)> {
122+
if self.use_nip04() {
123+
self.unwrap_nip04_envelope(note).await
124+
} else {
125+
self.unwrap_nip17_envelope(note).await
126+
}
127+
}
128+
129+
/// Unwrap envelope from private direct message
130+
async fn unwrap_nip17_envelope(
131+
&self,
132+
note: RelayPoolNotification,
114133
) -> Option<(EventEnvelope, PublicKey, EventId, Timestamp)> {
115134
let mut result: Option<(EventEnvelope, PublicKey, EventId, Timestamp)> = None;
116135
if let RelayPoolNotification::Event { event, .. } = note {
@@ -127,17 +146,68 @@ impl NostrClient {
127146
}
128147
result
129148
}
130-
}
131149

132-
impl ServiceTraitBounds for NostrClient {}
150+
/// Unwrap envelope from private direct message
151+
async fn unwrap_nip04_envelope(
152+
&self,
153+
note: RelayPoolNotification,
154+
) -> Option<(EventEnvelope, PublicKey, EventId, Timestamp)> {
155+
let mut result: Option<(EventEnvelope, PublicKey, EventId, Timestamp)> = None;
156+
if let RelayPoolNotification::Event { event, .. } = note {
157+
if event.kind == Kind::EncryptedDirectMessage {
158+
match nip04::decrypt(
159+
self.keys.get_nostr_keys().secret_key(),
160+
&event.pubkey,
161+
&event.content,
162+
) {
163+
Ok(decrypted) => {
164+
result = extract_text_envelope(&decrypted)
165+
.map(|e| (e, event.pubkey, event.id, event.created_at));
166+
}
167+
Err(e) => {
168+
error!("Decrypting event failed: {e}");
169+
}
170+
}
171+
} else {
172+
info!(
173+
"Received event with kind {} but expected GiftWrap",
174+
event.kind
175+
);
176+
}
177+
}
178+
result
179+
}
133180

134-
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
135-
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
136-
impl NotificationJsonTransportApi for NostrClient {
137-
fn get_sender_key(&self) -> String {
138-
self.get_node_id()
181+
pub async fn send_nip04_message(
182+
&self,
183+
recipient: &IdentityPublicData,
184+
event: EventEnvelope,
185+
) -> bcr_ebill_transport::Result<()> {
186+
if let Ok(npub) = crypto::get_nostr_npub_as_hex_from_node_id(&recipient.node_id) {
187+
let public_key = PublicKey::from_str(&npub).map_err(|e| {
188+
error!("Failed to parse Nostr npub when sending a notification: {e}");
189+
Error::Crypto("Failed to parse Nostr npub".to_string())
190+
})?;
191+
let message = serde_json::to_string(&event)?;
192+
let event =
193+
create_nip04_event(self.get_nostr_keys().secret_key(), &public_key, &message)?;
194+
if let Some(relay) = &recipient.nostr_relay {
195+
if let Err(e) = self.client.send_event_builder_to(vec![relay], event).await {
196+
error!("Error sending Nostr message: {e}")
197+
};
198+
} else if let Err(e) = self.client.send_event_builder(event).await {
199+
error!("Error sending Nostr message: {e}")
200+
}
201+
} else {
202+
error!(
203+
"Try to send Nostr message but Nostr npub not found in contact {}",
204+
recipient.name
205+
);
206+
}
207+
Ok(())
139208
}
140-
async fn send(
209+
210+
async fn send_nip17_message(
141211
&self,
142212
recipient: &IdentityPublicData,
143213
event: EventEnvelope,
@@ -173,6 +243,28 @@ impl NotificationJsonTransportApi for NostrClient {
173243
}
174244
}
175245

246+
impl ServiceTraitBounds for NostrClient {}
247+
248+
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
249+
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
250+
impl NotificationJsonTransportApi for NostrClient {
251+
fn get_sender_key(&self) -> String {
252+
self.get_node_id()
253+
}
254+
async fn send(
255+
&self,
256+
recipient: &IdentityPublicData,
257+
event: EventEnvelope,
258+
) -> bcr_ebill_transport::Result<()> {
259+
if self.use_nip04() {
260+
self.send_nip04_message(recipient, event).await?;
261+
} else {
262+
self.send_nip17_message(recipient, event).await?;
263+
}
264+
Ok(())
265+
}
266+
}
267+
176268
#[derive(Clone)]
177269
pub struct NostrConsumer {
178270
clients: HashMap<String, Arc<NostrClient>>,
@@ -226,15 +318,14 @@ impl NostrConsumer {
226318
// continue where we left off
227319
let offset_ts = get_offset(&offset_store, &node_id).await;
228320
let public_key = current_client.keys.get_nostr_keys().public_key();
321+
let filter = Filter::new()
322+
.pubkey(public_key)
323+
.kind(Kind::EncryptedDirectMessage)
324+
.since(offset_ts);
229325

230326
// subscribe only to private messages sent to our pubkey
231327
current_client
232-
.subscribe(
233-
Filter::new()
234-
.pubkey(public_key)
235-
.kind(Kind::GiftWrap)
236-
.since(offset_ts),
237-
)
328+
.subscribe(filter)
238329
.await
239330
.expect("Failed to subscribe to Nostr events");
240331

@@ -306,13 +397,18 @@ async fn valid_sender(
306397
}
307398

308399
async fn get_offset(db: &Arc<dyn NostrEventOffsetStoreApi>, node_id: &str) -> Timestamp {
309-
Timestamp::from_secs(
310-
db.current_offset(node_id)
311-
.await
312-
.map_err(|e| error!("Could not get event offset: {e}"))
313-
.ok()
314-
.unwrap_or(0),
315-
)
400+
let current = db
401+
.current_offset(node_id)
402+
.await
403+
.map_err(|e| error!("Could not get event offset: {e}"))
404+
.ok()
405+
.unwrap_or(0);
406+
let ts = if current <= NOSTR_EVENT_TIME_SLACK {
407+
current
408+
} else {
409+
current - NOSTR_EVENT_TIME_SLACK
410+
};
411+
Timestamp::from_secs(ts)
316412
}
317413

318414
async fn add_offset(
@@ -333,6 +429,16 @@ async fn add_offset(
333429
.ok();
334430
}
335431

432+
fn extract_text_envelope(message: &str) -> Option<EventEnvelope> {
433+
match serde_json::from_str::<EventEnvelope>(message) {
434+
Ok(envelope) => Some(envelope),
435+
Err(e) => {
436+
error!("Json deserializing event envelope failed: {e}");
437+
None
438+
}
439+
}
440+
}
441+
336442
fn extract_event_envelope(rumor: UnsignedEvent) -> Option<EventEnvelope> {
337443
if rumor.kind == Kind::PrivateDirectMessage {
338444
match serde_json::from_str::<EventEnvelope>(rumor.content.as_str()) {
@@ -347,6 +453,21 @@ fn extract_event_envelope(rumor: UnsignedEvent) -> Option<EventEnvelope> {
347453
}
348454
}
349455

456+
fn create_nip04_event(
457+
secret_key: &SecretKey,
458+
public_key: &PublicKey,
459+
message: &str,
460+
) -> Result<EventBuilder> {
461+
Ok(EventBuilder::new(
462+
Kind::EncryptedDirectMessage,
463+
nip04::encrypt(secret_key, public_key, message).map_err(|e| {
464+
error!("Failed to encrypt direct private message: {e}");
465+
Error::Crypto("Failed to encrypt direct private message".to_string())
466+
})?,
467+
)
468+
.tag(Tag::public_key(*public_key)))
469+
}
470+
350471
/// Handle extracted event with given handlers.
351472
async fn handle_event(
352473
event: EventEnvelope,

crates/bcr-ebill-transport/src/event/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ impl<T: Serialize> TryFrom<Event<T>> for EventEnvelope {
8585
/// Allows generic deserialization of an event from an envelope.
8686
/// # Example
8787
///
88-
/// ```ignore
88+
/// ```
8989
/// use serde::{Deserialize, Serialize};
90+
/// use bcr_ebill_transport::{EventType, Event, EventEnvelope};
9091
///
9192
/// #[derive(Serialize, Deserialize)]
9293
/// struct MyEventPayload {
@@ -99,10 +100,9 @@ impl<T: Serialize> TryFrom<Event<T>> for EventEnvelope {
99100
/// bar: 42,
100101
/// };
101102
///
102-
/// let event = Event::new(EventType::BillSigned, "recipient".to_string(), payload);
103+
/// let event = Event::new(EventType::Bill, "recipient", payload);
103104
/// let event: EventEnvelope = event.try_into().unwrap();
104105
/// let deserialized_event: Event<MyEventPayload> = event.try_into().unwrap();
105-
/// assert_eq!(deserialized_event.data, payload);
106106
///
107107
/// ```
108108
///

crates/bcr-ebill-transport/src/handler/bill_chain_event_handler.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ impl NotificationHandlerApi for BillChainEventHandler {
394394
.await
395395
{
396396
error!("Failed to process chain data: {}", e);
397+
return Ok(());
397398
}
398399
}
399400
if let Err(e) = self.create_notification(&decoded.data, node_id).await {

crates/bcr-ebill-wasm/main.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,6 @@ async function start() {
8181
console.log(current_identity);
8282
document.getElementById("current_identity").innerHTML = current_identity.node_id;
8383

84-
try {
85-
await identityApi.switch({ t: 1, node_id: "test" });
86-
} catch (err) {
87-
console.error("switching identity failed: ", err);
88-
}
89-
9084
// Company
9185
let companies = await companyApi.list();
9286
console.log("companies:", companies.companies.length, companies);

0 commit comments

Comments
 (0)