From edaae1cce3317830e3050f82ac60f721f87e1f92 Mon Sep 17 00:00:00 2001 From: brianp Date: Wed, 29 Mar 2023 17:17:54 +0200 Subject: [PATCH] Add cucumber tests for chat This provides a barebones chat client used for testing within cucumber. The client is relatively isolated from the test suite itself and could be expanded on in the future. --- integration_tests/Cargo.toml | 5 +- integration_tests/tests/cucumber.rs | 61 ++++++- integration_tests/tests/features/Chat.feature | 44 ++++++ .../tests/utils/base_node_process.rs | 5 +- .../tests/utils/chat_client/Cargo.toml | 23 +++ .../tests/utils/chat_client/src/client.rs | 149 ++++++++++++++++++ .../tests/utils/chat_client/src/database.rs | 55 +++++++ .../tests/utils/chat_client/src/lib.rs | 27 ++++ .../tests/utils/chat_client/src/networking.rs | 138 ++++++++++++++++ 9 files changed, 497 insertions(+), 10 deletions(-) create mode 100644 integration_tests/tests/features/Chat.feature create mode 100644 integration_tests/tests/utils/chat_client/Cargo.toml create mode 100644 integration_tests/tests/utils/chat_client/src/client.rs create mode 100644 integration_tests/tests/utils/chat_client/src/database.rs create mode 100644 integration_tests/tests/utils/chat_client/src/lib.rs create mode 100644 integration_tests/tests/utils/chat_client/src/networking.rs diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index c753111e9a0..c489f802dd8 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -11,11 +11,14 @@ tari_app_grpc = { path = "../applications/tari_app_grpc" } tari_app_utilities = { path = "../applications/tari_app_utilities" } tari_base_node = { path = "../applications/tari_base_node" } tari_base_node_grpc_client = { path = "../clients/rust/base_node_grpc_client" } +tari_chat_client = { path = "tests/utils/chat_client" } tari_common = { path = "../common" } +tari_common_sqlite = { path = "../common_sqlite" } tari_common_types = { path = "../base_layer/common_types" } tari_comms = { path = "../comms/core" } tari_comms_dht = { path = "../comms/dht" } tari_console_wallet = { path = "../applications/tari_console_wallet" } +tari_contacts = { path = "../base_layer/contacts" } tari_core = { path = "../base_layer/core" } tari_merge_mining_proxy = { path = "../applications/tari_merge_mining_proxy" } tari_miner = { path = "../applications/tari_miner" } @@ -24,8 +27,8 @@ tari_script = { path = "../infrastructure/tari_script" } tari_shutdown = { path = "../infrastructure/shutdown" } tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", tag="v0.4.10"} tari_wallet = { path = "../base_layer/wallet" } -tari_wallet_grpc_client = { path = "../clients/rust/wallet_grpc_client" } tari_wallet_ffi = { path = "../base_layer/wallet_ffi" } +tari_wallet_grpc_client = { path = "../clients/rust/wallet_grpc_client" } anyhow = "1.0.53" async-trait = "0.1.50" diff --git a/integration_tests/tests/cucumber.rs b/integration_tests/tests/cucumber.rs index dfcad43f9b4..9270a4fef9d 100644 --- a/integration_tests/tests/cucumber.rs +++ b/integration_tests/tests/cucumber.rs @@ -38,18 +38,19 @@ use cucumber::{event::ScenarioFinished, gherkin::Scenario, given, then, when, Wo use futures::StreamExt; use indexmap::IndexMap; use log::*; -use rand::Rng; +use rand::{rngs::OsRng, Rng}; use serde_json::Value; use tari_app_grpc::tari_rpc::{self as grpc}; use tari_app_utilities::utilities::UniPublicKey; use tari_base_node::BaseNodeConfig; use tari_base_node_grpc_client::grpc::{GetBlocksRequest, ListHeadersRequest}; +use tari_chat_client::Client; use tari_common::{configuration::Network, initialize_logging}; use tari_common_types::{ tari_address::TariAddress, types::{BlindingFactor, ComAndPubSignature, Commitment, PrivateKey, PublicKey}, }; -use tari_comms::multiaddr::Multiaddr; +use tari_comms::{multiaddr::Multiaddr, peer_manager::PeerFeatures, NodeIdentity}; use tari_console_wallet::{ BurnTariArgs, CliCommands, @@ -100,8 +101,9 @@ use thiserror::Error; use tokio::runtime::Runtime; use crate::utils::{ - base_node_process::{spawn_base_node, spawn_base_node_with_config, BaseNodeProcess}, + base_node_process::{get_base_dir, spawn_base_node, spawn_base_node_with_config, BaseNodeProcess}, get_peer_addresses, + get_port, merge_mining_proxy::{register_merge_mining_proxy_process, MergeMiningProxyProcess}, miner::{ mine_block, @@ -147,6 +149,7 @@ pub struct TariWorld { miners: IndexMap, ffi_wallets: IndexMap, wallets: IndexMap, + chat_clients: IndexMap, merge_mining_proxies: IndexMap, transactions: IndexMap, wallet_addresses: IndexMap, // values are strings representing tari addresses @@ -277,10 +280,11 @@ impl TariWorld { pub async fn after(&mut self, _scenario: &Scenario) { self.base_nodes.clear(); - self.seed_nodes.clear(); - self.wallets.clear(); + self.chat_clients.clear(); self.ffi_wallets.clear(); self.miners.clear(); + self.seed_nodes.clear(); + self.wallets.clear(); } } @@ -4980,6 +4984,53 @@ async fn merge_mining_ask_for_block_header_by_hash(world: &mut TariWorld, mining world.last_merge_miner_response = merge_miner.get_block_header_by_hash(hash).await; } +#[when(expr = "I have a chat client {word} connected to seed node {word}")] +async fn chat_client_connected_to_base_node(world: &mut TariWorld, name: String, seed_node_name: String) { + let base_node = world.get_node(&seed_node_name).unwrap(); + + let port = get_port(18000..18499).unwrap(); + let temp_dir_path = get_base_dir() + .join("chat_clients") + .join(format!("port_{}", port)) + .join(name.clone()); + let address = Multiaddr::from_str(&format!("/ip4/127.0.0.1/tcp/{}", port)).unwrap(); + let identity = NodeIdentity::random(&mut OsRng, address, PeerFeatures::COMMUNICATION_NODE); + + let mut client = Client::new(identity, vec![base_node.identity.to_peer()], temp_dir_path); + client.initialize().await; + + world.chat_clients.insert(name, client); +} + +#[when(regex = r"^I use (.+) to send a message '(.+)' to (.*)$")] +async fn send_message_to(world: &mut TariWorld, sender: String, message: String, receiver: String) { + let sender = world.chat_clients.get(&sender).unwrap(); + let receiver = world.chat_clients.get(&receiver).unwrap(); + let address = TariAddress::from_public_key(receiver.identity.public_key(), Network::LocalNet); + + sender.send_message(address, message).await; +} + +#[then(expr = "{word} will have {int} message(s) with {word}")] +async fn receive_n_messages(world: &mut TariWorld, receiver: String, message_count: u64, sender: String) { + let receiver = world.chat_clients.get(&receiver).unwrap(); + let sender = world.chat_clients.get(&sender).unwrap(); + let address = TariAddress::from_public_key(sender.identity.public_key(), Network::LocalNet); + + let messages = receiver.get_messages(address).await; + assert_eq!(messages.len() as u64, message_count) +} + +#[when(expr = "I add {word} as a contact to {word}")] +async fn add_as_contact(world: &mut TariWorld, receiver: String, sender: String) { + let receiver: &Client = world.chat_clients.get(&receiver).unwrap(); + let sender: &Client = world.chat_clients.get(&sender).unwrap(); + + let address = TariAddress::from_public_key(receiver.identity.public_key(), Network::LocalNet); + + sender.add_contact(&address).await; +} + fn flush_stdout(buffer: &Arc>>) { // After each test we flush the stdout to the logs. info!( diff --git a/integration_tests/tests/features/Chat.feature b/integration_tests/tests/features/Chat.feature new file mode 100644 index 00000000000..f7170e6c8ce --- /dev/null +++ b/integration_tests/tests/features/Chat.feature @@ -0,0 +1,44 @@ +# Copyright 2023 The Tari Project +# SPDX-License-Identifier: BSD-3-Clause + +Feature: Chat messaging + + Scenario: A message is propagated between nodes + Given I have a seed node SEED_A + When I have a chat client CHAT_A connected to seed node SEED_A + When I have a chat client CHAT_B connected to seed node SEED_A + When I wait 5 seconds + When I use CHAT_A to send a message 'Hey there' to CHAT_B + When I wait 5 seconds + Then CHAT_B will have 1 message with CHAT_A + + Scenario: A message is sent directly between nodes + Given I have a seed node SEED_A + When I have a chat client CHAT_A connected to seed node SEED_A + When I have a chat client CHAT_B connected to seed node SEED_A + When I add CHAT_B as a contact to CHAT_A + When I wait 5 seconds + When I use CHAT_A to send a message 'Hey there' to CHAT_B + When I wait 5 seconds + Then CHAT_B will have 1 message with CHAT_A + + Scenario: Message counts are distinct + Given I have a seed node SEED_A + When I have a chat client CHAT_A connected to seed node SEED_A + When I have a chat client CHAT_B connected to seed node SEED_A + When I have a chat client CHAT_C connected to seed node SEED_A + When I wait 5 seconds + When I use CHAT_A to send a message 'Message 1ab' to CHAT_B + When I use CHAT_A to send a message 'Message 2ab' to CHAT_B + When I use CHAT_C to send a message 'Message 1cb' to CHAT_B + When I use CHAT_A to send a message 'Message 1ac' to CHAT_C + When I use CHAT_B to send a message 'Message 1bc' to CHAT_C + When I use CHAT_B to send a message 'Message 2bc' to CHAT_C + When I wait 5 seconds + Then CHAT_B will have 2 messages with CHAT_A + Then CHAT_B will have 3 messages with CHAT_C + Then CHAT_C will have 1 messages with CHAT_A + Then CHAT_C will have 3 messages with CHAT_B + Then CHAT_A will have 2 messages with CHAT_B + Then CHAT_A will have 1 messages with CHAT_C + diff --git a/integration_tests/tests/utils/base_node_process.rs b/integration_tests/tests/utils/base_node_process.rs index de6ebe81caf..3826ed2cc3f 100644 --- a/integration_tests/tests/utils/base_node_process.rs +++ b/integration_tests/tests/utils/base_node_process.rs @@ -46,6 +46,7 @@ use crate::{ TariWorld, }; +#[derive(Clone)] pub struct BaseNodeProcess { pub name: String, pub port: u64, @@ -175,11 +176,7 @@ pub async fn spawn_base_node_with_config( format!("/ip4/127.0.0.1/tcp/{}", port).parse().unwrap(); base_node_config.base_node.p2p.public_addresses = vec![base_node_config.base_node.p2p.transport.tcp.listener_address.clone()]; - // base_node_config.base_node.p2p.datastore_path = temp_dir_path.to_path_buf(); - // base_node_config.base_node.p2p.peer_database_name = "peer_db.mdb".to_string(); base_node_config.base_node.p2p.dht = DhtConfig::default_local_test(); - // base_node_config.base_node.p2p.dht.database_url = - // DbConnectionUrl::File(temp_dir_path.clone().join("dht.sqlite")); base_node_config.base_node.p2p.dht.network_discovery.enabled = true; base_node_config.base_node.p2p.allow_test_addresses = true; base_node_config.base_node.storage.orphan_storage_capacity = 10; diff --git a/integration_tests/tests/utils/chat_client/Cargo.toml b/integration_tests/tests/utils/chat_client/Cargo.toml new file mode 100644 index 00000000000..c890c1d571e --- /dev/null +++ b/integration_tests/tests/utils/chat_client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "tari_chat_client" +authors = ["The Tari Development Community"] +description = "Tari cucumber chat client" +license = "BSD-3-Clause" +version = "0.48.0-pre.1" +edition = "2018" + +[dependencies] +tari_common_types = { path = "../../../../base_layer/common_types" } +tari_common_sqlite = { path = "../../../../common_sqlite" } +tari_comms = { path = "../../../../comms/core" } +tari_comms_dht = { path = "../../../../comms/dht" } +tari_contacts = { path = "../../../../base_layer/contacts" } +tari_p2p = { path = "../../../../base_layer/p2p" } +tari_service_framework= { path = "../../../../base_layer/service_framework" } +tari_shutdown = { path = "../../../../infrastructure/shutdown" } +tari_test_utils = { path = "../../../../infrastructure/test_utils" } +tari_storage = { path = "../../../../infrastructure/storage" } + +anyhow = "1.0.41" +diesel = { version = "2.0.3", features = ["sqlite", "r2d2", "serde_json", "chrono", "64-column-tables"] } +lmdb-zero = "0.4.4" diff --git a/integration_tests/tests/utils/chat_client/src/client.rs b/integration_tests/tests/utils/chat_client/src/client.rs new file mode 100644 index 00000000000..5b3fd85eea1 --- /dev/null +++ b/integration_tests/tests/utils/chat_client/src/client.rs @@ -0,0 +1,149 @@ +// Copyright 2023. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{ + fmt::{Debug, Formatter}, + path::PathBuf, + sync::Arc, + time::Duration, +}; + +use tari_common_types::tari_address::TariAddress; +use tari_comms::{peer_manager::Peer, CommsNode, NodeIdentity}; +use tari_contacts::contacts_service::{ + handle::ContactsServiceHandle, + types::{Message, MessageBuilder}, +}; +use tari_shutdown::Shutdown; + +use crate::{database, networking}; + +#[derive(Clone)] +pub struct Client { + pub identity: Arc, + pub base_dir: PathBuf, + pub seed_peers: Vec, + pub shutdown: Shutdown, + pub contacts: Option, +} + +impl Debug for Client { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Client") + .field("identity", &self.identity) + .field("base_dir", &self.base_dir) + .field("seed_peers", &self.seed_peers) + .field("shutdown", &self.shutdown) + .finish() + } +} + +impl Drop for Client { + fn drop(&mut self) { + self.quit(); + } +} + +impl Client { + pub fn new(identity: NodeIdentity, seed_peers: Vec, base_dir: PathBuf) -> Self { + Self { + identity: Arc::new(identity), + base_dir, + seed_peers, + shutdown: Shutdown::new(), + contacts: None, + } + } + + pub async fn add_contact(&self, address: &TariAddress) { + if let Some(mut contacts_service) = self.contacts.clone() { + contacts_service + .upsert_contact(address.into()) + .await + .expect("Contact wasn't added"); + } + } + + pub async fn send_message(&self, receiver: TariAddress, message: String) { + if let Some(mut contacts_service) = self.contacts.clone() { + contacts_service + .send_message(MessageBuilder::new().message(message).address(receiver).build()) + .await + .expect("Message wasn't sent"); + } + } + + pub async fn get_messages(&self, sender: TariAddress) -> Vec { + let mut messages = vec![]; + if let Some(mut contacts_service) = self.contacts.clone() { + messages = contacts_service + .get_messages(sender, 0, 0) + .await + .expect("Messages not fetched"); + } + + messages + } + + pub async fn initialize(&mut self) { + println!("initializing chat"); + + let signal = self.shutdown.to_signal(); + let db = database::create_chat_storage(self.base_dir.clone()).unwrap(); + + let (contacts, comms_node) = networking::start( + self.identity.clone(), + self.base_dir.clone(), + self.seed_peers.clone(), + db, + signal, + ) + .await + .unwrap(); + + if !self.seed_peers.is_empty() { + loop { + println!("Waiting for peer connections..."); + match wait_for_connectivity(comms_node.clone()).await { + Ok(_) => break, + Err(e) => println!("{}. Still waiting...", e), + } + } + } + + self.contacts = Some(contacts); + + println!("Connections established") + } + + pub fn quit(&mut self) { + self.shutdown.trigger(); + } +} + +pub async fn wait_for_connectivity(comms: CommsNode) -> anyhow::Result<()> { + comms + .connectivity() + .wait_for_connectivity(Duration::from_secs(30)) + .await?; + Ok(()) +} diff --git a/integration_tests/tests/utils/chat_client/src/database.rs b/integration_tests/tests/utils/chat_client/src/database.rs new file mode 100644 index 00000000000..3b2d0f8215d --- /dev/null +++ b/integration_tests/tests/utils/chat_client/src/database.rs @@ -0,0 +1,55 @@ +// Copyright 2023. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{convert::TryInto, path::PathBuf}; + +use diesel::{Connection, SqliteConnection}; +use tari_common_sqlite::{ + connection::{DbConnection, DbConnectionUrl}, + error::StorageError, +}; +use tari_storage::lmdb_store::{LMDBBuilder, LMDBConfig}; +use tari_test_utils::random::string; + +pub fn create_chat_storage(base_path: PathBuf) -> Result { + std::fs::create_dir_all(&base_path).unwrap(); + let db_name = format!("{}.sqlite3", string(8).as_str()); + let db_path = format!("{}/{}", base_path.to_str().unwrap(), db_name); + let url: DbConnectionUrl = db_path.clone().try_into().unwrap(); + + // Create the db + let _db = SqliteConnection::establish(&db_path).unwrap_or_else(|_| panic!("Error connecting to {}", db_path)); + + DbConnection::connect_url(&url) +} + +pub fn create_peer_storage(base_path: PathBuf) { + std::fs::create_dir_all(&base_path).unwrap(); + + LMDBBuilder::new() + .set_path(&base_path) + .set_env_config(LMDBConfig::default()) + .set_max_number_of_databases(1) + .add_database("peerdb", lmdb_zero::db::CREATE) + .build() + .unwrap(); +} diff --git a/integration_tests/tests/utils/chat_client/src/lib.rs b/integration_tests/tests/utils/chat_client/src/lib.rs new file mode 100644 index 00000000000..561f08a5dfa --- /dev/null +++ b/integration_tests/tests/utils/chat_client/src/lib.rs @@ -0,0 +1,27 @@ +// Copyright 2023. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod client; +pub use client::Client; + +pub mod database; +pub mod networking; diff --git a/integration_tests/tests/utils/chat_client/src/networking.rs b/integration_tests/tests/utils/chat_client/src/networking.rs new file mode 100644 index 00000000000..0d0bee17018 --- /dev/null +++ b/integration_tests/tests/utils/chat_client/src/networking.rs @@ -0,0 +1,138 @@ +// Copyright 2023. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{path::PathBuf, sync::Arc, time::Duration}; + +use tari_common_sqlite::connection::DbConnection; +// Re-exports +pub use tari_comms::{ + multiaddr::Multiaddr, + peer_manager::{NodeIdentity, PeerFeatures}, +}; +use tari_comms::{peer_manager::Peer, CommsNode, UnspawnedCommsNode}; +use tari_comms_dht::{DhtConfig, NetworkDiscoveryConfig}; +use tari_contacts::contacts_service::{ + handle::ContactsServiceHandle, + storage::sqlite_db::ContactsServiceSqliteDatabase, + ContactsServiceInitializer, +}; +use tari_p2p::{ + comms_connector::pubsub_connector, + initialization::{spawn_comms_using_transport, P2pInitializer}, + services::liveness::{LivenessConfig, LivenessInitializer}, + Network, + P2pConfig, + PeerSeedsConfig, + TcpTransportConfig, + TransportConfig, +}; +use tari_service_framework::StackBuilder; +use tari_shutdown::ShutdownSignal; + +use crate::database; + +pub async fn start( + node_identity: Arc, + base_path: PathBuf, + seed_peers: Vec, + db: DbConnection, + shutdown_signal: ShutdownSignal, +) -> anyhow::Result<(ContactsServiceHandle, CommsNode)> { + database::create_peer_storage(base_path.clone()); + let backend = ContactsServiceSqliteDatabase::init(db); + + let (publisher, subscription_factory) = pubsub_connector(100, 50); + let in_msg = Arc::new(subscription_factory); + + let transport_config = TransportConfig::new_tcp(TcpTransportConfig { + listener_address: node_identity.first_public_address(), + ..TcpTransportConfig::default() + }); + + let mut config = P2pConfig { + datastore_path: base_path.clone(), + dht: DhtConfig { + network_discovery: NetworkDiscoveryConfig { + enabled: true, + ..NetworkDiscoveryConfig::default() + }, + ..DhtConfig::default_local_test() + }, + transport: transport_config.clone(), + allow_test_addresses: true, + public_addresses: vec![node_identity.first_public_address()], + ..P2pConfig::default() + }; + config.set_base_path(base_path.clone()); + + let seed_config = PeerSeedsConfig { + peer_seeds: seed_peers + .iter() + .map(|p| format!("{}::{}", p.public_key, p.addresses.best().unwrap().address())) + .collect::>() + .into(), + ..PeerSeedsConfig::default() + }; + + let fut = StackBuilder::new(shutdown_signal) + .add_initializer(P2pInitializer::new( + config, + seed_config, + Network::LocalNet, + node_identity, + publisher, + )) + .add_initializer(LivenessInitializer::new( + LivenessConfig { + auto_ping_interval: Some(Duration::from_secs(1)), + num_peers_per_round: 0, // No random peers + max_allowed_ping_failures: 0, // Peer with failed ping-pong will never be removed + ..Default::default() + }, + in_msg.clone(), + )) + .add_initializer(ContactsServiceInitializer::new( + backend, + in_msg, + Duration::from_secs(5), + 2, + )) + .build(); + + let mut handles = fut.await.expect("Service initialization failed"); + + let comms = handles + .take_handle::() + .expect("P2pInitializer was not added to the stack or did not add UnspawnedCommsNode"); + + let peer_manager = comms.peer_manager(); + for peer in seed_peers { + peer_manager.add_peer(peer).await?; + } + + let comms = spawn_comms_using_transport(comms, transport_config).await.unwrap(); + handles.register(comms); + + let comms = handles.expect_handle::(); + let contacts = handles.expect_handle::(); + Ok((contacts, comms)) +}