diff --git a/lightspark/src/client.rs b/lightspark/src/client.rs index 0f26cbb..559ebd9 100644 --- a/lightspark/src/client.rs +++ b/lightspark/src/client.rs @@ -5,6 +5,7 @@ use std::str::FromStr; use bitcoin::bip32::{DerivationPath, ExtendedPrivKey}; use bitcoin::secp256k1::Secp256k1; +use chrono::{DateTime, Datelike, Utc}; use serde_json::Value; use sha2::{Digest, Sha256}; @@ -866,6 +867,26 @@ impl LightsparkClient { amount_msats: i64, metadata: &str, expiry_secs: Option, + ) -> Result { + self.create_uma_invoice_with_receiver_identifier( + node_id, + amount_msats, + metadata, + expiry_secs, + None, + None, + ) + .await + } + + pub async fn create_uma_invoice_with_receiver_identifier( + &self, + node_id: &str, + amount_msats: i64, + metadata: &str, + expiry_secs: Option, + signing_private_key: Option<&[u8]>, + receiver_identifier: Option<&str>, ) -> Result { let mutation = format!( "mutation CreateUmaInvoice( @@ -873,12 +894,14 @@ impl LightsparkClient { $amount_msats: Long! $metadata_hash: String! $expiry_secs: Int + $receiver_hash: String = null ) {{ create_uma_invoice(input: {{ node_id: $node_id amount_msats: $amount_msats metadata_hash: $metadata_hash expiry_secs: $expiry_secs + receiver_hash: $receiver_hash }}) {{ invoice {{ ...InvoiceFragment @@ -891,6 +914,21 @@ impl LightsparkClient { invoice::FRAGMENT ); + let receiver_hash = if let Some(receiver_identifier) = receiver_identifier { + if signing_private_key.is_none() { + return Err(Error::InvalidArgumentError( + "receiver identifier provided without signing private key".to_owned(), + )); + } + Some(Self::hash_uma_identifier( + receiver_identifier, + signing_private_key.unwrap(), + chrono::Utc::now(), + )) + } else { + None + }; + let mut hasher = Sha256::new(); hasher.update(metadata.as_bytes()); @@ -903,6 +941,9 @@ impl LightsparkClient { if let Some(expiry_secs) = expiry_secs { variables.insert("expiry_secs", expiry_secs.into()); } + if let Some(receiver_hash) = receiver_hash { + variables.insert("receiver_hash", receiver_hash.into()); + } let value = serde_json::to_value(variables).map_err(Error::ConversionError)?; let json = self @@ -922,6 +963,28 @@ impl LightsparkClient { timeout_secs: i32, maximum_fees_msats: i64, amount_msats: Option, + ) -> Result { + self.pay_uma_invoice_with_sender_identifier( + node_id, + encoded_invoice, + timeout_secs, + maximum_fees_msats, + amount_msats, + None, + None, + ) + .await + } + + pub async fn pay_uma_invoice_with_sender_identifier( + &self, + node_id: &str, + encoded_invoice: &str, + timeout_secs: i32, + maximum_fees_msats: i64, + amount_msats: Option, + signing_private_key: Option<&[u8]>, + sender_identifier: Option<&str>, ) -> Result { let operation = format!( "mutation PayUmaInvoice( @@ -930,6 +993,7 @@ impl LightsparkClient { $timeout_secs: Int! $maximum_fees_msats: Long! $amount_msats: Long + $sender_hash: String = null ) {{ pay_uma_invoice(input: {{ node_id: $node_id @@ -937,6 +1001,7 @@ impl LightsparkClient { timeout_secs: $timeout_secs maximum_fees_msats: $maximum_fees_msats amount_msats: $amount_msats + sender_hash: $sender_hash }}) {{ payment {{ ...OutgoingPaymentFragment @@ -949,6 +1014,21 @@ impl LightsparkClient { outgoing_payment::FRAGMENT ); + let sender_hash = if let Some(sender_identifier) = sender_identifier { + if signing_private_key.is_none() { + return Err(Error::InvalidArgumentError( + "sender identifier provided without signing private key".to_owned(), + )); + } + Some(Self::hash_uma_identifier( + sender_identifier, + signing_private_key.unwrap(), + chrono::Utc::now(), + )) + } else { + None + }; + let mut variables: HashMap<&str, Value> = HashMap::new(); variables.insert("node_id", node_id.into()); variables.insert("encoded_invoice", encoded_invoice.into()); @@ -957,6 +1037,9 @@ impl LightsparkClient { } variables.insert("timeout_secs", timeout_secs.into()); variables.insert("maximum_fees_msats", maximum_fees_msats.into()); + if let Some(sender_hash) = sender_hash { + variables.insert("sender_hash", sender_hash.into()); + } let value = serde_json::to_value(variables).map_err(Error::ConversionError)?; @@ -1214,6 +1297,23 @@ impl LightsparkClient { Ok(result) } + pub fn hash_uma_identifier( + identifier: &str, + signing_private_key: &[u8], + now: DateTime, + ) -> String { + let input_data = format!( + "{}{}-{}{}", + identifier, + now.month(), + now.year(), + hex::encode(signing_private_key) + ); + let mut hasher = Sha256::new(); + hasher.update(input_data.as_bytes()); + hex::encode(hasher.finalize()) + } + fn hash_phone_number(phone_number_e164: &str) -> Result { let e164_regex = regex::Regex::new(r"^\+[1-9]\d{1,14}$").unwrap(); if !e164_regex.is_match(phone_number_e164) { @@ -1224,3 +1324,36 @@ impl LightsparkClient { Ok(hex::encode(hasher.finalize())) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::Secp256k1SigningKey; + use chrono::prelude::*; + + #[test] + fn test_hash_uma_identifier() { + let signing_key = "xyz".as_bytes(); + let mock_time_jan = Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(); + let mock_time_feb = Utc.with_ymd_and_hms(2021, 2, 1, 0, 0, 0).unwrap(); + + let hashed_uma = LightsparkClient::::hash_uma_identifier( + "user@domain.com", + signing_key, + mock_time_jan, + ); + let hashed_uma_same_month = LightsparkClient::::hash_uma_identifier( + "user@domain.com", + signing_key, + mock_time_jan, + ); + assert_eq!(hashed_uma, hashed_uma_same_month); + + let hashed_uma_diff_month = LightsparkClient::::hash_uma_identifier( + "user@domain.com", + signing_key, + mock_time_feb, + ); + assert_ne!(hashed_uma, hashed_uma_diff_month); + } +} diff --git a/lightspark/src/error.rs b/lightspark/src/error.rs index 9c9708d..22567a0 100644 --- a/lightspark/src/error.rs +++ b/lightspark/src/error.rs @@ -17,6 +17,7 @@ pub enum Error { SigningKeyNotFound, InvalidCurrencyConversion, InvalidPhoneNumber, + InvalidArgumentError(String), } impl fmt::Display for Error { @@ -35,6 +36,7 @@ impl fmt::Display for Error { Self::SigningKeyNotFound => write!(f, "Signing key not found"), Self::InvalidCurrencyConversion => write!(f, "Invalid currency conversion"), Self::InvalidPhoneNumber => write!(f, "Invalid phone number. Must be E.164 format."), + Self::InvalidArgumentError(err) => write!(f, "Invalid argument error {}", err), } } }