From 4760ad1e620c170a3b890c876701e8ef5ce8ed36 Mon Sep 17 00:00:00 2001 From: elnosh Date: Sun, 21 Apr 2024 12:39:52 -0500 Subject: [PATCH] add list_transactions nwc command --- mutiny-core/src/lib.rs | 70 +++++- mutiny-core/src/nostr/mod.rs | 1 - mutiny-core/src/nostr/nwc.rs | 467 +++++++++++++++++++++++------------ 3 files changed, 379 insertions(+), 159 deletions(-) diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index b7dcbe55c..0fb80ff1b 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -84,7 +84,7 @@ use ::nostr::nips::nip47::Method; use ::nostr::nips::nip57; #[cfg(target_arch = "wasm32")] use ::nostr::prelude::rand::rngs::OsRng; -use ::nostr::prelude::ZapRequestData; +use ::nostr::prelude::{LookupInvoiceResponseResult, TransactionType, ZapRequestData}; #[cfg(target_arch = "wasm32")] use ::nostr::Tag; use ::nostr::{EventBuilder, EventId, JsonUtil, Keys, Kind}; @@ -146,6 +146,7 @@ pub trait InvoiceHandler { fn get_network(&self) -> Network; async fn get_best_block(&self) -> Result; async fn lookup_payment(&self, payment_hash: &[u8; 32]) -> Option; + async fn get_payments_by_label(&self, label: &str) -> Result, MutinyError>; async fn pay_invoice( &self, invoice: &Bolt11Invoice, @@ -427,6 +428,60 @@ impl From for MutinyInvoice { } } +impl From for LookupInvoiceResponseResult { + fn from(invoice: MutinyInvoice) -> Self { + let transaction_type = if invoice.inbound { + Some(TransactionType::Incoming) + } else { + Some(TransactionType::Outgoing) + }; + + let (description, description_hash) = match invoice.bolt11.as_ref() { + None => (None, None), + Some(invoice) => match invoice.description() { + Bolt11InvoiceDescription::Direct(desc) => (Some(desc.to_string()), None), + Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())), + }, + }; + + // try to get created_at from invoice, + // if it is not set, use last_updated as that's our closest approximation + let created_at = invoice + .bolt11 + .as_ref() + .map(|b| b.duration_since_epoch().as_secs()) + .unwrap_or(invoice.last_updated); + + let settled_at = if invoice.status == HTLCStatus::Succeeded { + Some(invoice.last_updated) + } else { + None + }; + + // only reveal preimage if it is settled + let preimage = if invoice.status == HTLCStatus::Succeeded { + invoice.preimage + } else { + None + }; + + LookupInvoiceResponseResult { + transaction_type, + invoice: invoice.bolt11.map(|i| i.to_string()), + description, + description_hash, + preimage, + payment_hash: invoice.payment_hash.into_32().to_lower_hex_string(), + amount: invoice.amount_sats.map(|a| a * 1_000).unwrap_or(0), + fees_paid: invoice.fees_paid.map(|a| a * 1_000).unwrap_or(0), + created_at, + expires_at: invoice.expire, + settled_at, + metadata: Default::default(), + } + } +} + impl From for PaymentInfo { fn from(invoice: MutinyInvoice) -> Self { let preimage: Option<[u8; 32]> = invoice @@ -3105,6 +3160,19 @@ impl InvoiceHandler for MutinyWallet { .ok() } + async fn get_payments_by_label(&self, label: &str) -> Result, MutinyError> { + let label_activity = self.get_label_activity(&label.to_string()).await?; + let mut invoices: Vec = Vec::with_capacity(label.len()); + + for item in label_activity { + if let ActivityItem::Lightning(mutiny_invoice) = item { + invoices.push(*mutiny_invoice); + } + } + + Ok(invoices) + } + async fn pay_invoice( &self, invoice: &Bolt11Invoice, diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 553879424..42f4c88c3 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -1541,7 +1541,6 @@ impl NostrManager { self.storage.set_nwc_sync_time(event.created_at.as_u64())?; - // TODO: handle nwc response here if let Some(mut nwc) = nwc { nwc.handle_nwc_request(event, invoice_handler, self).await?; } diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index 982221f71..f79d6fa2d 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -5,8 +5,8 @@ use crate::nostr::nip49::NIP49Confirmation; use crate::nostr::primal::PrimalApi; use crate::nostr::{derive_nwc_keys, NostrManager}; use crate::storage::MutinyStorage; -use crate::utils; use crate::InvoiceHandler; +use crate::{utils, MutinyInvoice}; use anyhow::anyhow; use bitcoin::bip32::ExtendedPrivKey; use bitcoin::hashes::hex::FromHex; @@ -21,7 +21,7 @@ use itertools::Itertools; use lightning::ln::{PaymentHash, PaymentPreimage}; use lightning::util::logger::Logger; use lightning::{log_error, log_info, log_warn}; -use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; +use lightning_invoice::Bolt11Invoice; use nostr::nips::nip04::{decrypt, encrypt}; use nostr::nips::nip47::*; use nostr::{Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Tag, Timestamp}; @@ -146,6 +146,11 @@ pub enum NwcResponse { MultiEvent(Vec), } +struct PayInvoiceRequest { + params: PayInvoiceRequestParams, + is_multi_pay: bool, +} + /// Type of Nostr Wallet Connect profile #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum NwcProfileTag { @@ -511,6 +516,10 @@ impl NostrWalletConnect { self.handle_lookup_invoice_request(event, node, nostr_manager, params) .await? } + RequestParams::ListTransactions(params) => { + self.handle_list_transactions(event, node, nostr_manager, params) + .await? + } RequestParams::GetBalance => { self.handle_get_balance_request(event, nostr_manager) .await? @@ -538,8 +547,6 @@ impl NostrWalletConnect { self.handle_multi_pay_keysend_request(event, node, nostr_manager, params) .await? } - // TODO: list_transactions - _ => return Err(anyhow!("Invalid request params for {}", req.method)), }; } @@ -637,6 +644,89 @@ impl NostrWalletConnect { Ok(Some(NwcResponse::SingleEvent(response_event))) } + async fn handle_list_transactions( + &mut self, + event: Event, + node: &impl InvoiceHandler, + nostr_manager: &NostrManager, + params: ListTransactionsRequestParams, + ) -> anyhow::Result> { + let label = self + .profile + .label + .clone() + .unwrap_or(self.profile.name.clone()); + let invoices = node.get_payments_by_label(&label).await?; + + let from = params.from.unwrap_or(0); + let until = params.until.unwrap_or(utils::now().as_secs()); + let unpaid = params.unpaid.unwrap_or(false); + + let mut invoices: Vec = invoices + .into_iter() + .filter(|invoice| { + let created_at = invoice + .bolt11 + .as_ref() + .map(|b| b.duration_since_epoch().as_secs()) + .unwrap_or(invoice.last_updated); + + if unpaid { + created_at > from && created_at < until + } else { + created_at > from + && created_at < until + && invoice.status == HTLCStatus::Succeeded + } + }) + .collect(); + + if params.transaction_type.is_some() { + let incoming = match params.transaction_type.unwrap() { + TransactionType::Incoming => true, + TransactionType::Outgoing => false, + }; + invoices.retain(|invoice| invoice.inbound == incoming); + } + + let mut transactions = Vec::with_capacity(invoices.len()); + for invoice in invoices { + let transaction: LookupInvoiceResponseResult = invoice.into(); + transactions.push(transaction); + } + // sort in descending order by creation time + transactions.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + + if params.limit.is_some() { + let limit = params.limit.unwrap() as usize; + if limit - 1 < transactions.len() { + transactions = transactions[..limit].to_vec(); + } + } + if params.offset.is_some() { + let offset = params.offset.unwrap() as usize; + if offset - 1 < transactions.len() { + transactions = transactions[offset..].to_vec(); + } + } + + let content = Response { + result_type: Method::ListTransactions, + error: None, + result: Some(ResponseResult::ListTransactions(transactions)), + }; + + let response_event = self.build_nwc_response_event(event, content, None)?; + if let Err(e) = nostr_manager + .client + .send_event(response_event.clone()) + .await + { + return Err(anyhow!("Error sending NWC event: {}", e)); + } + Ok(Some(NwcResponse::SingleEvent(response_event))) + } + async fn handle_make_invoice_request( &mut self, event: Event, @@ -708,55 +798,7 @@ impl NostrWalletConnect { result: None, }, Some(invoice) => { - let transaction_type = if invoice.inbound { - Some(TransactionType::Incoming) - } else { - Some(TransactionType::Outgoing) - }; - - let (description, description_hash) = match invoice.bolt11.as_ref() { - None => (None, None), - Some(invoice) => match invoice.description() { - Bolt11InvoiceDescription::Direct(desc) => (Some(desc.to_string()), None), - Bolt11InvoiceDescription::Hash(hash) => (None, Some(hash.0.to_string())), - }, - }; - - // try to get created_at from invoice, - // if it is not set, use last_updated as that's our closest approximation - let created_at = invoice - .bolt11 - .as_ref() - .map(|b| b.duration_since_epoch().as_secs()) - .unwrap_or(invoice.last_updated); - - let settled_at = if invoice.status == HTLCStatus::Succeeded { - Some(invoice.last_updated) - } else { - None - }; - - // only reveal preimage if it is settled - let preimage = if invoice.status == HTLCStatus::Succeeded { - invoice.preimage - } else { - None - }; - - let result = LookupInvoiceResponseResult { - transaction_type, - invoice: invoice.bolt11.map(|i| i.to_string()), - description, - description_hash, - preimage, - payment_hash: invoice.payment_hash.into_32().to_lower_hex_string(), - amount: invoice.amount_sats.map(|a| a * 1_000).unwrap_or(0), - fees_paid: invoice.fees_paid.map(|a| a * 1_000).unwrap_or(0), - created_at, - expires_at: invoice.expire, - settled_at, - metadata: Default::default(), - }; + let result: LookupInvoiceResponseResult = invoice.into(); Response { result_type: Method::LookupInvoice, @@ -836,18 +878,17 @@ impl NostrWalletConnect { event: Event, node: &impl InvoiceHandler, nostr_manager: &NostrManager, - params: PayInvoiceRequestParams, + request: PayInvoiceRequest, needs_delete: &mut bool, needs_save: &mut bool, - is_multi_pay: bool, ) -> anyhow::Result> { - let method = if is_multi_pay { + let method = if request.is_multi_pay { Method::MultiPayInvoice } else { Method::PayInvoice }; - let invoice: Bolt11Invoice = match check_valid_nwc_invoice(¶ms, node).await { + let invoice: Bolt11Invoice = match check_valid_nwc_invoice(&request.params, node).await { Ok(Some(invoice)) => invoice, Ok(None) => return Ok(None), Err(err_string) => { @@ -918,7 +959,7 @@ impl NostrWalletConnect { event.id, event.pubkey, invoice, - params.id.clone(), + request.params.id.clone(), ) .await? } @@ -971,7 +1012,8 @@ impl NostrWalletConnect { nostr_manager.save_nwc_profile(self.clone())?; } - let response_event = self.build_nwc_response_event(event, content, params.id)?; + let response_event = + self.build_nwc_response_event(event, content, request.params.id)?; Ok(Some(response_event)) } // if we need approval, just save in the db for later @@ -981,7 +1023,7 @@ impl NostrWalletConnect { event.id, event.pubkey, invoice, - params.id, + request.params.id, ) .await?; @@ -1005,7 +1047,7 @@ impl NostrWalletConnect { event.id, event.pubkey, invoice, - params.id.clone(), + request.params.id.clone(), ) .await?; Response { @@ -1074,7 +1116,7 @@ impl NostrWalletConnect { event.id, event.pubkey, invoice, - params.id.clone(), + request.params.id.clone(), ) .await? } @@ -1093,7 +1135,8 @@ impl NostrWalletConnect { } }; - let response_event = self.build_nwc_response_event(event, content, params.id)?; + let response_event = + self.build_nwc_response_event(event, content, request.params.id)?; Ok(Some(response_event)) } } @@ -1108,15 +1151,19 @@ impl NostrWalletConnect { needs_delete: &mut bool, needs_save: &mut bool, ) -> anyhow::Result> { + let pay_invoice_request = PayInvoiceRequest { + params, + is_multi_pay: false, + }; + match self .handle_nwc_invoice_payment( event.clone(), node, nostr_manager, - params.clone(), + pay_invoice_request, needs_delete, needs_save, - false, ) .await { @@ -1147,15 +1194,18 @@ impl NostrWalletConnect { let mut response_events: Vec = Vec::with_capacity(params.invoices.len()); for param in params.invoices { + let pay_invoice_request = PayInvoiceRequest { + params: param, + is_multi_pay: true, + }; match self .handle_nwc_invoice_payment( event.clone(), node, nostr_manager, - param.clone(), + pay_invoice_request, needs_delete, needs_save, - true, ) .await { @@ -1976,6 +2026,13 @@ mod wasm_test { ) .unwrap(); + let event_id = EventId::all_zeros(); + nostr_manager + .client + .expect_send_event() + .times(7) + .returning(move |_| Ok(event_id)); + let secp = Secp256k1::new(); let mut nwc = NostrWalletConnect::new(&secp, xprivkey, profile.profile()).unwrap(); let uri = nwc.get_nwc_uri().unwrap().unwrap(); @@ -2006,12 +2063,6 @@ mod wasm_test { .unwrap() }; - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; if let NwcResponse::SingleEvent(event) = result.unwrap().unwrap() { check_nwc_error_response( @@ -2044,12 +2095,6 @@ mod wasm_test { .unwrap() }; - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; if let NwcResponse::SingleEvent(event) = result.unwrap().unwrap() { check_nwc_error_response( @@ -2065,12 +2110,6 @@ mod wasm_test { // test invalid invoice let event = create_nwc_request(&uri, "invalid invoice".to_string()); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; if let NwcResponse::SingleEvent(event) = result.unwrap().unwrap() { check_nwc_error_response( @@ -2086,12 +2125,6 @@ mod wasm_test { // test expired invoice let event = create_nwc_request(&uri, INVOICE.to_string()); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; if let NwcResponse::SingleEvent(event) = result.unwrap().unwrap() { check_nwc_error_response( @@ -2108,12 +2141,6 @@ mod wasm_test { // test amount-less invoice let (invoice, _) = create_dummy_invoice(None, Network::Regtest, None); let event = create_nwc_request(&uri, invoice.to_string()); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; if let NwcResponse::SingleEvent(event) = result.unwrap().unwrap() { check_nwc_error_response( @@ -2136,12 +2163,6 @@ mod wasm_test { .0 .to_string(); let event = create_nwc_request(&uri, invoice); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; if let NwcResponse::SingleEvent(event) = result.unwrap().unwrap() { check_nwc_error_response( @@ -2208,12 +2229,6 @@ mod wasm_test { // test keysend payment - require approval let event = create_pay_keysend_nwc_request(&uri, 1_000, "dummy".to_string()); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc.handle_nwc_request(event, &node, &nostr_manager).await; if let NwcResponse::SingleEvent(event) = result.unwrap().unwrap() { check_nwc_error_response( @@ -2349,15 +2364,16 @@ mod wasm_test { let mut nwc = NostrWalletConnect::new(&secp, xprivkey, profile.profile()).unwrap(); let uri = nwc.get_nwc_uri().unwrap().unwrap(); - // test failed payment goes to pending, we have no channels so it will fail - let (invoice, _) = create_dummy_invoice(Some(10), Network::Regtest, None); - let event = create_nwc_request(&uri, invoice.to_string()); let event_id = EventId::all_zeros(); nostr_manager .client .expect_send_event() - .once() + .times(5) .returning(move |_| Ok(event_id)); + + // test failed payment goes to pending, we have no channels so it will fail + let (invoice, _) = create_dummy_invoice(Some(10), Network::Regtest, None); + let event = create_nwc_request(&uri, invoice.to_string()); let result = nwc .handle_nwc_request(event.clone(), &mw, &nostr_manager) .await; @@ -2375,12 +2391,6 @@ mod wasm_test { // test over budget payment goes to pending let (invoice, _) = create_dummy_invoice(Some(budget + 1), Network::Regtest, None); let event = create_nwc_request(&uri, invoice.to_string()); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc .handle_nwc_request(event.clone(), &mw, &nostr_manager) .await; @@ -2404,8 +2414,8 @@ mod wasm_test { let uri = nwc.get_nwc_uri().unwrap().unwrap(); // test multi invoice over budget after 1st invoice. 2nd goes to pending - let (invoice_1, preimage_1) = create_dummy_invoice(Some(8000_000), Network::Regtest, None); - let (invoice_2, _) = create_dummy_invoice(Some(4000_000), Network::Regtest, None); + let (invoice_1, preimage_1) = create_dummy_invoice(Some(8_000_000), Network::Regtest, None); + let (invoice_2, _) = create_dummy_invoice(Some(4_000_000), Network::Regtest, None); let invoices = vec![invoice_1.to_string(), invoice_2.to_string()]; node.expect_pay_invoice() .once() @@ -2419,12 +2429,6 @@ mod wasm_test { }); let event = create_multi_invoice_nwc_request(&uri, invoices); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .times(2) - .returning(move |_| Ok(event_id)); let result = nwc .handle_nwc_request(event.clone(), &node, &nostr_manager) .await; @@ -2446,12 +2450,6 @@ mod wasm_test { let event = create_pay_keysend_nwc_request(&uri, budget * 1000 + 1_000, pubkey.to_string()); let mut node = MockInvoiceHandler::new(); node.expect_lookup_payment().return_const(None); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc.handle_nwc_request(event, &mw, &nostr_manager).await; if let NwcResponse::SingleEvent(event) = result.unwrap().unwrap() { check_nwc_error_response( @@ -2502,6 +2500,13 @@ mod wasm_test { .await .unwrap(); + let event_id = EventId::all_zeros(); + nostr_manager + .client + .expect_send_event() + .times(2) + .returning(move |_| Ok(event_id)); + let budget = 10_000; let profile = nostr_manager .create_new_nwc_profile_internal( @@ -2525,12 +2530,6 @@ mod wasm_test { // test successful payment let event = create_nwc_request(&uri, invoice.to_string()); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc .handle_nwc_request(event.clone(), &node, &nostr_manager) .await; @@ -2586,7 +2585,7 @@ mod wasm_test { description: None, payment_hash, preimage: Some(payment_preimage), - payee_pubkey: Some(pubkey.clone()), + payee_pubkey: Some(pubkey), amount_sats: None, expire: 0, status: HTLCStatus::Succeeded, @@ -2601,11 +2600,6 @@ mod wasm_test { let keysend_amount_msat = 5_000; let event = create_pay_keysend_nwc_request(&uri, keysend_amount_msat, pubkey.to_string()); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc .handle_nwc_request(event.clone(), &node, &nostr_manager) .await; @@ -3011,6 +3005,13 @@ mod wasm_test { ) .unwrap(); + let event_id = EventId::all_zeros(); + nostr_manager + .client + .expect_send_event() + .times(2) + .returning(move |_| Ok(event_id)); + let secp = Secp256k1::new(); let mut nwc = NostrWalletConnect::new(&secp, xprivkey, profile.profile()).unwrap(); let uri = nwc.get_nwc_uri().unwrap().unwrap(); @@ -3025,12 +3026,6 @@ mod wasm_test { bolt11: Some("lntbs1m1pjrmuu3pp52hk0j956d7s8azaps87amadshnrcvqtkvk06y2nue2w69g6e5vasdqqcqzpgxqyz5vqsp5wu3py6257pa3yzarw0et2200c08r5fu6k3u94yfwmlnc8skdkc9s9qyyssqc783940p82c64qq9pu3xczt4tdxzex9wpjn54486y866aayft2cxxusl9eags4cs3kcmuqdrvhvs0gudpj5r2a6awu4wcq29crpesjcqhdju55".to_string()), }), ); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc .handle_nwc_request(event.clone(), &node, &nostr_manager) .await; @@ -3061,12 +3056,6 @@ mod wasm_test { bolt11: Some(invoice.to_string()), }), ); - let event_id = EventId::all_zeros(); - nostr_manager - .client - .expect_send_event() - .once() - .returning(move |_| Ok(event_id)); let result = nwc .handle_nwc_request(event.clone(), &node, &nostr_manager) .await; @@ -3111,4 +3100,168 @@ mod wasm_test { .await; assert!(result.is_err()); } + + #[test] + async fn test_list_transactions() { + let storage = MemoryStorage::default(); + + let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); + let stop = Arc::new(AtomicBool::new(false)); + let mut nostr_manager = NostrManager::from_mnemonic( + xprivkey, + NostrKeySource::Derived, + storage.clone(), + MockPrimalApi::new(), + get_mock_nostr_client(), + Arc::new(MutinyLogger::default()), + stop, + ) + .await + .unwrap(); + + let profile = nostr_manager + .create_new_nwc_profile_internal( + ProfileType::Normal { + name: "test".to_string(), + }, + SpendingConditions::RequireApproval, + NwcProfileTag::General, + vec![Method::ListTransactions], + ) + .unwrap(); + + let event_id = EventId::all_zeros(); + nostr_manager + .client + .expect_send_event() + .times(3) + .returning(move |_| Ok(event_id)); + + let secp = Secp256k1::new(); + let mut nwc = NostrWalletConnect::new(&secp, xprivkey, profile.profile()).unwrap(); + let uri = nwc.get_nwc_uri().unwrap().unwrap(); + + let invoice_1: MutinyInvoice = create_dummy_invoice(Some(69696969), Network::Regtest, None) + .0 + .clone() + .into(); + let invoice_2: MutinyInvoice = create_dummy_invoice(Some(42_000), Network::Regtest, None) + .0 + .clone() + .into(); + let invoice_3: MutinyInvoice = create_dummy_invoice(Some(42069), Network::Regtest, None) + .0 + .clone() + .into(); + let mut invoice_4: MutinyInvoice = + create_dummy_invoice(Some(21_000), Network::Regtest, None) + .0 + .clone() + .into(); + invoice_4.status = HTLCStatus::Succeeded; + + let invoices: Vec = vec![ + invoice_1.clone(), + invoice_2.clone(), + invoice_3.clone(), + invoice_4.clone(), + ]; + + let mut node = MockInvoiceHandler::new(); + node.expect_get_payments_by_label() + .times(3) + .returning(move |_| Ok(invoices.clone())); + + // only paid ones + let time = utils::now().as_secs() + 2000000; + let event = sign_nwc_request( + &uri, + Request::list_transactions(ListTransactionsRequestParams { + from: None, + until: Some(time), + limit: None, + offset: None, + unpaid: None, + transaction_type: None, + }), + ); + + let result = nwc + .handle_nwc_request(event.clone(), &node, &nostr_manager) + .await; + + let event = if let NwcResponse::SingleEvent(nwc_event) = result.unwrap().unwrap() { + nwc_event + } else { + panic!("invalid nwc response") + }; + let content = decrypt(&uri.secret, &event.pubkey, &event.content).unwrap(); + let response: Response = Response::from_json(content).unwrap(); + let result = response.to_list_transactions().unwrap(); + + assert_eq!(result.len(), 1); + assert_eq!(result[0].amount, 21_000); + assert_eq!( + result[0].invoice, + Some(invoice_4.bolt11.unwrap().to_string()) + ); + + // with limit + let time = utils::now().as_secs() + 2000000; + let event = sign_nwc_request( + &uri, + Request::list_transactions(ListTransactionsRequestParams { + from: None, + until: Some(time), + limit: Some(2), + offset: None, + unpaid: Some(true), + transaction_type: None, + }), + ); + + let result = nwc + .handle_nwc_request(event.clone(), &node, &nostr_manager) + .await; + + let event = if let NwcResponse::SingleEvent(nwc_event) = result.unwrap().unwrap() { + nwc_event + } else { + panic!("invalid nwc response") + }; + let content = decrypt(&uri.secret, &event.pubkey, &event.content).unwrap(); + let response: Response = Response::from_json(content).unwrap(); + let result = response.to_list_transactions().unwrap(); + + assert_eq!(result.len(), 2); + + // outgoing + let time = utils::now().as_secs() + 2000000; + let event = sign_nwc_request( + &uri, + Request::list_transactions(ListTransactionsRequestParams { + from: None, + until: Some(time), + limit: None, + offset: None, + unpaid: None, + transaction_type: Some(TransactionType::Outgoing), + }), + ); + + let result = nwc + .handle_nwc_request(event.clone(), &node, &nostr_manager) + .await; + + let event = if let NwcResponse::SingleEvent(nwc_event) = result.unwrap().unwrap() { + nwc_event + } else { + panic!("invalid nwc response") + }; + let content = decrypt(&uri.secret, &event.pubkey, &event.content).unwrap(); + let response: Response = Response::from_json(content).unwrap(); + let result = response.to_list_transactions().unwrap(); + + assert_eq!(result.len(), 0); + } }