use crate::amount::AsSats;
use crate::async_runtime::Handle;
use crate::errors::Result;
use crate::key_derivation::derive_analytics_key;
use crate::locker::Locker;
use crate::util::{unix_timestamp_to_system_time, LogIgnoreError};
use crate::{ExchangeRate, InvoiceDetails, UserPreferences};
use breez_sdk_core::{
InvoicePaidDetails, Payment, PaymentDetails, PaymentFailedData, ReceivePaymentResponse,
};
use log::{error, info, warn, Level};
use num_enum::TryFromPrimitive;
use parrot::{AnalyticsClient, AnalyticsEvent, PayFailureReason, PaymentSource};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use uuid::Uuid;
#[derive(Debug, PartialEq, TryFromPrimitive, Clone)]
#[repr(u8)]
pub enum AnalyticsConfig {
Enabled,
Disabled,
}
pub(crate) struct AnalyticsInterceptor {
pub analytics_client: Arc<AnalyticsClient>,
pub user_preferences: Arc<Mutex<UserPreferences>>,
pub rt_handle: Handle,
pub config: Mutex<AnalyticsConfig>,
}
pub struct PaymentMetadata {
pub source: PaymentSource,
pub process_started_at: SystemTime,
}
pub struct InvoiceCreationMetadata {
pub request_currency: String,
}
impl AnalyticsInterceptor {
pub fn new(
analytics_client: AnalyticsClient,
user_preferences: Arc<Mutex<UserPreferences>>,
rt_handle: Handle,
analytics_config: AnalyticsConfig,
) -> Self {
Self {
analytics_client: Arc::new(analytics_client),
user_preferences,
rt_handle,
config: Mutex::new(analytics_config),
}
}
pub fn pay_initiated(
&self,
invoice_details: InvoiceDetails,
metadata: PaymentMetadata,
paid_amount: Option<u64>,
exchange_rate: Option<ExchangeRate>,
) {
if *self.config.lock_unwrap() == AnalyticsConfig::Disabled {
return;
}
let invoice_amount = invoice_details.amount.map(|a| a.sats.as_sats().msats);
let paid_amount_msat = match paid_amount.or(invoice_amount) {
Some(a) => a,
None => {
error!(
"Couldn't retrieve invoice amount of initiated payment: {}",
invoice_details.payment_hash
);
return;
}
};
let user_currency = self.user_preferences.lock_unwrap().fiat_currency.clone();
let analytics_client = Arc::clone(&self.analytics_client);
self.rt_handle.spawn(async move {
analytics_client
.report_event(AnalyticsEvent::PayInitiated {
payment_hash: invoice_details.payment_hash,
paid_amount_msat,
requested_amount_msat: invoice_amount,
sats_per_user_currency: exchange_rate.map(|e| e.rate),
source: metadata.source,
user_currency,
process_started_at: metadata.process_started_at,
executed_at: SystemTime::now(),
})
.await
.log_ignore_error(Level::Warn, "Failed to report an analytics event")
});
}
pub fn pay_succeeded(&self, payment: Payment) {
if *self.config.lock_unwrap() == AnalyticsConfig::Disabled {
return;
}
if let PaymentDetails::Ln { data } = payment.details {
let analytics_client = Arc::clone(&self.analytics_client);
self.rt_handle.spawn(async move {
analytics_client
.report_event(AnalyticsEvent::PaySucceeded {
payment_hash: data.payment_hash,
ln_fees_paid_msat: payment.fee_msat,
confirmed_at: unix_timestamp_to_system_time(
payment.payment_time.unsigned_abs(),
),
})
.await
.log_ignore_error(Level::Warn, "Failed to report an analytics event")
});
}
}
pub fn pay_failed(&self, failed_data: PaymentFailedData) {
if *self.config.lock_unwrap() == AnalyticsConfig::Disabled {
return;
}
if failed_data.invoice.is_none() {
info!("Payment failed without invoice, not reporting");
return;
}
let analytics_client = Arc::clone(&self.analytics_client);
self.rt_handle.spawn(async move {
analytics_client
.report_event(AnalyticsEvent::PayFailed {
payment_hash: failed_data.invoice.unwrap().payment_hash,
reason: map_error_to_failure_reason(failed_data.error),
failed_at: SystemTime::now(),
})
.await
.log_ignore_error(Level::Warn, "Failed to report an analytics event")
});
}
pub fn request_initiated(
&self,
receive_response: ReceivePaymentResponse,
exchange_rate: Option<ExchangeRate>,
metadata: InvoiceCreationMetadata,
) {
if *self.config.lock_unwrap() == AnalyticsConfig::Disabled {
return;
}
let analytics_client = Arc::clone(&self.analytics_client);
let user_currency = self.user_preferences.lock_unwrap().fiat_currency.clone();
self.rt_handle.spawn(async move {
analytics_client
.report_event(AnalyticsEvent::RequestInitiated {
payment_hash: receive_response.ln_invoice.payment_hash,
entered_amount_msat: receive_response.ln_invoice.amount_msat,
sats_per_user_currency: exchange_rate.map(|e| e.rate),
user_currency,
request_currency: metadata.request_currency,
created_at: SystemTime::now(),
})
.await
.log_ignore_error(Level::Warn, "Failed to report an analytics event")
});
}
pub fn request_succeeded(&self, paid_details: InvoicePaidDetails) {
if *self.config.lock_unwrap() == AnalyticsConfig::Disabled {
return;
}
let analytics_client = Arc::clone(&self.analytics_client);
let payment = if let Some(payment) = paid_details.payment {
payment
} else {
warn!(
"Request succeeded without payment data available: {}",
paid_details.bolt11
);
return;
};
self.rt_handle.spawn(async move {
analytics_client
.report_event(AnalyticsEvent::RequestSucceeded {
payment_hash: paid_details.payment_hash,
paid_amount_msat: payment.amount_msat,
channel_opening_fee_msat: payment.fee_msat,
received_at: SystemTime::now(),
})
.await
.log_ignore_error(Level::Warn, "Failed to report an analytics event")
});
}
}
pub(crate) fn derive_analytics_keys(seed: &[u8; 64]) -> Result<String> {
let key = derive_analytics_key(seed)?;
Ok(Uuid::new_v5(&Uuid::NAMESPACE_OID, &key)
.hyphenated()
.to_string())
}
fn map_error_to_failure_reason(error: String) -> PayFailureReason {
if error.starts_with("Route not found:") {
return PayFailureReason::NoRoute;
}
PayFailureReason::Unknown
}