From 0e5fb6857e7287432194d8d4afc22e8bed722bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Fri, 23 Feb 2024 12:28:15 +0000 Subject: [PATCH] Implement payment personal notes --- examples/node/cli.rs | 20 ++++++++++++++ mock/breez-sdk/src/lib.rs | 4 +++ src/activity.rs | 24 +++++++++++++++++ src/lib.rs | 55 +++++++++++++++++++++++++++++++++++++++ src/lipalightninglib.udl | 4 +++ 5 files changed, 107 insertions(+) diff --git a/examples/node/cli.rs b/examples/node/cli.rs index a4357785..c5f0b94e 100644 --- a/examples/node/cli.rs +++ b/examples/node/cli.rs @@ -196,6 +196,11 @@ pub(crate) fn poll_for_user_input(node: &LightningNode, log_file_path: &str) { Ok(uuid) => println!("{uuid}"), Err(message) => eprintln!("{}", format!("{message:#}").red()), }, + "personalnote" => { + if let Err(message) = set_personal_note(node, &mut words) { + println!("{}", format!("{message:#}").red()); + } + } "swaponchaintolightning" => { if let Err(message) = swap_onchain_to_lightning(node) { println!("{}", format!("{message:#}").red()); @@ -366,6 +371,10 @@ fn setup_editor(history_path: &Path) -> Editor { "paymentuuid ", "paymentuuid ", )); + hints.insert(CommandHint::new( + "personalnote [note]", + "personalnote ", + )); hints.insert(CommandHint::new("sweep
", "sweep ")); hints.insert(CommandHint::new("clearwalletinfo", "clearwalletinfo")); hints.insert(CommandHint::new("clearwallet
", "clearwallet ")); @@ -428,6 +437,7 @@ fn help() { println!(" l | listactivities [number of activities = 2]"); println!(" listlightningaddresses"); println!(" paymentuuid "); + println!(" personalnote [note]"); println!(); println!(" getchannelcloseresolvingfees"); println!(" sweep
"); @@ -1135,6 +1145,7 @@ fn print_payment(payment: Payment) -> Result<()> { println!(" Offer: {}", offer_to_string(payment.offer)); println!(" Swap: {:?}", payment.swap); println!(" Recipient: {:?}", payment.recipient); + println!(" Personal note: {:?}", payment.personal_note); Ok(()) } @@ -1164,6 +1175,15 @@ fn payment_uuid(node: &LightningNode, words: &mut dyn Iterator) -> Ok(node.get_payment_uuid(payment_hash.to_string())?) } +fn set_personal_note(node: &LightningNode, words: &mut dyn Iterator) -> Result<()> { + let payment_hash = words.next().ok_or(anyhow!("Payment Hash is required"))?; + let note = words.collect::>().join(" "); + + node.set_payment_personal_note(payment_hash.to_string(), note.to_string())?; + + Ok(()) +} + fn sweep(node: &LightningNode, address: String) -> Result { let fee_rate = node.query_onchain_fee_rate()?; let sweep_info = node.prepare_sweep(address, fee_rate)?; diff --git a/mock/breez-sdk/src/lib.rs b/mock/breez-sdk/src/lib.rs index f4586909..6b6a93fb 100644 --- a/mock/breez-sdk/src/lib.rs +++ b/mock/breez-sdk/src/lib.rs @@ -581,4 +581,8 @@ impl BreezServices { pub async fn register_webhook(&self, _webhook_url: String) -> SdkResult<()> { Ok(()) } + + pub async fn set_payment_metadata(&self, _hash: String, _metadata: String) -> SdkResult<()> { + todo!("set_payment_metadata"); + } } diff --git a/src/activity.rs b/src/activity.rs index 9b43f7f6..e02191dc 100644 --- a/src/activity.rs +++ b/src/activity.rs @@ -2,6 +2,7 @@ use crate::{Amount, InvoiceDetails, OfferKind, PayErrorCode, SwapInfo, TzTime}; use std::time::SystemTime; use breez_sdk_core::{LnPaymentDetails, PaymentStatus}; +use serde::{Deserialize, Serialize}; #[derive(PartialEq, Eq, Debug, Clone)] #[repr(u8)] @@ -68,6 +69,8 @@ pub struct Payment { pub swap: Option, /// Information about a payment's recipient. Will only be present for outgoing payments. pub recipient: Option, + /// A personal note previously added to this payment through [`LightningNode::set_payment_personal_note`](crate::LightningNode::set_payment_personal_note) + pub personal_note: Option, } /// User-friendly representation of an outgoing payment's recipient. @@ -149,3 +152,24 @@ pub enum ChannelCloseState { Pending, Confirmed, } + +#[derive(Serialize, Deserialize, PartialEq, Debug)] +pub(crate) struct BreezPaymentMetadata { + pub personal_note: Option, +} + +#[cfg(test)] +mod tests { + use crate::activity::BreezPaymentMetadata; + + #[test] + fn test_payment_metadata_serde() { + let metadata = BreezPaymentMetadata { + personal_note: None, + }; + + let json = "{}"; + + assert_eq!(metadata, serde_json::from_str(json).unwrap()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9d2322d0..85f42f09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,7 @@ use crate::util::{ }; pub use notification_handling::{handle_notification, Notification, RecommendedAction}; +use crate::activity::BreezPaymentMetadata; use crate::symmetric_encryption::encrypt; pub use breez_sdk_core::error::ReceiveOnchainError as SwapError; use breez_sdk_core::error::{LnUrlWithdrawError, ReceiveOnchainError, SendPaymentError}; @@ -1035,6 +1036,49 @@ impl LightningNode { } } + /// Set a personal note on a specific payment. + /// + /// Parameters: + /// * `payment_hash` - The hash of the payment for which a personal note will be set. + /// * `note` - The personal note. + pub fn set_payment_personal_note(&self, payment_hash: String, note: String) -> Result<()> { + let note = Some(note.trim().to_string()).filter(|s| !s.is_empty()); + + let previous_metadata = self + .rt + .handle() + .block_on(self.sdk.payment_by_hash(payment_hash.clone())) + .map_to_runtime_error( + RuntimeErrorCode::NodeUnavailable, + "Failed to fetch payment by hash", + )? + .ok_or_invalid_input("Payment not found")? + .metadata; + + let new_metadata = match previous_metadata { + None => BreezPaymentMetadata { + personal_note: note, + }, + Some(m) => { + let mut m: BreezPaymentMetadata = serde_json::from_str(&m) + .map_to_permanent_failure("Payment metadata got corrupted")?; + m.personal_note = note; + m + } + }; + let new_metadata = serde_json::to_string(&new_metadata) + .map_to_permanent_failure("Failed to serialize BreezPaymentMetadata")?; + + self.rt + .handle() + .block_on(self.sdk.set_payment_metadata(payment_hash, new_metadata)) + .map_to_runtime_error( + RuntimeErrorCode::NodeUnavailable, + "Failed to set payment metadata", + )?; + Ok(()) + } + fn activity_from_breez_payment( &self, breez_payment: breez_sdk_core::Payment, @@ -1189,6 +1233,12 @@ impl LightningNode { _ => description, }; + let payment_metadata: Option = breez_payment + .metadata + .as_ref() + .and_then(|m| serde_json::from_str(m).unwrap_or(None)); + let personal_note = payment_metadata.and_then(|p| p.personal_note); + Ok(Activity::PaymentActivity { payment: Payment { payment_type, @@ -1210,6 +1260,7 @@ impl LightningNode { offer, swap, recipient, + personal_note, }, }) } @@ -1329,6 +1380,7 @@ impl LightningNode { offer: None, swap: None, recipient: None, + personal_note: None, }) } @@ -2599,6 +2651,7 @@ mod tests { offer: None, swap: None, recipient: None, + personal_note: None, }, Payment { payment_type: PaymentType::Receiving, @@ -2648,6 +2701,7 @@ mod tests { }), swap: None, recipient: None, + personal_note: None, }, Payment { payment_type: PaymentType::Receiving, @@ -2697,6 +2751,7 @@ mod tests { }), swap: None, recipient: None, + personal_note: None, }, ]; diff --git a/src/lipalightninglib.udl b/src/lipalightninglib.udl index 13fee30d..3b26a455 100644 --- a/src/lipalightninglib.udl +++ b/src/lipalightninglib.udl @@ -44,6 +44,9 @@ interface LightningNode { [Throws=LnError] Payment get_payment(string hash); + [Throws=LnError] + void set_payment_personal_note(string payment_hash, string note); + [Throws=LnError] sequence list_lightning_addresses(); @@ -332,6 +335,7 @@ dictionary Payment { OfferKind? offer; SwapInfo? swap; Recipient? recipient; + string? personal_note; }; enum PaymentType {