diff --git a/.cargo/config.toml.breez.sample b/.cargo/config.toml.breez.sample index 5823130e..4a2c780e 100644 --- a/.cargo/config.toml.breez.sample +++ b/.cargo/config.toml.breez.sample @@ -30,5 +30,6 @@ BREEZ_SDK_API_KEY = "" BREEZ_SDK_PARTNER_CERTIFICATE = "" BREEZ_SDK_PARTNER_KEY = "" -BREEZ_SDK_MNEMONIC_ALICE = "" -BREEZ_SDK_MNEMONIC_BOB = "" +BREEZ_SDK_MNEMONIC_EMPTY = "" +BREEZ_SDK_MNEMONIC_FUNDED_SENDER = "" +BREEZ_SDK_MNEMONIC_FUNDED_RECEIVER = "" diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml new file mode 100644 index 00000000..b3493a83 --- /dev/null +++ b/.github/workflows/e2e_tests.yml @@ -0,0 +1,34 @@ +name: E2E Tests + +on: + push: + branches: + - main + pull_request: + schedule: + - cron: '21 2 * * *' # every day at 5:21 AM UTC + +env: + GITHUB_REF: ${{ github.ref }} + +jobs: + integration: + name: E2E Tests + runs-on: ubuntu-latest + steps: + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Config cargo + run: echo -e "$CARGO_CONFIG_TOML_BREEZ" > .cargo/config.toml + env: + CARGO_CONFIG_TOML_BREEZ: ${{ secrets.CARGO_CONFIG_TOML_BREEZ }} + - name: Rust Cache + uses: Swatinem/rust-cache@v2.7.0 + - name: Run e2e tests + run: make testsatflows diff --git a/Makefile b/Makefile index 4685126c..56205863 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,10 @@ integrationtests: testregisternode: cargo test --test register_node_test -- --ignored --nocapture +.PHONY: testsatflows +testsatflows: + cargo test --test sat_flows -- --ignored --nocapture + .PHONY: testall testall: test integrationtests diff --git a/src/task_manager.rs b/src/task_manager.rs index 9406f448..59b678db 100644 --- a/src/task_manager.rs +++ b/src/task_manager.rs @@ -165,6 +165,7 @@ impl TaskManager { Ok(Ok(rates)) => { persist_exchange_rates(&data_store, &rates); *exchange_rates.lock_unwrap() = rates; + debug!("Updated exchange rates successfully"); } Ok(Err(e)) => { error!("Failed to update exchange rates: {e}"); diff --git a/tests/decode_test.rs b/tests/decode_test.rs index 03602976..c9b0b54a 100644 --- a/tests/decode_test.rs +++ b/tests/decode_test.rs @@ -1,7 +1,7 @@ mod print_events_handler; mod setup; -use crate::setup::start_alice; +use crate::setup::start_node; use breez_sdk_core::Network; use serial_test::file_serial; @@ -11,7 +11,7 @@ use uniffi_lipalightninglib::{DecodeDataError, DecodedData, InvoiceDetails, Unsu #[test] #[file_serial(key, path => "/tmp/3l-int-tests-lock")] fn test_decoding() { - let node = start_alice().unwrap(); + let node = start_node().unwrap(); let invalid_invoice = "invalid".to_string(); let result = node.decode_data(invalid_invoice); diff --git a/tests/node_info_test.rs b/tests/node_info_test.rs index 9cc7301a..d6264d72 100644 --- a/tests/node_info_test.rs +++ b/tests/node_info_test.rs @@ -1,7 +1,7 @@ mod print_events_handler; mod setup; -use crate::setup::start_alice; +use crate::setup::start_node; use bitcoin::secp256k1::PublicKey; use serial_test::file_serial; @@ -10,7 +10,7 @@ use std::str::FromStr; #[test] #[file_serial(key, path => "/tmp/3l-int-tests-lock")] fn test_get_node_info() { - let node = start_alice().unwrap(); + let node = start_node().unwrap(); let node_info = node.get_node_info().unwrap(); assert!( diff --git a/tests/payment_fetching_test.rs b/tests/payment_fetching_test.rs index 36bdaeb5..1c3d8ed6 100644 --- a/tests/payment_fetching_test.rs +++ b/tests/payment_fetching_test.rs @@ -1,7 +1,7 @@ mod print_events_handler; mod setup; -use crate::setup::start_alice; +use crate::setup::start_node; use serial_test::file_serial; use uniffi_lipalightninglib::{Activity, InvoiceCreationMetadata}; @@ -9,7 +9,7 @@ use uniffi_lipalightninglib::{Activity, InvoiceCreationMetadata}; #[test] #[file_serial(key, path => "/tmp/3l-int-tests-lock")] fn test_payment_fetching() { - let node = start_alice().unwrap(); + let node = start_node().unwrap(); let invoice = node .create_invoice( diff --git a/tests/receive_onchain_test.rs b/tests/receive_onchain_test.rs index 52c249ca..1d93f06d 100644 --- a/tests/receive_onchain_test.rs +++ b/tests/receive_onchain_test.rs @@ -1,14 +1,14 @@ mod print_events_handler; mod setup; -use crate::setup::start_alice; +use crate::setup::start_node; use serial_test::file_serial; #[test] #[file_serial(key, path => "/tmp/3l-int-tests-lock")] fn test_receive_onchain() { - let node = start_alice().unwrap(); + let node = start_node().unwrap(); let swap_info = node.generate_swap_address(None).unwrap(); assert!(swap_info.address.starts_with("bc1")); diff --git a/tests/sat_flows.rs b/tests/sat_flows.rs new file mode 100644 index 00000000..c861747a --- /dev/null +++ b/tests/sat_flows.rs @@ -0,0 +1,316 @@ +mod print_events_handler; +mod setup; + +use crate::setup::{start_specific_node, NodeType}; +use uniffi_lipalightninglib::{ + Activity, BreezHealthCheckStatus, EventsCallback, InvoiceCreationMetadata, LightningNode, + PaymentMetadata, PaymentState, +}; + +use parrot::PaymentSource; +use serial_test::file_serial; +use std::sync::mpsc; +use std::sync::mpsc::{channel, Receiver}; +use std::time::Instant; +use thousands::Separable; + +const PAYMENT_AMOUNT_SATS: u64 = 300; +const MAX_PAYMENT_TIME_SECS: u64 = 60; +const INVOICE_DESCRIPTION: &str = "automated bolt11 test"; + +struct TransactingNode { + node: LightningNode, + sent_payment_learn: mpsc::Receiver, + received_payment_learn: mpsc::Receiver, +} + +struct ReturnFundsEventsHandler { + pub received_payment_inform: mpsc::Sender, + pub sent_payment_inform: mpsc::Sender, +} + +struct PaymentAmount { + exact: u64, + plus_fees: u64, + minus_fees: u64, +} + +impl EventsCallback for ReturnFundsEventsHandler { + fn payment_received(&self, payment_hash: String) { + self.received_payment_inform.send(payment_hash).unwrap(); + } + + fn channel_closed(&self, channel_id: String, reason: String) { + panic!("A channel was closed! Channel ID {channel_id} was closed due to {reason}"); + } + + fn payment_sent(&self, payment_hash: String, _: String) { + self.sent_payment_inform.send(payment_hash).unwrap(); + } + + fn payment_failed(&self, payment_hash: String) { + panic!("An outgoing payment has failed! Its hash is {payment_hash}"); + } + + fn swap_received(&self, _payment_hash: String) { + todo!() + } + + fn breez_health_status_changed_to(&self, _status: BreezHealthCheckStatus) { + // do nothing + } + + fn synced(&self) { + // do nothing + } +} + +#[test] +#[ignore = "This test costs real sats!"] +#[file_serial(key, path => "/tmp/3l-int-tests-lock")] +fn test_bolt11_payment() { + let amount = get_payment_amount(); + + let sender = setup_sender_node(amount.plus_fees); + let receiver = setup_receiver_node(amount.plus_fees); + + let before_invoice_creation = Instant::now(); + let send_invoice = receiver + .node + .create_invoice( + amount.exact, + None, + INVOICE_DESCRIPTION.to_string(), + InvoiceCreationMetadata { + request_currency: "EUR".to_string(), + }, + ) + .unwrap(); + println!( + "Created invoice in {} milliseconds", + before_invoice_creation + .elapsed() + .as_millis() + .separate_with_commas() + ); + let payment_hash = send_invoice.payment_hash.clone(); + + let before_paying_invoice = Instant::now(); + sender + .node + .pay_invoice( + send_invoice.clone(), + PaymentMetadata { + source: PaymentSource::Manual, + process_started_at: std::time::SystemTime::now(), + }, + ) + .unwrap(); + + wait_for_payment( + &payment_hash, + &sender.sent_payment_learn, + &receiver.received_payment_learn, + ) + .unwrap(); + println!( + "Payment [{} sat] successful after {} milliseconds", + amount.exact, + before_paying_invoice + .elapsed() + .as_millis() + .separate_with_commas() + ); + + // return funds to keep sender well funded + let return_invoice = sender + .node + .create_invoice( + amount.minus_fees, + None, + INVOICE_DESCRIPTION.to_string(), + InvoiceCreationMetadata { + request_currency: "EUR".to_string(), + }, + ) + .unwrap(); + let payment_hash = return_invoice.payment_hash.clone(); + + receiver + .node + .pay_invoice( + return_invoice.clone(), + PaymentMetadata { + source: PaymentSource::Manual, + process_started_at: std::time::SystemTime::now(), + }, + ) + .unwrap(); + + wait_for_payment( + &payment_hash, + &receiver.sent_payment_learn, + &sender.received_payment_learn, + ) + .unwrap(); + + let payments = sender.node.get_latest_activities(2).unwrap(); + assert_eq!(payments.completed_activities.len(), 2); + + for payment in payments.completed_activities { + match payment { + Activity::OutgoingPayment { + outgoing_payment_info, + } => { + assert_eq!( + outgoing_payment_info.payment_info.payment_state, + PaymentState::Succeeded + ); + assert_eq!( + outgoing_payment_info + .payment_info + .invoice_details + .payment_hash, + send_invoice.payment_hash + ); + } + Activity::IncomingPayment { + incoming_payment_info, + } => { + assert_eq!( + incoming_payment_info.requested_amount.sats, + amount.minus_fees + ); + assert_eq!( + incoming_payment_info.payment_info.payment_state, + PaymentState::Succeeded + ); + assert_eq!( + incoming_payment_info + .payment_info + .invoice_details + .payment_hash, + return_invoice.payment_hash + ); + } + _ => { + panic!("Unexpected activity: {payment:?}"); + } + } + } + + // Check whether exchange rate has updated during test run + let backend_exchange_rate_update_interval_secs: u64 = 5 * 60; // exchange_rate.updated_at measures the elpased time since the server updated, NOT since 3L last fetched that data. + let exchange_rate = sender.node.get_exchange_rate().unwrap(); + assert!( + exchange_rate.updated_at.elapsed().unwrap().as_secs() + <= backend_exchange_rate_update_interval_secs + ); +} + +fn setup_sender_node(payment_amount_plus_fees: u64) -> TransactingNode { + let tn = setup_node(Some(NodeType::Sender)); + + let node_info = tn.node.get_node_info().unwrap(); + let outbound_capacity = node_info.channels_info.outbound_capacity.sats; + assert!( + outbound_capacity > payment_amount_plus_fees, + "Sending node ({}) is insufficiently funded [Outbound capacity: {outbound_capacity}, required: {payment_amount_plus_fees}]", + node_info.node_pubkey + ); + + tn +} + +fn setup_receiver_node(max_payment_amount: u64) -> TransactingNode { + let tn = setup_node(Some(NodeType::Receiver)); + + let node_info = tn.node.get_node_info().unwrap(); + let inbound_capacity = node_info.channels_info.inbound_capacity.sats; + assert!( + inbound_capacity > max_payment_amount, + "Sending node ({}) has insufficient inbound capacity: {inbound_capacity} (required: {max_payment_amount})", + node_info.node_pubkey + ); + + tn +} + +fn setup_node(node_type: Option) -> TransactingNode { + let (sent_payment_inform, sent_payment_learn) = channel(); + let (received_payment_inform, received_payment_learn) = channel(); + + let before_node_started = Instant::now(); + let node = start_specific_node( + node_type.clone(), + Box::new(ReturnFundsEventsHandler { + sent_payment_inform, + received_payment_inform, + }), + ) + .unwrap(); + println!( + "{:?} node started in {} milliseconds", + node_type.unwrap(), + before_node_started + .elapsed() + .as_millis() + .separate_with_commas() + ); + + // Additional check: Have LSP fees been fetched successfully + assert!(node.query_lsp_fee().is_ok()); + + TransactingNode { + node, + sent_payment_learn, + received_payment_learn, + } +} + +fn wait_for_payment( + payment_hash: &str, + sender: &Receiver, + receiver: &Receiver, +) -> Result<(), &'static str> { + let start_time = Instant::now(); + let mut sender_sent_payment = false; + let mut receiver_received_payment = false; + loop { + if start_time.elapsed().as_secs() >= MAX_PAYMENT_TIME_SECS { + return Err("Payment did not go through within {MAX_PAYMENT_TIME_SECS} seconds!"); + } + + if let Ok(received_payment_hash) = sender.try_recv() { + if received_payment_hash == payment_hash { + sender_sent_payment = true; + } else { + return Err("Unexpected payment sent: {last_payment_hash} (expected payment: {payment_hash})"); + } + } + + if let Ok(received_payment_hash) = receiver.try_recv() { + if received_payment_hash == payment_hash { + receiver_received_payment = true; + } else { + return Err("Unexpected payment received: {last_payment_hash} (expected payment: {payment_hash})"); + } + } + + if sender_sent_payment && receiver_received_payment { + return Ok(()); + } + + std::thread::sleep(std::time::Duration::from_secs(1)); + } +} + +fn get_payment_amount() -> PaymentAmount { + let fee_deviation = 5 + PAYMENT_AMOUNT_SATS / 25; + + PaymentAmount { + exact: PAYMENT_AMOUNT_SATS, + plus_fees: PAYMENT_AMOUNT_SATS + fee_deviation, + minus_fees: PAYMENT_AMOUNT_SATS - fee_deviation, + } +} diff --git a/tests/setup/mod.rs b/tests/setup/mod.rs index 62f350f2..e42ad400 100644 --- a/tests/setup/mod.rs +++ b/tests/setup/mod.rs @@ -1,7 +1,7 @@ use crate::print_events_handler::PrintEventsHandler; use uniffi_lipalightninglib::{ - mnemonic_to_secret, AnalyticsConfig, BreezSdkConfig, Config, LightningNode, + mnemonic_to_secret, AnalyticsConfig, BreezSdkConfig, Config, EventsCallback, LightningNode, MaxRoutingFeeConfig, ReceiveLimitsConfig, RemoteServicesConfig, RuntimeErrorCode, TzConfig, }; @@ -13,6 +13,13 @@ type Result = std::result::Result>; const LOCAL_PERSISTENCE_PATH: &str = ".3l_local_test"; +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub enum NodeType { + Sender, + Receiver, +} + #[macro_export] macro_rules! wait_for_condition { ($cond:expr, $message_if_not_satisfied:expr, $attempts:expr, $sleep_duration:expr) => { @@ -49,18 +56,23 @@ macro_rules! wait_for { } #[allow(dead_code)] -pub fn start_alice() -> Result { - start_node("ALICE") +pub fn start_node() -> Result { + start_specific_node(None, Box::new(PrintEventsHandler {})) } #[allow(dead_code)] -pub fn start_bob() -> Result { - start_node("BOB") -} - -fn start_node(node_name: &str) -> Result { +pub fn start_specific_node( + node_type: Option, + events_callback: Box, +) -> Result { std::env::set_var("TESTING_TASK_PERIODS", "5"); + let node_name = match node_type { + Some(NodeType::Sender) => "FUNDED_SENDER", + Some(NodeType::Receiver) => "FUNDED_RECEIVER", + None => "EMPTY", + }; + let local_persistence_path = format!("{LOCAL_PERSISTENCE_PATH}/{node_name}"); let _ = fs::remove_dir_all(local_persistence_path.clone()); fs::create_dir_all(local_persistence_path.clone()).unwrap(); @@ -106,8 +118,7 @@ fn start_node(node_name: &str) -> Result { }, }; - let events_handler = PrintEventsHandler {}; - let node = LightningNode::new(config, Box::new(events_handler))?; + let node = LightningNode::new(config, events_callback)?; node.set_analytics_config(AnalyticsConfig::Disabled)?; // tests produce misleading noise Ok(node) diff --git a/tests/topup_test.rs b/tests/topup_test.rs index b6b49302..885b6f8f 100644 --- a/tests/topup_test.rs +++ b/tests/topup_test.rs @@ -1,7 +1,7 @@ mod print_events_handler; mod setup; -use crate::setup::start_alice; +use crate::setup::start_node; use perro::Error::InvalidInput; use std::time::Duration; @@ -11,7 +11,7 @@ use uniffi_lipalightninglib::{OfferInfo, OfferKind, OfferStatus}; #[test] #[file_serial(key, path => "/tmp/3l-int-tests-lock")] fn test_topup() { - let node = start_alice().unwrap(); + let node = start_node().unwrap(); node.register_fiat_topup(None, "CH8689144834469929874".to_string(), "CHF".to_string()) .expect("Couldn't register topup without email");