Skip to content
This repository was archived by the owner on Feb 3, 2025. It is now read-only.

Commit

Permalink
DM requests for invoices
Browse files Browse the repository at this point in the history
  • Loading branch information
benthecarman committed Jan 16, 2024
1 parent 4b01aca commit 34f67df
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 112 deletions.
53 changes: 31 additions & 22 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,8 @@ impl<S: MutinyStorage> MutinyWalletBuilder<S> {
}
};

// start the nostr wallet connect background process
mw.start_nostr_wallet_connect().await;
// start the nostr background process
mw.start_nostr().await;

Ok(mw)
}
Expand Down Expand Up @@ -665,8 +665,8 @@ impl<S: MutinyStorage> MutinyWallet<S> {
Ok(())
}

/// Starts a background process that will watch for nostr wallet connect events
pub(crate) async fn start_nostr_wallet_connect(&self) {
/// Starts a background process that will watch for nostr events
pub(crate) async fn start_nostr(&self) {
let nostr = self.nostr.clone();
let logger = self.logger.clone();
let stop = self.stop.clone();
Expand All @@ -677,10 +677,9 @@ impl<S: MutinyStorage> MutinyWallet<S> {
break;
};

// if we have no relays, then there are no nwc profiles enabled
// wait 10 seconds and see if we do again
let relays = nostr.get_relays();
if relays.is_empty() {
// if we have no filters, then wait 10 seconds and see if we do again
let mut last_filters = nostr.get_filters().unwrap_or_default();
if last_filters.is_empty() {
utils::sleep(10_000).await;
continue;
}
Expand Down Expand Up @@ -712,7 +711,6 @@ impl<S: MutinyStorage> MutinyWallet<S> {
.expect("Failed to add relays");
client.connect().await;

let mut last_filters = nostr.get_nwc_filters();
client.subscribe(last_filters.clone()).await;

// handle NWC requests
Expand All @@ -738,17 +736,27 @@ impl<S: MutinyStorage> MutinyWallet<S> {
notification = read_fut => {
match notification {
Ok(RelayPoolNotification::Event { event, .. }) => {
if event.kind == Kind::WalletConnectRequest && event.verify().is_ok() {
match nostr.handle_nwc_request(event, &self_clone).await {
Ok(Some(event)) => {
if let Err(e) = client.send_event(event).await {
log_warn!(logger, "Error sending NWC event: {e}");
if event.verify().is_ok() {
match event.kind {
Kind::WalletConnectRequest => {
match nostr.handle_nwc_request(event, &self_clone).await {
Ok(Some(event)) => {
if let Err(e) = client.send_event(event).await {
log_warn!(logger, "Error sending NWC event: {e}");
}
}
Ok(None) => {} // no response
Err(e) => {
log_error!(logger, "Error handling NWC request: {e}");
}
}
}
Ok(None) => {} // no response
Err(e) => {
log_error!(logger, "Error handling NWC request: {e}");
Kind::EncryptedDirectMessage => {
if let Err(e) = nostr.handle_direct_message(event).await {
log_error!(logger, "Error handling dm: {e}");
}
}
kind => log_warn!(logger, "Received unexpected note of kind {kind}")
}
}
},
Expand All @@ -766,11 +774,12 @@ impl<S: MutinyStorage> MutinyWallet<S> {
}
_ = filter_check_fut => {
// Check if the filters have changed
let current_filters = nostr.get_nwc_filters();
if current_filters != last_filters {
log_debug!(logger, "subscribing to new nwc filters");
client.subscribe(current_filters.clone()).await;
last_filters = current_filters;
if let Ok(current_filters) = nostr.get_filters() {
if current_filters != last_filters {
log_debug!(logger, "subscribing to new nwc filters");
client.subscribe(current_filters.clone()).await;
last_filters = current_filters;
}
}
// Set the time for the next filter check
next_filter_check = crate::utils::now().as_secs() + 5;
Expand Down
215 changes: 171 additions & 44 deletions mutiny-core/src/nostr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ use futures::{pin_mut, select, FutureExt};
use futures_util::lock::Mutex;
use lightning::util::logger::Logger;
use lightning::{log_error, log_warn};
use nostr::key::SecretKey;
use lightning_invoice::Bolt11Invoice;
use nostr::key::{SecretKey, XOnlyPublicKey};
use nostr::nips::nip47::*;
use nostr::prelude::{decrypt, encrypt};
use nostr::{Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Tag};
use nostr::{Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Tag, Timestamp};
use nostr_sdk::{Client, RelayPoolNotification};
use std::collections::HashSet;
use std::sync::{atomic::Ordering, Arc, RwLock};
use std::time::Duration;
use std::{str::FromStr, sync::atomic::AtomicBool};
Expand Down Expand Up @@ -91,14 +93,17 @@ impl<S: MutinyStorage> NostrManager<S> {
.map(|x| x.profile.relay.clone())
.collect();

// add relay to pull DMs from
relays.push("wss://relay.primal.net".to_string());

// remove duplicates
relays.sort();
relays.dedup();

relays
}

pub fn get_nwc_filters(&self) -> Vec<Filter> {
fn get_nwc_filters(&self) -> Vec<Filter> {
self.nwc
.read()
.unwrap()
Expand All @@ -108,6 +113,46 @@ impl<S: MutinyStorage> NostrManager<S> {
.collect()
}

/// Filters for getting DMs to and from ourselves with our contacts
fn get_dm_filters(&self) -> Result<Vec<Filter>, MutinyError> {
let contacts = self.storage.get_contacts()?;
let last_sync_time = self.storage.get_dm_sync_time()?;
let me = self.primary_key.public_key();
let npubs: HashSet<XOnlyPublicKey> = contacts.into_values().flat_map(|c| c.npub).collect();

// if we haven't synced before, use now and save to storage
let time_stamp = match last_sync_time {
None => {
let now = Timestamp::now();
self.storage.set_dm_sync_time(now.as_u64())?;
now
}
Some(time) => Timestamp::from(time),
};

let sent_dm_filter = Filter::new()
.kind(Kind::EncryptedDirectMessage)
.author(me)
.pubkeys(npubs.clone())
.since(time_stamp);

let received_dm_filter = Filter::new()
.kind(Kind::EncryptedDirectMessage)
.authors(npubs)
.pubkey(me)
.since(time_stamp);

Ok(vec![sent_dm_filter, received_dm_filter])
}

pub fn get_filters(&self) -> Result<Vec<Filter>, MutinyError> {
let mut nwc = self.get_nwc_filters();
let dm = self.get_dm_filters()?;
nwc.extend(dm);

Ok(nwc)
}

pub fn get_nwc_uri(&self, index: u32) -> Result<Option<NostrWalletConnectURI>, MutinyError> {
let opt = self
.nwc
Expand Down Expand Up @@ -539,7 +584,7 @@ impl<S: MutinyStorage> NostrManager<S> {
fn find_nwc_data(
&self,
hash: &sha256::Hash,
) -> Result<(NostrWalletConnect, PendingNwcInvoice), MutinyError> {
) -> Result<(Option<NostrWalletConnect>, PendingNwcInvoice), MutinyError> {
let pending: Vec<PendingNwcInvoice> = self
.storage
.get_data(PENDING_NWC_EVENTS_KEY)?
Expand All @@ -550,14 +595,17 @@ impl<S: MutinyStorage> NostrManager<S> {
.find(|x| x.invoice.payment_hash() == hash)
.ok_or(MutinyError::NotFound)?;

let nwc = {
let profiles = self.nwc.read().unwrap();
profiles
.iter()
.find(|x| x.profile.index == inv.index)
.ok_or(MutinyError::NotFound)?
.clone()
};
let nwc = inv
.index
.map(|index| {
let profiles = self.nwc.read().unwrap();
profiles
.iter()
.find(|x| x.profile.index == index)
.ok_or(MutinyError::NotFound)
.cloned()
})
.transpose()?;

Ok((nwc, inv.to_owned()))
}
Expand Down Expand Up @@ -612,12 +660,32 @@ impl<S: MutinyStorage> NostrManager<S> {
&self,
hash: sha256::Hash,
invoice_handler: &impl InvoiceHandler,
) -> Result<EventId, MutinyError> {
) -> Result<Option<EventId>, MutinyError> {
let (nwc, inv) = self.find_nwc_data(&hash)?;

let resp = nwc.pay_nwc_invoice(invoice_handler, &inv.invoice).await?;
let event_id = match nwc {
Some(nwc) => {
let resp = nwc.pay_nwc_invoice(invoice_handler, &inv.invoice).await?;
Some(self.broadcast_nwc_response(resp, nwc, inv).await?)
}
None => {
// handle dm invoice

let event_id = self.broadcast_nwc_response(resp, nwc, inv).await?;
// find contact, tag invoice with id
let contacts = self.storage.get_contacts()?;
let label = contacts
.into_iter()
.find(|(_, c)| c.npub == Some(inv.pubkey))
.map(|(id, _)| vec![id])
.unwrap_or_default();
if let Err(e) = invoice_handler.pay_invoice(&inv.invoice, None, label).await {
log_error!(invoice_handler.logger(), "failed to pay invoice: {e}");
return Err(e);
}

None
}
};

// get lock for writing
self.pending_nwc_lock.lock().await;
Expand Down Expand Up @@ -651,7 +719,9 @@ impl<S: MutinyStorage> NostrManager<S> {
result: None,
};
let (nwc, inv) = self.find_nwc_data(&hash)?;
self.broadcast_nwc_response(resp, nwc, inv).await?;
if let Some(nwc) = nwc {
self.broadcast_nwc_response(resp, nwc, inv).await?;
}
}

// wait for lock
Expand Down Expand Up @@ -707,33 +777,35 @@ impl<S: MutinyStorage> NostrManager<S> {
};
let (nwc, inv) = self.find_nwc_data(invoice.invoice.payment_hash())?;

let encrypted = encrypt(
&nwc.server_key.secret_key().unwrap(),
&nwc.client_pubkey(),
resp.as_json(),
)
.unwrap();

let p_tag = Tag::PublicKey {
public_key: inv.pubkey,
relay_url: None,
alias: None,
};
let e_tag = Tag::Event {
event_id: inv.event_id,
relay_url: None,
marker: None,
};
let response =
EventBuilder::new(Kind::WalletConnectResponse, encrypted, [p_tag, e_tag])
.to_event(&nwc.server_key)
.map_err(|e| {
MutinyError::Other(anyhow::anyhow!("Failed to create event: {e:?}"))
})?;

client.send_event(response).await.map_err(|e| {
MutinyError::Other(anyhow::anyhow!("Failed to send info event: {e:?}"))
})?;
if let Some(nwc) = nwc {
let encrypted = encrypt(
&nwc.server_key.secret_key().unwrap(),
&nwc.client_pubkey(),
resp.as_json(),
)
.unwrap();

let p_tag = Tag::PublicKey {
public_key: inv.pubkey,
relay_url: None,
alias: None,
};
let e_tag = Tag::Event {
event_id: inv.event_id,
relay_url: None,
marker: None,
};
let response =
EventBuilder::new(Kind::WalletConnectResponse, encrypted, [p_tag, e_tag])
.to_event(&nwc.server_key)
.map_err(|e| {
MutinyError::Other(anyhow::anyhow!("Failed to create event: {e:?}"))
})?;

client.send_event(response).await.map_err(|e| {
MutinyError::Other(anyhow::anyhow!("Failed to send info event: {e:?}"))
})?;
}
}

let _ = client.disconnect().await;
Expand Down Expand Up @@ -768,6 +840,61 @@ impl<S: MutinyStorage> NostrManager<S> {
Ok(())
}

/// Handles an encrypted direct message. If it is an invoice we add it to our pending
/// invoice storage.
pub async fn handle_direct_message(&self, event: Event) -> anyhow::Result<()> {
if event.kind != Kind::EncryptedDirectMessage {
anyhow::bail!("Not a direct message");
}

// update sync time
self.storage.set_dm_sync_time(event.created_at.as_u64())?;

// todo we should handle NIP-44 as well
let decrypted = decrypt(
&self.primary_key.secret_key()?,
&event.pubkey,
event.content,
)?;

if let Ok(invoice) = Bolt11Invoice::from_str(&decrypted) {
self.save_pending_nwc_invoice(None, event.id, event.pubkey, invoice)
.await?;
}

Ok(())
}

pub(crate) async fn save_pending_nwc_invoice(
&self,
profile_index: Option<u32>,
event_id: EventId,
event_pk: XOnlyPublicKey,
invoice: Bolt11Invoice,
) -> anyhow::Result<()> {
let pending = PendingNwcInvoice {
index: profile_index,
invoice,
event_id,
pubkey: event_pk,
};
self.pending_nwc_lock.lock().await;

let mut current: Vec<PendingNwcInvoice> = self
.storage
.get_data(PENDING_NWC_EVENTS_KEY)?
.unwrap_or_default();

if !current.contains(&pending) {
current.push(pending);

self.storage
.set_data(PENDING_NWC_EVENTS_KEY.to_string(), current, None)?;
}

Ok(())
}

pub async fn handle_nwc_request(
&self,
event: Event,
Expand Down Expand Up @@ -1388,7 +1515,7 @@ mod test {
.unwrap();

let inv = PendingNwcInvoice {
index: profile.index,
index: Some(profile.index),
invoice: Bolt11Invoice::from_str("lnbc923720n1pj9nrefpp5pczykgk37af5388n8dzynljpkzs7sje4melqgazlwv9y3apay8jqhp5rd8saxz3juve3eejq7z5fjttxmpaq88d7l92xv34n4h3mq6kwq2qcqzzsxqzfvsp5z0jwpehkuz9f2kv96h62p8x30nku76aj8yddpcust7g8ad0tr52q9qyyssqfy622q25helv8cj8hyxqltws4rdwz0xx2hw0uh575mn7a76cp3q4jcptmtjkjs4a34dqqxn8uy70d0qlxqleezv4zp84uk30pp5q3nqq4c9gkz").unwrap(),
event_id: EventId::from_slice(&[0; 32]).unwrap(),
pubkey: XOnlyPublicKey::from_str("552a9d06810f306bfc085cb1e1c26102554138a51fa3a7fdf98f5b03a945143a").unwrap(),
Expand Down
Loading

0 comments on commit 34f67df

Please sign in to comment.