Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support new NWC commands #1048

Merged
merged 8 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions mutiny-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ use crate::{
subscription::MutinySubscriptionClient,
};
use crate::{nostr::NostrManager, utils::sleep};
use ::nostr::nips::nip47::Method;
use ::nostr::nips::nip57;
#[cfg(target_arch = "wasm32")]
use ::nostr::prelude::rand::rngs::OsRng;
Expand All @@ -94,6 +95,7 @@ use futures_util::join;
use hex_conservative::{DisplayHex, FromHex};
#[cfg(target_arch = "wasm32")]
use instant::Instant;
use lightning::chain::BestBlock;
use lightning::ln::PaymentHash;
use lightning::util::logger::Logger;
use lightning::{log_debug, log_error, log_info, log_trace, log_warn};
Expand Down Expand Up @@ -130,7 +132,9 @@ const MELT_CASHU_TOKEN: &str = "Cashu Token Melt";
pub trait InvoiceHandler {
fn logger(&self) -> &MutinyLogger;
fn skip_hodl_invoices(&self) -> bool;
async fn get_outbound_payment_status(&self, payment_hash: &[u8; 32]) -> Option<HTLCStatus>;
fn get_network(&self) -> Network;
async fn get_best_block(&self) -> Result<BestBlock, MutinyError>;
async fn lookup_payment(&self, payment_hash: &[u8; 32]) -> Option<MutinyInvoice>;
async fn pay_invoice(
&self,
invoice: &Bolt11Invoice,
Expand Down Expand Up @@ -337,6 +341,27 @@ pub struct MutinyInvoice {
pub last_updated: u64,
}

#[cfg(test)]
impl Default for MutinyInvoice {
fn default() -> Self {
MutinyInvoice {
bolt11: None,
description: None,
payment_hash: sha256::Hash::all_zeros(),
preimage: None,
payee_pubkey: None,
amount_sats: None,
expire: 0,
status: HTLCStatus::Pending,
privacy_level: PrivacyLevel::NotAvailable,
fees_paid: None,
inbound: false,
labels: vec![],
last_updated: 0,
}
}
}

impl MutinyInvoice {
pub fn paid(&self) -> bool {
self.status == HTLCStatus::Succeeded
Expand Down Expand Up @@ -1653,6 +1678,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
period: BudgetPeriod::Month,
}),
NwcProfileTag::Subscription,
vec![Method::PayInvoice], // subscription only needs pay invoice
)
.await?
.nwc_uri
Expand All @@ -1662,6 +1688,7 @@ impl<S: MutinyStorage> MutinyWallet<S> {
ProfileType::Reserved(ReservedProfile::MutinySubscription),
SpendingConditions::RequireApproval,
NwcProfileTag::Subscription,
vec![Method::PayInvoice], // subscription only needs pay invoice
)
.await?
.nwc_uri
Expand Down Expand Up @@ -2449,11 +2476,19 @@ impl<S: MutinyStorage> InvoiceHandler for MutinyWallet<S> {
self.skip_hodl_invoices
}

async fn get_outbound_payment_status(&self, payment_hash: &[u8; 32]) -> Option<HTLCStatus> {
fn get_network(&self) -> Network {
self.network
}

async fn get_best_block(&self) -> Result<BestBlock, MutinyError> {
let node = self.node_manager.get_node_by_key_or_first(None).await?;
Ok(node.channel_manager.current_best_block())
}

async fn lookup_payment(&self, payment_hash: &[u8; 32]) -> Option<MutinyInvoice> {
self.get_invoice_by_hash(&sha256::Hash::from_byte_array(*payment_hash))
.await
.ok()
.map(|p| p.status)
}

async fn pay_invoice(
Expand Down
85 changes: 60 additions & 25 deletions mutiny-core/src/nostr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,11 @@ impl<S: MutinyStorage> NostrManager<S> {
let futures: Vec<_> = indices_to_remove
.into_iter()
.map(|(index, hash)| async move {
match invoice_handler.get_outbound_payment_status(&hash).await {
match invoice_handler
.lookup_payment(&hash)
.await
.map(|x| x.status)
{
Some(HTLCStatus::Succeeded) => Some(index),
_ => None,
}
Expand Down Expand Up @@ -433,6 +437,7 @@ impl<S: MutinyStorage> NostrManager<S> {
uri: NIP49URI,
budget: Option<BudgetedSpendingConditions>,
tag: NwcProfileTag,
commands: Vec<Method>,
) -> Result<NwcProfile, MutinyError> {
let spending_conditions = match uri.budget {
None => match budget {
Expand Down Expand Up @@ -486,6 +491,7 @@ impl<S: MutinyStorage> NostrManager<S> {
enabled: None,
archived: None,
spending_conditions,
commands: Some(commands),
tag,
label,
};
Expand Down Expand Up @@ -514,6 +520,7 @@ impl<S: MutinyStorage> NostrManager<S> {
profile_type: ProfileType,
spending_conditions: SpendingConditions,
tag: NwcProfileTag,
commands: Vec<Method>,
) -> Result<NwcProfile, MutinyError> {
let mut profiles = self.nwc.try_write()?;

Expand All @@ -530,6 +537,7 @@ impl<S: MutinyStorage> NostrManager<S> {
tag,
client_key: None,
label: None,
commands: Some(commands),
};
let nwc = NostrWalletConnect::new(&Secp256k1::new(), self.xprivkey, profile)?;

Expand All @@ -556,9 +564,10 @@ impl<S: MutinyStorage> NostrManager<S> {
profile_type: ProfileType,
spending_conditions: SpendingConditions,
tag: NwcProfileTag,
commands: Vec<Method>,
) -> Result<NwcProfile, MutinyError> {
let profile =
self.create_new_nwc_profile_internal(profile_type, spending_conditions, tag)?;
self.create_new_nwc_profile_internal(profile_type, spending_conditions, tag, commands)?;
// add relay if needed
let needs_connect = self.client.add_relay(profile.relay.as_str()).await?;
if needs_connect {
Expand Down Expand Up @@ -596,8 +605,13 @@ impl<S: MutinyStorage> NostrManager<S> {
amount_sats,
payment_hash: None,
});
self.create_new_nwc_profile(profile, spending_conditions, NwcProfileTag::Gift)
.await
self.create_new_nwc_profile(
profile,
spending_conditions,
NwcProfileTag::Gift,
vec![Method::PayInvoice], // gifting only needs pay invoice
)
.await
}

/// Approves a nostr wallet auth request.
Expand All @@ -616,7 +630,7 @@ impl<S: MutinyStorage> NostrManager<S> {

let secret = uri.secret.clone();
let relay = uri.relay_url.to_string();
let profile = self.nostr_wallet_auth(profile_type, uri, budget, tag)?;
let profile = self.nostr_wallet_auth(profile_type, uri, budget, tag, commands.clone())?;

let nwc = self.nwc.try_read()?.iter().find_map(|nwc| {
if nwc.profile.index == profile.index {
Expand Down Expand Up @@ -709,7 +723,12 @@ impl<S: MutinyStorage> NostrManager<S> {

let p_tag = Tag::public_key(inv.pubkey);
let e_tag = Tag::event(inv.event_id);
let response = EventBuilder::new(Kind::WalletConnectResponse, encrypted, [p_tag, e_tag])
let tags = match inv.identifier {
Some(id) => vec![p_tag, e_tag, Tag::Identifier(id)],
None => vec![p_tag, e_tag],
};

let response = EventBuilder::new(Kind::WalletConnectResponse, encrypted, tags)
.to_event(&nwc.server_key)
.map_err(|e| MutinyError::Other(anyhow::anyhow!("Failed to create event: {e:?}")))?;

Expand Down Expand Up @@ -909,17 +928,22 @@ impl<S: MutinyStorage> NostrManager<S> {

let decrypted = self.decrypt_dm(event.pubkey, &event.content).await?;

let invoice: Bolt11Invoice =
match check_valid_nwc_invoice(&decrypted, invoice_handler).await {
Ok(Some(invoice)) => invoice,
Ok(None) => return Ok(()),
Err(msg) => {
log_debug!(self.logger, "Not adding DM'd invoice: {msg}");
return Ok(());
}
};
// handle it like a pay invoice NWC request, to see if it is valid
let params = PayInvoiceRequestParams {
id: None,
invoice: decrypted,
amount: None,
};
let invoice: Bolt11Invoice = match check_valid_nwc_invoice(&params, invoice_handler).await {
Ok(Some(invoice)) => invoice,
Ok(None) => return Ok(()),
Err(msg) => {
log_debug!(self.logger, "Not adding DM'd invoice: {msg}");
return Ok(());
}
};

self.save_pending_nwc_invoice(None, event.id, event.pubkey, invoice)
self.save_pending_nwc_invoice(None, event.id, event.pubkey, invoice, None)
.await?;

Ok(())
Expand All @@ -931,12 +955,14 @@ impl<S: MutinyStorage> NostrManager<S> {
event_id: EventId,
event_pk: nostr::PublicKey,
invoice: Bolt11Invoice,
identifier: Option<String>,
) -> anyhow::Result<()> {
let pending = PendingNwcInvoice {
index: profile_index,
invoice,
event_id,
pubkey: event_pk,
identifier,
};
self.pending_nwc_lock.lock().await;

Expand Down Expand Up @@ -1075,11 +1101,11 @@ impl<S: MutinyStorage> NostrManager<S> {

// check if the invoice has been paid, if so, return, otherwise continue
// checking for response event
if let Some(status) = invoice_handler
.get_outbound_payment_status(&bolt11.payment_hash().into_32())
if let Some(inv) = invoice_handler
.lookup_payment(&bolt11.payment_hash().into_32())
.await
{
if status == HTLCStatus::Succeeded {
if inv.status == HTLCStatus::Succeeded {
break;
}
}
Expand Down Expand Up @@ -1414,7 +1440,7 @@ mod test {

// add handling for mock
inv_handler
.expect_get_outbound_payment_status()
.expect_lookup_payment()
.with(eq(invoice.payment_hash().into_32()))
.returning(move |_| None);

Expand Down Expand Up @@ -1458,6 +1484,7 @@ mod test {
ProfileType::Normal { name: name.clone() },
SpendingConditions::default(),
Default::default(),
vec![Method::PayInvoice],
)
.unwrap();

Expand Down Expand Up @@ -1501,6 +1528,7 @@ mod test {
ProfileType::Reserved(ReservedProfile::MutinySubscription),
SpendingConditions::default(),
Default::default(),
vec![Method::PayInvoice],
)
.unwrap();

Expand Down Expand Up @@ -1534,6 +1562,7 @@ mod test {
ProfileType::Normal { name: name.clone() },
SpendingConditions::default(),
Default::default(),
vec![Method::PayInvoice],
)
.unwrap();

Expand All @@ -1551,6 +1580,7 @@ mod test {
archived: None,
child_key_index: None,
spending_conditions: Default::default(),
commands: None,
tag: Default::default(),
label: None,
};
Expand Down Expand Up @@ -1617,6 +1647,7 @@ mod test {
uri.clone(),
None,
Default::default(),
vec![Method::PayInvoice],
)
.unwrap();

Expand Down Expand Up @@ -1666,6 +1697,7 @@ mod test {
ProfileType::Normal { name: name.clone() },
SpendingConditions::default(),
Default::default(),
vec![Method::PayInvoice],
)
.unwrap();

Expand Down Expand Up @@ -1707,6 +1739,7 @@ mod test {
ProfileType::Normal { name: name.clone() },
SpendingConditions::default(),
Default::default(),
vec![Method::PayInvoice],
)
.unwrap();

Expand Down Expand Up @@ -1739,15 +1772,17 @@ mod test {
ProfileType::Normal { name },
SpendingConditions::default(),
Default::default(),
vec![Method::PayInvoice],
)
.unwrap();

let inv = PendingNwcInvoice {
index: Some(profile.index),
invoice: Bolt11Invoice::from_str("lnbc923720n1pj9nrefpp5pczykgk37af5388n8dzynljpkzs7sje4melqgazlwv9y3apay8jqhp5rd8saxz3juve3eejq7z5fjttxmpaq88d7l92xv34n4h3mq6kwq2qcqzzsxqzfvsp5z0jwpehkuz9f2kv96h62p8x30nku76aj8yddpcust7g8ad0tr52q9qyyssqfy622q25helv8cj8hyxqltws4rdwz0xx2hw0uh575mn7a76cp3q4jcptmtjkjs4a34dqqxn8uy70d0qlxqleezv4zp84uk30pp5q3nqq4c9gkz").unwrap(),
event_id: EventId::from_slice(&[0; 32]).unwrap(),
pubkey: nostr::PublicKey::from_str("552a9d06810f306bfc085cb1e1c26102554138a51fa3a7fdf98f5b03a945143a").unwrap(),
};
index: Some(profile.index),
invoice: Bolt11Invoice::from_str("lnbc923720n1pj9nrefpp5pczykgk37af5388n8dzynljpkzs7sje4melqgazlwv9y3apay8jqhp5rd8saxz3juve3eejq7z5fjttxmpaq88d7l92xv34n4h3mq6kwq2qcqzzsxqzfvsp5z0jwpehkuz9f2kv96h62p8x30nku76aj8yddpcust7g8ad0tr52q9qyyssqfy622q25helv8cj8hyxqltws4rdwz0xx2hw0uh575mn7a76cp3q4jcptmtjkjs4a34dqqxn8uy70d0qlxqleezv4zp84uk30pp5q3nqq4c9gkz").unwrap(),
event_id: EventId::from_slice(&[0; 32]).unwrap(),
pubkey: nostr::PublicKey::from_str("552a9d06810f306bfc085cb1e1c26102554138a51fa3a7fdf98f5b03a945143a").unwrap(),
identifier: None,
};

// add dummy to storage
nostr_manager
Expand Down
Loading
Loading