From 2b39f2595fc3caba9435decab397ae734835966b Mon Sep 17 00:00:00 2001 From: Dominic Leutenegger Date: Wed, 8 Nov 2023 15:51:28 +0100 Subject: [PATCH 1/3] Add parrot analytics library --- Cargo.toml | 1 + README.md | 4 + graphql/schemas/operations.graphql | 4 + graphql/src/lib.rs | 12 ++ graphql/src/schema.rs | 10 +- parrot/Cargo.toml | 13 ++ parrot/src/lib.rs | 220 +++++++++++++++++++++++++++++ 7 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 parrot/Cargo.toml create mode 100644 parrot/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 955063e..75118d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,5 @@ members = [ "honey-badger", "mole", "squirrel", + "parrot", ] diff --git a/README.md b/README.md index b0d9e76..8f964ff 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,7 @@ The library allows to register for and list withdraw collect offers (e.g. Lightn ## Squirrel Squirrels love to store nuts for the winter. The library allows backups of local data to be created. + +## Parrot +This feathered companion, similar to its namesakes, repeats what he hears and as such delivers, +important analytics data about payments to the backend. This data is pseudonymized and used to improve our services. diff --git a/graphql/schemas/operations.graphql b/graphql/schemas/operations.graphql index 3763e8e..71e0db2 100644 --- a/graphql/schemas/operations.graphql +++ b/graphql/schemas/operations.graphql @@ -186,3 +186,7 @@ mutation RecoverBackup($schemaName: String!) { updatedAt } } + +mutation ReportPaymentTelemetry($telemetryId: String!, $events: PaymentTelemetryEventsInput) { + report_payment_telemetry(telemetryId: $telemetryId, events: $events) +} diff --git a/graphql/src/lib.rs b/graphql/src/lib.rs index c263379..af0962d 100644 --- a/graphql/src/lib.rs +++ b/graphql/src/lib.rs @@ -7,6 +7,7 @@ pub use perro; pub use reqwest; use graphql_client::reqwest::{post_graphql, post_graphql_blocking}; +use chrono::{DateTime, Utc}; use graphql_client::Response; use perro::{permanent_failure, runtime_error, MapToError, OptionToError}; use reqwest::blocking::Client; @@ -202,3 +203,14 @@ mod tests { assert_eq!(timestamp, 1695314361 + 2 * 3600); } } + +pub trait ToRfc3339 { + fn to_rfc3339(&self) -> String; +} + +impl ToRfc3339 for SystemTime { + fn to_rfc3339(&self) -> String { + let datetime: DateTime = DateTime::from(*self); + datetime.to_rfc3339() + } +} diff --git a/graphql/src/schema.rs b/graphql/src/schema.rs index 6cfa435..e4d97b2 100644 --- a/graphql/src/schema.rs +++ b/graphql/src/schema.rs @@ -165,8 +165,6 @@ pub struct GetLatestChannelManager; #[allow(non_camel_case_types)] type bigint = u64; type BigInteger = bigint; -#[allow(non_camel_case_types)] -type float8 = f64; #[derive(GraphQLQuery)] #[graphql( @@ -207,3 +205,11 @@ pub struct CreateBackup; response_derives = "Debug" )] pub struct RecoverBackup; + +#[derive(GraphQLQuery)] +#[graphql( +schema_path = "schemas/schema_wallet_read.graphql", +query_path = "schemas/operations.graphql", +response_derives = "Debug" +)] +pub struct ReportPaymentTelemetry; diff --git a/parrot/Cargo.toml b/parrot/Cargo.toml new file mode 100644 index 0000000..65af1e4 --- /dev/null +++ b/parrot/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "parrot" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = "0.4.17" + +graphql = { path = "../graphql" } +honey-badger = { path = "../honey-badger" } + +[dev-dependencies] +simplelog = { version ="0.12.0", features = ["test"] } diff --git a/parrot/src/lib.rs b/parrot/src/lib.rs new file mode 100644 index 0000000..4a62e5e --- /dev/null +++ b/parrot/src/lib.rs @@ -0,0 +1,220 @@ +use graphql::schema::report_payment_telemetry::{ + PayFailedInput, PayInitiatedInput, PaySource, PaySucceededInput, PaymentTelemetryEventsInput, + RequestInitiatedInput, RequestSucceededInput, +}; +use graphql::schema::{report_payment_telemetry, ReportPaymentTelemetry}; +use graphql::{build_client, post_blocking, ToRfc3339}; +use honey_badger::Auth; +use std::sync::Arc; +use std::time::SystemTime; + +pub enum PaymentSource { + Camera, + Clipboard, + Nfc, + Manual, +} +pub enum PayFailureReason { + NoRoute, + Unkown, +} + +pub enum AnalyticsEvent { + PayInitiated { + payment_hash: String, + + paid_amount_msat: u64, + requested_amount_msat: Option, + sats_per_user_currency: Option, + + source: PaymentSource, + user_currency: String, + + process_started_at: SystemTime, + executed_at: SystemTime, + }, + PaySucceeded { + payment_hash: String, + + ln_fees_paid_msat: u64, + confirmed_at: SystemTime, + }, + PayFailed { + payment_hash: String, + + reason: PayFailureReason, + failed_at: SystemTime, + }, + RequestInitiated { + payment_hash: String, + + entered_amount_msat: Option, + sats_per_user_currency: Option, + + user_currency: String, + request_currency: String, + + created_at: SystemTime, + }, + RequestSucceeded { + payment_hash: String, + + paid_amount_sat: u64, + channel_opening_fee_msat: u64, + + received_at: SystemTime, + }, +} + +pub struct AnalyticsClient { + backend_url: String, + analytics_id: String, + auth: Arc, +} + +impl AnalyticsClient { + pub fn new(backend_url: String, analytics_id: String, auth: Arc) -> Self { + Self { + backend_url, + analytics_id, + auth, + } + } + + pub fn report_event(&self, analytics_event: AnalyticsEvent) -> graphql::Result<()> { + let variables = match analytics_event { + AnalyticsEvent::PayInitiated { + payment_hash, + paid_amount_msat, + requested_amount_msat, + sats_per_user_currency, + source, + user_currency, + process_started_at, + executed_at, + } => report_payment_telemetry::Variables { + telemetry_id: self.analytics_id.clone(), + events: Some(PaymentTelemetryEventsInput { + pay_failed: None, + pay_initiated: Some(PayInitiatedInput { + process_started_at: process_started_at.to_rfc3339(), + executed_at: executed_at.to_rfc3339(), + paid_amount_m_sat: paid_amount_msat, + payment_hash, + requested_amount_m_sat: requested_amount_msat.unwrap_or(0), + sats_per_user_currency: sats_per_user_currency.unwrap_or(0) as i64, + source: map_payment_source(source), + user_currency, + }), + pay_succeeded: None, + request_initiated: None, + request_succeeded: None, + }), + }, + AnalyticsEvent::PaySucceeded { + payment_hash, + ln_fees_paid_msat, + confirmed_at, + } => report_payment_telemetry::Variables { + telemetry_id: self.analytics_id.clone(), + events: Some(PaymentTelemetryEventsInput { + pay_failed: None, + pay_initiated: None, + pay_succeeded: Some(PaySucceededInput { + confirmed_at: confirmed_at.to_rfc3339(), + ln_fees_paid_m_sat: ln_fees_paid_msat, + payment_hash, + }), + request_initiated: None, + request_succeeded: None, + }), + }, + AnalyticsEvent::PayFailed { + payment_hash, + reason, + failed_at, + } => report_payment_telemetry::Variables { + telemetry_id: self.analytics_id.clone(), + events: Some(PaymentTelemetryEventsInput { + pay_failed: Some(PayFailedInput { + failed_at: failed_at.to_rfc3339(), + payment_hash, + reason: map_pay_failure_reason(reason), + }), + pay_initiated: None, + pay_succeeded: None, + request_initiated: None, + request_succeeded: None, + }), + }, + AnalyticsEvent::RequestInitiated { + payment_hash, + entered_amount_msat, + sats_per_user_currency, + user_currency, + request_currency, + created_at, + } => report_payment_telemetry::Variables { + telemetry_id: self.analytics_id.clone(), + events: Some(PaymentTelemetryEventsInput { + pay_failed: None, + pay_initiated: None, + pay_succeeded: None, + request_initiated: Some(RequestInitiatedInput { + created_at: created_at.to_rfc3339(), + entered_amount_m_sat: entered_amount_msat.unwrap_or(0), + payment_hash, + request_currency, + sats_per_user_currency: sats_per_user_currency.unwrap_or(0) as i64, + user_currency, + }), + request_succeeded: None, + }), + }, + AnalyticsEvent::RequestSucceeded { + payment_hash, + paid_amount_sat, + channel_opening_fee_msat, + received_at, + } => report_payment_telemetry::Variables { + telemetry_id: self.analytics_id.clone(), + events: Some(PaymentTelemetryEventsInput { + pay_failed: None, + pay_initiated: None, + pay_succeeded: None, + request_initiated: None, + request_succeeded: Some(RequestSucceededInput { + channel_opening_fee_m_sat: channel_opening_fee_msat, + paid_amount_m_sat: paid_amount_sat, + payment_hash, + payment_received_at: received_at.to_rfc3339(), + }), + }), + }, + }; + + let token = self.auth.query_token()?; + let client = build_client(Some(&token))?; + post_blocking::(&client, &self.backend_url, variables)?; + + Ok(()) + } +} + +fn map_payment_source(payment_source: PaymentSource) -> PaySource { + match payment_source { + PaymentSource::Camera => PaySource::CAMERA, + PaymentSource::Clipboard => PaySource::CLIPBOARD, + PaymentSource::Nfc => PaySource::NFC, + PaymentSource::Manual => PaySource::MANUAL, + } +} + +fn map_pay_failure_reason( + pay_failure_reason: PayFailureReason, +) -> report_payment_telemetry::PayFailureReason { + match pay_failure_reason { + PayFailureReason::NoRoute => report_payment_telemetry::PayFailureReason::NO_ROUTE, + PayFailureReason::Unkown => report_payment_telemetry::PayFailureReason::UNKNOWN, + } +} From dd039f149be09eed781f48f00d73506ed3edd964 Mon Sep 17 00:00:00 2001 From: Dominic Leutenegger Date: Thu, 9 Nov 2023 16:15:23 +0100 Subject: [PATCH 2/3] Adjust chameleon to schema changes --- graphql/src/schema.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphql/src/schema.rs b/graphql/src/schema.rs index e4d97b2..955c2a8 100644 --- a/graphql/src/schema.rs +++ b/graphql/src/schema.rs @@ -165,6 +165,8 @@ pub struct GetLatestChannelManager; #[allow(non_camel_case_types)] type bigint = u64; type BigInteger = bigint; +#[allow(non_camel_case_types)] +type float8 = f64; #[derive(GraphQLQuery)] #[graphql( From 439d10f8b9a60bab94e873239c626754160ad78c Mon Sep 17 00:00:00 2001 From: Dominic Date: Thu, 9 Nov 2023 17:21:20 +0100 Subject: [PATCH 3/3] Apply code review suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Granhão <32176319+danielgranhao@users.noreply.github.com> --- graphql/schemas/operations.graphql | 4 +++- graphql/src/lib.rs | 2 +- graphql/src/schema.rs | 6 +++--- parrot/Cargo.toml | 5 ----- parrot/src/lib.rs | 4 ++-- squirrel/Cargo.toml | 1 + squirrel/tests/integration_tests.rs | 2 +- 7 files changed, 11 insertions(+), 13 deletions(-) diff --git a/graphql/schemas/operations.graphql b/graphql/schemas/operations.graphql index 71e0db2..dceac66 100644 --- a/graphql/schemas/operations.graphql +++ b/graphql/schemas/operations.graphql @@ -188,5 +188,7 @@ mutation RecoverBackup($schemaName: String!) { } mutation ReportPaymentTelemetry($telemetryId: String!, $events: PaymentTelemetryEventsInput) { - report_payment_telemetry(telemetryId: $telemetryId, events: $events) + report_payment_telemetry(telemetryId: $telemetryId, events: $events) { + payFailed + } } diff --git a/graphql/src/lib.rs b/graphql/src/lib.rs index af0962d..6559b40 100644 --- a/graphql/src/lib.rs +++ b/graphql/src/lib.rs @@ -6,8 +6,8 @@ pub use crate::errors::*; pub use perro; pub use reqwest; -use graphql_client::reqwest::{post_graphql, post_graphql_blocking}; use chrono::{DateTime, Utc}; +use graphql_client::reqwest::{post_graphql, post_graphql_blocking}; use graphql_client::Response; use perro::{permanent_failure, runtime_error, MapToError, OptionToError}; use reqwest::blocking::Client; diff --git a/graphql/src/schema.rs b/graphql/src/schema.rs index 955c2a8..dfa7ff8 100644 --- a/graphql/src/schema.rs +++ b/graphql/src/schema.rs @@ -210,8 +210,8 @@ pub struct RecoverBackup; #[derive(GraphQLQuery)] #[graphql( -schema_path = "schemas/schema_wallet_read.graphql", -query_path = "schemas/operations.graphql", -response_derives = "Debug" + schema_path = "schemas/schema_wallet_read.graphql", + query_path = "schemas/operations.graphql", + response_derives = "Debug" )] pub struct ReportPaymentTelemetry; diff --git a/parrot/Cargo.toml b/parrot/Cargo.toml index 65af1e4..c2d9a71 100644 --- a/parrot/Cargo.toml +++ b/parrot/Cargo.toml @@ -4,10 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] -log = "0.4.17" - graphql = { path = "../graphql" } honey-badger = { path = "../honey-badger" } - -[dev-dependencies] -simplelog = { version ="0.12.0", features = ["test"] } diff --git a/parrot/src/lib.rs b/parrot/src/lib.rs index 4a62e5e..7661411 100644 --- a/parrot/src/lib.rs +++ b/parrot/src/lib.rs @@ -16,7 +16,7 @@ pub enum PaymentSource { } pub enum PayFailureReason { NoRoute, - Unkown, + Unknown, } pub enum AnalyticsEvent { @@ -215,6 +215,6 @@ fn map_pay_failure_reason( ) -> report_payment_telemetry::PayFailureReason { match pay_failure_reason { PayFailureReason::NoRoute => report_payment_telemetry::PayFailureReason::NO_ROUTE, - PayFailureReason::Unkown => report_payment_telemetry::PayFailureReason::UNKNOWN, + PayFailureReason::Unknown => report_payment_telemetry::PayFailureReason::UNKNOWN, } } diff --git a/squirrel/Cargo.toml b/squirrel/Cargo.toml index 6e1f5c2..2942d8d 100644 --- a/squirrel/Cargo.toml +++ b/squirrel/Cargo.toml @@ -8,6 +8,7 @@ hex = "0.4.3" graphql = { path = "../graphql" } honey-badger = { path = "../honey-badger" } +bdk = { version = "0.29.0", features = ["keys-bip39"] } [dev-dependencies] bitcoin = { version = "0.29.2" } diff --git a/squirrel/tests/integration_tests.rs b/squirrel/tests/integration_tests.rs index c2a53e8..d8d1ac6 100644 --- a/squirrel/tests/integration_tests.rs +++ b/squirrel/tests/integration_tests.rs @@ -1,4 +1,4 @@ -use bitcoin::Network; +use bdk::bitcoin::Network; use graphql::perro::Error::RuntimeError; use graphql::GraphQlRuntimeErrorCode; use honey_badger::asynchronous::Auth;