diff --git a/Cargo.lock b/Cargo.lock index 9100ddf10a..dfc881fb4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5653,6 +5653,7 @@ dependencies = [ "lmdb-zero", "log", "minotari_app_utilities", + "rand", "serde", "tari_common", "tari_common_sqlite", diff --git a/base_layer/chat_ffi/chat.h b/base_layer/chat_ffi/chat.h index 60a14a9dfc..507493ec9d 100644 --- a/base_layer/chat_ffi/chat.h +++ b/base_layer/chat_ffi/chat.h @@ -18,6 +18,8 @@ struct Confirmation; struct ContactsLivenessData; +struct ContactsServiceHandle; + struct ConversationalistsVector; struct Message; @@ -71,6 +73,37 @@ struct ChatClient *create_chat_client(struct ApplicationConfig *config, CallbackDeliveryConfirmationReceived callback_delivery_confirmation_received, CallbackReadConfirmationReceived callback_read_confirmation_received); +/** + * Side loads a chat client + * + * ## Arguments + * `config` - The ApplicationConfig pointer + * `contacts_handler` - A pointer to a contacts handler extracted from the wallet ffi + * `error_out` - Pointer to an int which will be modified + * `callback_contact_status_change` - A callback function pointer. this is called whenever a + * contacts liveness event comes in. + * `callback_message_received` - A callback function pointer. This is called whenever a chat + * message is received. + * `callback_delivery_confirmation_received` - A callback function pointer. This is called when the + * client receives a confirmation of message delivery. + * `callback_read_confirmation_received` - A callback function pointer. This is called when the + * client receives a confirmation of message read. + * + * ## Returns + * `*mut ChatClient` - Returns a pointer to a ChatClient, note that it returns ptr::null_mut() + * if any error was encountered or if the runtime could not be created. + * + * # Safety + * The ```destroy_chat_client``` method must be called when finished with a ClientFFI to prevent a memory leak + */ +struct ChatClient *sideload_chat_client(struct ApplicationConfig *config, + struct ContactsServiceHandle *contacts_handle, + int *error_out, + CallbackContactStatusChange callback_contact_status_change, + CallbackMessageReceived callback_message_received, + CallbackDeliveryConfirmationReceived callback_delivery_confirmation_received, + CallbackReadConfirmationReceived callback_read_confirmation_received); + /** * Frees memory for a ChatClient * diff --git a/base_layer/chat_ffi/src/lib.rs b/base_layer/chat_ffi/src/lib.rs index aa3fcf5815..dbea07cb37 100644 --- a/base_layer/chat_ffi/src/lib.rs +++ b/base_layer/chat_ffi/src/lib.rs @@ -29,6 +29,7 @@ use libc::c_int; use log::info; use minotari_app_utilities::identity_management::setup_node_identity; use tari_chat_client::{config::ApplicationConfig, networking::PeerFeatures, ChatClient as ChatClientTrait, Client}; +use tari_contacts::contacts_service::handle::ContactsServiceHandle; use tokio::runtime::Runtime; use crate::{ @@ -148,9 +149,109 @@ pub unsafe extern "C" fn create_chat_client( }; let mut client = Client::new(identity, (*config).clone()); + + if let Ok(()) = runtime.block_on(client.initialize()) { + let contacts_handler = match client.contacts.clone() { + Some(contacts_handler) => contacts_handler, + None => { + error = + LibChatError::from(InterfaceError::NullError("No contacts service loaded yet".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + }, + }; + + let mut callback_handler = CallbackHandler::new( + contacts_handler, + client.shutdown.to_signal(), + callback_contact_status_change, + callback_message_received, + callback_delivery_confirmation_received, + callback_read_confirmation_received, + ); + + runtime.spawn(async move { + callback_handler.start().await; + }); + } + + let client = ChatClient { client, runtime }; + + Box::into_raw(Box::new(client)) +} + +/// Side loads a chat client +/// +/// ## Arguments +/// `config` - The ApplicationConfig pointer +/// `contacts_handler` - A pointer to a contacts handler extracted from the wallet ffi +/// `error_out` - Pointer to an int which will be modified +/// `callback_contact_status_change` - A callback function pointer. this is called whenever a +/// contacts liveness event comes in. +/// `callback_message_received` - A callback function pointer. This is called whenever a chat +/// message is received. +/// `callback_delivery_confirmation_received` - A callback function pointer. This is called when the +/// client receives a confirmation of message delivery. +/// `callback_read_confirmation_received` - A callback function pointer. This is called when the +/// client receives a confirmation of message read. +/// +/// ## Returns +/// `*mut ChatClient` - Returns a pointer to a ChatClient, note that it returns ptr::null_mut() +/// if any error was encountered or if the runtime could not be created. +/// +/// # Safety +/// The ```destroy_chat_client``` method must be called when finished with a ClientFFI to prevent a memory leak +#[no_mangle] +pub unsafe extern "C" fn sideload_chat_client( + config: *mut ApplicationConfig, + contacts_handle: *mut ContactsServiceHandle, + error_out: *mut c_int, + callback_contact_status_change: CallbackContactStatusChange, + callback_message_received: CallbackMessageReceived, + callback_delivery_confirmation_received: CallbackDeliveryConfirmationReceived, + callback_read_confirmation_received: CallbackReadConfirmationReceived, +) -> *mut ChatClient { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if config.is_null() { + error = LibChatError::from(InterfaceError::NullError("config".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + } + + if let Some(log_path) = (*config).clone().chat_client.log_path { + init_logging(log_path, (*config).clone().chat_client.log_verbosity, error_out); + + if error > 0 { + return ptr::null_mut(); + } + } + info!( + target: LOG_TARGET, + "Sideloading Tari Chat FFI version: {}", + consts::APP_VERSION + ); + + let runtime = match Runtime::new() { + Ok(r) => r, + Err(e) => { + error = LibChatError::from(InterfaceError::TokioError(e.to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + }, + }; + + if contacts_handle.is_null() { + error = LibChatError::from(InterfaceError::NullError("contacts_handle".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + } + + let mut client = Client::sideload((*config).clone(), (*contacts_handle).clone()); if let Ok(()) = runtime.block_on(client.initialize()) { let mut callback_handler = CallbackHandler::new( - client.contacts.clone().expect("No contacts service loaded yet"), + (*contacts_handle).clone(), client.shutdown.to_signal(), callback_contact_status_change, callback_message_received, diff --git a/base_layer/contacts/src/chat_client/Cargo.toml b/base_layer/contacts/src/chat_client/Cargo.toml index 1a55a5cf9e..2d1365578c 100644 --- a/base_layer/contacts/src/chat_client/Cargo.toml +++ b/base_layer/contacts/src/chat_client/Cargo.toml @@ -26,5 +26,6 @@ config = { version = "0.13.0" } diesel = { version = "2.0.3", features = ["sqlite", "r2d2", "serde_json", "chrono", "64-column-tables"] } lmdb-zero = "0.4.4" log = "0.4.17" +rand = "0.8" serde = "1.0.136" thiserror = "1.0.50" diff --git a/base_layer/contacts/src/chat_client/src/client.rs b/base_layer/contacts/src/chat_client/src/client.rs index 1f1ca50d65..0e33079075 100644 --- a/base_layer/contacts/src/chat_client/src/client.rs +++ b/base_layer/contacts/src/chat_client/src/client.rs @@ -28,8 +28,9 @@ use std::{ use async_trait::async_trait; use log::debug; +use rand::rngs::OsRng; use tari_common_types::tari_address::TariAddress; -use tari_comms::{CommsNode, NodeIdentity}; +use tari_comms::{peer_manager::PeerFeatures, CommsNode, NodeIdentity}; use tari_contacts::contacts_service::{ handle::ContactsServiceHandle, service::ContactOnlineStatus, @@ -37,7 +38,7 @@ use tari_contacts::contacts_service::{ }; use tari_shutdown::Shutdown; -use crate::{config::ApplicationConfig, error::Error, networking}; +use crate::{config::ApplicationConfig, error::Error, networking, networking::Multiaddr}; const LOG_TARGET: &str = "contacts::chat_client"; @@ -51,7 +52,7 @@ pub trait ChatClient { async fn send_message(&self, message: Message) -> Result<(), Error>; async fn send_read_receipt(&self, message: Message) -> Result<(), Error>; async fn get_conversationalists(&self) -> Result, Error>; - fn identity(&self) -> &NodeIdentity; + fn address(&self) -> TariAddress; fn shutdown(&mut self); } @@ -88,26 +89,45 @@ impl Client { } } - pub async fn initialize(&mut self) -> Result<(), Error> { - debug!(target: LOG_TARGET, "initializing chat"); + pub fn sideload(config: ApplicationConfig, contacts: ContactsServiceHandle) -> Self { + // Create a placeholder ID. It won't be written or used when sideloaded. + let identity = Arc::new(NodeIdentity::random( + &mut OsRng, + Multiaddr::empty(), + PeerFeatures::COMMUNICATION_NODE, + )); - let signal = self.shutdown.to_signal(); + Self { + config, + contacts: Some(contacts), + identity, + shutdown: Shutdown::new(), + } + } - let (contacts, comms_node) = networking::start(self.identity.clone(), self.config.clone(), signal) - .await - .map_err(|e| Error::InitializationError(e.to_string()))?; + pub async fn initialize(&mut self) -> Result<(), Error> { + debug!(target: LOG_TARGET, "initializing chat"); - if !self.config.peer_seeds.peer_seeds.is_empty() { - loop { - debug!(target: LOG_TARGET, "Waiting for peer connections..."); - match wait_for_connectivity(comms_node.clone()).await { - Ok(_) => break, - Err(e) => debug!(target: LOG_TARGET, "{}. Still waiting...", e), + // Only run the networking if we're operating as a standalone client. If we're sideloading we can skip all this + if self.contacts.is_none() { + let signal = self.shutdown.to_signal(); + + let (contacts, comms_node) = networking::start(self.identity.clone(), self.config.clone(), signal) + .await + .map_err(|e| Error::InitializationError(e.to_string()))?; + + if !self.config.peer_seeds.peer_seeds.is_empty() { + loop { + debug!(target: LOG_TARGET, "Waiting for peer connections..."); + match wait_for_connectivity(comms_node.clone()).await { + Ok(_) => break, + Err(e) => debug!(target: LOG_TARGET, "{}. Still waiting...", e), + } } } - } - self.contacts = Some(contacts); + self.contacts = Some(contacts); + } debug!(target: LOG_TARGET, "Connections established"); @@ -121,8 +141,8 @@ impl Client { #[async_trait] impl ChatClient for Client { - fn identity(&self) -> &NodeIdentity { - &self.identity + fn address(&self) -> TariAddress { + TariAddress::from_public_key(self.identity.public_key(), self.config.chat_client.network) } fn shutdown(&mut self) { diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index f382acbcc1..aa6871d35d 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -128,7 +128,7 @@ use tari_comms::{ types::CommsPublicKey, }; use tari_comms_dht::{store_forward::SafConfig, DbConnectionUrl, DhtConfig}; -use tari_contacts::contacts_service::types::Contact; +use tari_contacts::contacts_service::{handle::ContactsServiceHandle, types::Contact}; use tari_core::{ borsh::FromBytes, consensus::ConsensusManager, @@ -4985,6 +4985,7 @@ pub unsafe extern "C" fn public_keys_get_at( #[allow(clippy::too_many_lines)] unsafe fn init_logging( log_path: *const c_char, + log_verbosity: c_int, num_rolling_log_files: c_uint, size_per_log_file_bytes: c_uint, error_out: *mut c_int, @@ -4998,6 +4999,17 @@ unsafe fn init_logging( ptr::swap(error_out, &mut error as *mut c_int); return; } + + let log_level = match log_verbosity { + 0 => LevelFilter::Off, + 1 => LevelFilter::Error, + 2 => LevelFilter::Warn, + 3 => LevelFilter::Info, + 4 => LevelFilter::Debug, + 5 | 11 => LevelFilter::Trace, // Cranked up to 11 + _ => LevelFilter::Warn, + }; + let path = v.unwrap().to_owned(); let encoder = PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S.%f)} [{t}] {l:5} {m}{n}"); let log_appender: Box = if num_rolling_log_files != 0 && size_per_log_file_bytes != 0 { @@ -5043,57 +5055,57 @@ unsafe fn init_logging( Logger::builder() .appender("logfile") .additive(false) - .build("comms", LevelFilter::Warn), + .build("comms", log_level), ) .logger( Logger::builder() .appender("logfile") .additive(false) - .build("comms::noise", LevelFilter::Warn), + .build("comms::noise", log_level), ) .logger( Logger::builder() .appender("logfile") .additive(false) - .build("tokio_util", LevelFilter::Warn), + .build("tokio_util", log_level), ) .logger( Logger::builder() .appender("logfile") .additive(false) - .build("tracing", LevelFilter::Warn), + .build("tracing", log_level), ) .logger( Logger::builder() .appender("logfile") .additive(false) - .build("wallet::transaction_service::callback_handler", LevelFilter::Warn), + .build("wallet::transaction_service::callback_handler", log_level), ) .logger( Logger::builder() .appender("logfile") .additive(false) - .build("p2p", LevelFilter::Warn), + .build("p2p", log_level), ) .logger( Logger::builder() .appender("logfile") .additive(false) - .build("yamux", LevelFilter::Warn), + .build("yamux", log_level), ) .logger( Logger::builder() .appender("logfile") .additive(false) - .build("dht", LevelFilter::Warn), + .build("dht", log_level), ) .logger( Logger::builder() .appender("logfile") .additive(false) - .build("mio", LevelFilter::Warn), + .build("mio", log_level), ) - .build(Root::builder().appender("logfile").build(LevelFilter::Debug)) + .build(Root::builder().appender("logfile").build(log_level)) .expect("Should be able to create a Config"); match log4rs::init_config(lconfig) { @@ -5108,6 +5120,13 @@ unsafe fn init_logging( /// `config` - The TariCommsConfig pointer /// `log_path` - An optional file path to the file where the logs will be written. If no log is required pass *null* /// pointer. +/// `log_verbosity` - how verbose should logging be as a c_int 0-5, or 11 +/// 0 => Off +/// 1 => Error +/// 2 => Warn +/// 3 => Info +/// 4 => Debug +/// 5 | 11 => Trace // Cranked up to 11 /// `num_rolling_log_files` - Specifies how many rolling log files to produce, if no rolling files are wanted then set /// this to 0 /// `size_per_log_file_bytes` - Specifies the size, in bytes, at which the logs files will roll over, if no @@ -5200,11 +5219,13 @@ unsafe fn init_logging( pub unsafe extern "C" fn wallet_create( config: *mut TariCommsConfig, log_path: *const c_char, + log_verbosity: c_int, num_rolling_log_files: c_uint, size_per_log_file_bytes: c_uint, passphrase: *const c_char, seed_words: *const TariSeedWords, network_str: *const c_char, + callback_received_transaction: unsafe extern "C" fn(*mut TariPendingInboundTransaction), callback_received_transaction_reply: unsafe extern "C" fn(*mut TariCompletedTransaction), callback_received_finalized_transaction: unsafe extern "C" fn(*mut TariCompletedTransaction), @@ -5236,7 +5257,13 @@ pub unsafe extern "C" fn wallet_create( } if !log_path.is_null() { - init_logging(log_path, num_rolling_log_files, size_per_log_file_bytes, error_out); + init_logging( + log_path, + log_verbosity, + num_rolling_log_files, + size_per_log_file_bytes, + error_out, + ); if error > 0 { return ptr::null_mut(); @@ -8573,6 +8600,47 @@ pub unsafe extern "C" fn fee_per_gram_stat_destroy(fee_per_gram_stat: *mut TariF } } +/// Returns a ptr to the ContactsServiceHandle for use with chat +/// +/// ## Arguments +/// `wallet` - The wallet instance +/// `error_out` - Pointer to an int which will be modified +/// +/// ## Returns +/// `*mut ContactsServiceHandle` an opaque pointer used in chat sideloading initialization +/// +/// # Safety +/// You should release the returned pointer after it's been used to initialize chat using `contacts_handle_destroy` +#[no_mangle] +pub unsafe extern "C" fn contacts_handle(wallet: *mut TariWallet, error_out: *mut c_int) -> *mut ContactsServiceHandle { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if wallet.is_null() { + error = LibWalletError::from(InterfaceError::NullError("wallet".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + } + + Box::into_raw(Box::new((*wallet).wallet.contacts_service.clone())) +} + +/// Frees memory for a ContactsServiceHandle +/// +/// ## Arguments +/// `contacts_handle` - The pointer to a ContactsServiceHandle +/// +/// ## Returns +/// `()` - Does not return a value, equivalent to void in C +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn contacts_handle_destroy(contacts_handle: *mut ContactsServiceHandle) { + if !contacts_handle.is_null() { + drop(Box::from_raw(contacts_handle)) + } +} /// ------------------------------------------------------------------------------------------ /// #[cfg(test)] mod test { @@ -9427,6 +9495,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), alice_network_str, @@ -9470,6 +9539,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), alice_network_str, @@ -9582,6 +9652,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), network_str, @@ -9805,6 +9876,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), network_str, @@ -9867,6 +9939,7 @@ mod test { log_path, 0, 0, + 0, passphrase, seed_words, network_str, @@ -9943,6 +10016,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), network_str, @@ -10091,6 +10165,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), network_str, @@ -10208,6 +10283,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), network_str, @@ -10408,6 +10484,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), network_str, @@ -10615,6 +10692,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), network_str, @@ -10865,6 +10943,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), network_str, @@ -11102,6 +11181,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), alice_network_str, @@ -11161,6 +11241,7 @@ mod test { ptr::null(), 0, 0, + 0, passphrase, ptr::null(), bob_network_str, diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index db1a525a42..329e4fa086 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -72,6 +72,8 @@ struct Contact; struct ContactsLivenessData; +struct ContactsServiceHandle; + /** * A covenant allows a UTXO to specify some restrictions on how it is spent in a future transaction. * See https://rfc.tari.com/RFC-0250_Covenants.html for details. @@ -2584,6 +2586,13 @@ TariPublicKey *public_keys_get_at(const struct TariPublicKeys *public_keys, * `config` - The TariCommsConfig pointer * `log_path` - An optional file path to the file where the logs will be written. If no log is required pass *null* * pointer. + * `log_verbosity` - how verbose should logging be as a c_int 0-5, or 11 + * 0 => Off + * 1 => Error + * 2 => Warn + * 3 => Info + * 4 => Debug + * 5 | 11 => Trace // Cranked up to 11 * `num_rolling_log_files` - Specifies how many rolling log files to produce, if no rolling files are wanted then set * this to 0 * `size_per_log_file_bytes` - Specifies the size, in bytes, at which the logs files will roll over, if no @@ -2673,6 +2682,7 @@ TariPublicKey *public_keys_get_at(const struct TariPublicKeys *public_keys, */ struct TariWallet *wallet_create(TariCommsConfig *config, const char *log_path, + int log_verbosity, unsigned int num_rolling_log_files, unsigned int size_per_log_file_bytes, const char *passphrase, @@ -3918,6 +3928,36 @@ unsigned long long fee_per_gram_stat_get_max_fee_per_gram(TariFeePerGramStat *fe */ void fee_per_gram_stat_destroy(TariFeePerGramStat *fee_per_gram_stat); +/** + * Returns a ptr to the ContactsServiceHandle for use with chat + * + * ## Arguments + * `wallet` - The wallet instance + * `error_out` - Pointer to an int which will be modified + * + * ## Returns + * `*mut ContactsServiceHandle` an opaque pointer used in chat sideloading initialization + * + * # Safety + * You should release the returned pointer after it's been used to initialize chat using `contacts_handle_destroy` + */ +struct ContactsServiceHandle *contacts_handle(struct TariWallet *wallet, + int *error_out); + +/** + * Frees memory for a ContactsServiceHandle + * + * ## Arguments + * `contacts_handle` - The pointer to a ContactsServiceHandle + * + * ## Returns + * `()` - Does not return a value, equivalent to void in C + * + * # Safety + * None + */ +void contacts_handle_destroy(struct ContactsServiceHandle *contacts_handle); + /** * Extracts a `NodeId` represented as a vector of bytes wrapped into a `ByteVector` * diff --git a/integration_tests/src/chat_ffi.rs b/integration_tests/src/chat_ffi.rs index 5a61cc57cb..6ecd2d0be7 100644 --- a/integration_tests/src/chat_ffi.rs +++ b/integration_tests/src/chat_ffi.rs @@ -35,11 +35,11 @@ type ClientFFI = c_void; use libc::{c_char, c_int, c_uchar, c_uint}; use minotari_app_utilities::identity_management::setup_node_identity; use tari_chat_client::{database, error::Error as ClientError, ChatClient}; +use tari_common::configuration::Network; use tari_common_types::tari_address::TariAddress; use tari_comms::{ multiaddr::Multiaddr, peer_manager::{Peer, PeerFeatures}, - NodeIdentity, }; use tari_contacts::contacts_service::{ service::ContactOnlineStatus, @@ -79,6 +79,15 @@ extern "C" { callback_delivery_confirmation_received: unsafe extern "C" fn(*mut c_void), callback_read_confirmation_received: unsafe extern "C" fn(*mut c_void), ) -> *mut ClientFFI; + pub fn sideload_chat_client( + config: *mut c_void, + contact_handle: *mut c_void, + error_out: *const c_int, + callback_contact_status_change: unsafe extern "C" fn(*mut c_void), + callback_message_received: unsafe extern "C" fn(*mut c_void), + callback_delivery_confirmation_received: unsafe extern "C" fn(*mut c_void), + callback_read_confirmation_received: unsafe extern "C" fn(*mut c_void), + ) -> *mut ClientFFI; pub fn create_chat_message(receiver: *mut c_void, message: *const c_char, error_out: *const c_int) -> *mut c_void; pub fn send_chat_message(client: *mut ClientFFI, message: *mut c_void, error_out: *const c_int); pub fn add_chat_message_metadata( @@ -113,7 +122,7 @@ unsafe impl Send for PtrWrapper {} #[derive(Debug)] pub struct ChatFFI { ptr: Arc>, - pub identity: Arc, + pub address: TariAddress, } struct Conversationalists(Vec); @@ -236,8 +245,8 @@ impl ChatClient for ChatFFI { Ok(addresses) } - fn identity(&self) -> &NodeIdentity { - &self.identity + fn address(&self) -> TariAddress { + self.address.clone() } fn shutdown(&mut self) { @@ -296,10 +305,41 @@ pub async fn spawn_ffi_chat_client(name: &str, seed_peers: Vec, base_dir: ChatFFI { ptr: Arc::new(Mutex::new(PtrWrapper(client_ptr))), - identity, + address: TariAddress::from_public_key(identity.public_key(), Network::LocalNet), } } +pub async fn sideload_ffi_chat_client( + address: TariAddress, + base_dir: PathBuf, + contacts_handle_ptr: *mut c_void, +) -> ChatFFI { + let mut config = test_config(Multiaddr::empty()); + config.chat_client.set_base_path(base_dir); + + let config_ptr = Box::into_raw(Box::new(config)) as *mut c_void; + + let client_ptr; + let error_out = Box::into_raw(Box::new(0)); + unsafe { + *ChatCallback::instance().contact_status_change.lock().unwrap() = 0; + + client_ptr = sideload_chat_client( + config_ptr, + contacts_handle_ptr, + error_out, + callback_contact_status_change, + callback_message_received, + callback_delivery_confirmation_received, + callback_read_confirmation_received, + ); + } + + ChatFFI { + ptr: Arc::new(Mutex::new(PtrWrapper(client_ptr))), + address, + } +} static mut INSTANCE: Option = None; static START: Once = Once::new(); diff --git a/integration_tests/src/ffi/ffi_import.rs b/integration_tests/src/ffi/ffi_import.rs index ce317c8e47..5e88c0e3d4 100644 --- a/integration_tests/src/ffi/ffi_import.rs +++ b/integration_tests/src/ffi/ffi_import.rs @@ -380,6 +380,7 @@ extern "C" { pub fn wallet_create( config: *mut TariCommsConfig, log_path: *const c_char, + log_level: c_int, num_rolling_log_files: c_uint, size_per_log_file_bytes: c_uint, passphrase: *const c_char, @@ -590,4 +591,5 @@ extern "C" { error_out: *mut c_int, ) -> c_ulonglong; pub fn fee_per_gram_stat_destroy(fee_per_gram_stat: *mut TariFeePerGramStat); + pub fn contacts_handle(wallet: *mut TariWallet, error_out: *mut c_int) -> *mut c_void; } diff --git a/integration_tests/src/ffi/wallet.rs b/integration_tests/src/ffi/wallet.rs index eb0066280b..5a9ec92f05 100644 --- a/integration_tests/src/ffi/wallet.rs +++ b/integration_tests/src/ffi/wallet.rs @@ -172,6 +172,7 @@ impl Wallet { ptr = wallet_create( comms_config.get_ptr(), CString::new(log_path).unwrap().into_raw(), + 11, 50, 104857600, // 100 MB CString::new("kensentme").unwrap().into_raw(), @@ -432,4 +433,16 @@ impl Wallet { } FeePerGramStats::from_ptr(ptr) } + + pub fn contacts_handle(&self) -> *mut c_void { + let ptr; + let mut error = 0; + unsafe { + ptr = ffi_import::contacts_handle(self.ptr, &mut error); + if error > 0 { + println!("contacts_handle error {}", error); + } + } + ptr + } } diff --git a/integration_tests/src/wallet_ffi.rs b/integration_tests/src/wallet_ffi.rs index a280d7968c..601f4b3485 100644 --- a/integration_tests/src/wallet_ffi.rs +++ b/integration_tests/src/wallet_ffi.rs @@ -55,8 +55,7 @@ use crate::{ pub struct WalletFFI { pub name: String, pub port: u64, - // pub grpc_port: u64, - // pub temp_dir_path: String, + pub base_dir: PathBuf, pub wallet: Arc>, } @@ -76,7 +75,12 @@ impl WalletFFI { .unwrap() .into(); let wallet = ffi::Wallet::create(comms_config, log_path, seed_words_ptr); - Self { name, port, wallet } + Self { + name, + port, + base_dir: base_dir_path, + wallet, + } } pub fn identify(&self) -> String { @@ -186,6 +190,10 @@ impl WalletFFI { pub fn get_fee_per_gram_stats(&self, count: u32) -> FeePerGramStats { self.wallet.lock().unwrap().get_fee_per_gram_stats(count) } + + pub fn contacts_handle(&self) -> *mut c_void { + self.wallet.lock().unwrap().contacts_handle() + } } pub fn spawn_wallet_ffi(world: &mut TariWorld, wallet_name: String, seed_words_ptr: *const c_void) { diff --git a/integration_tests/tests/features/ChatFFI.feature b/integration_tests/tests/features/ChatFFI.feature index 304ed2e05e..989e8130ee 100644 --- a/integration_tests/tests/features/ChatFFI.feature +++ b/integration_tests/tests/features/ChatFFI.feature @@ -105,3 +105,11 @@ Feature: Chat FFI messaging When CHAT_A will have 1 message with CHAT_C When CHAT_A will have 1 message with CHAT_D Then CHAT_A will have 3 conversationalists + + Scenario: A message is propagated between side loaded chat and client via 3rd party + Given I have a seed node SEED_A + Given I have a ffi wallet WALLET_A connected to base node SEED_A + When I have a sideloaded chat FFI client CHAT_A from WALLET_A + When I have a chat FFI client CHAT_B connected to seed node SEED_A + When I use CHAT_A to send a message 'Hey there' to CHAT_B + Then CHAT_B will have 1 message with CHAT_A \ No newline at end of file diff --git a/integration_tests/tests/steps/chat_ffi_steps.rs b/integration_tests/tests/steps/chat_ffi_steps.rs index b3148d817a..35acd32845 100644 --- a/integration_tests/tests/steps/chat_ffi_steps.rs +++ b/integration_tests/tests/steps/chat_ffi_steps.rs @@ -23,8 +23,9 @@ use std::time::Duration; use cucumber::{then, when}; +use tari_common_types::tari_address::TariAddress; use tari_integration_tests::{ - chat_ffi::{spawn_ffi_chat_client, ChatCallback}, + chat_ffi::{sideload_ffi_chat_client, spawn_ffi_chat_client, ChatCallback}, TariWorld, }; @@ -43,6 +44,16 @@ async fn chat_ffi_client_connected_to_base_node(world: &mut TariWorld, name: Str world.chat_clients.insert(name, Box::new(client)); } +#[when(expr = "I have a sideloaded chat FFI client {word} from {word}")] +async fn sideloaded_chat_ffi_client_connected_to_wallet(world: &mut TariWorld, chat_name: String, wallet_name: String) { + let wallet = world.get_ffi_wallet(&wallet_name).unwrap(); + let pubkey = world.get_wallet_address(&wallet_name).await.unwrap(); + let address = TariAddress::from_hex(&pubkey).unwrap(); + + let client = sideload_ffi_chat_client(address, wallet.base_dir.clone(), wallet.contacts_handle()).await; + world.chat_clients.insert(chat_name, Box::new(client)); +} + #[then(expr = "there will be a contact status update callback of at least {int}")] async fn contact_status_update_callback(_world: &mut TariWorld, callback_count: usize) { let mut count = 0; diff --git a/integration_tests/tests/steps/chat_steps.rs b/integration_tests/tests/steps/chat_steps.rs index f7745e7819..7b7d6aa525 100644 --- a/integration_tests/tests/steps/chat_steps.rs +++ b/integration_tests/tests/steps/chat_steps.rs @@ -23,8 +23,6 @@ use std::{cmp::Ordering, convert::TryFrom, time::Duration}; use cucumber::{then, when}; -use tari_common::configuration::Network; -use tari_common_types::tari_address::TariAddress; use tari_contacts::contacts_service::{ handle::{DEFAULT_MESSAGE_LIMIT, DEFAULT_MESSAGE_PAGE}, service::ContactOnlineStatus, @@ -71,9 +69,8 @@ async fn send_message_to( ) -> anyhow::Result<()> { 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); - let message = sender.create_message(&address, message); + let message = sender.create_message(&receiver.address(), message); sender.send_message(message).await?; Ok(()) @@ -89,7 +86,7 @@ async fn i_reply_to_message( ) -> anyhow::Result<()> { 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); + let address = receiver.address(); for _ in 0..(TWO_MINUTES_WITH_HALF_SECOND_SLEEP) { let messages: Vec = (*sender) @@ -132,7 +129,7 @@ async fn receive_n_messages( ) -> anyhow::Result<()> { 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 address = sender.address(); let mut messages = vec![]; for _ in 0..(TWO_MINUTES_WITH_HALF_SECOND_SLEEP) { @@ -149,7 +146,7 @@ async fn receive_n_messages( panic!( "Receiver {} only received {}/{} messages", - (*receiver).identity().node_id(), + address, messages.len(), message_count ) @@ -160,9 +157,7 @@ async fn add_as_contact(world: &mut TariWorld, sender: String, receiver: String) let receiver = world.chat_clients.get(&receiver).unwrap(); let sender = world.chat_clients.get(&sender).unwrap(); - let address = TariAddress::from_public_key(receiver.identity().public_key(), Network::LocalNet); - - sender.add_contact(&address).await?; + sender.add_contact(&receiver.address()).await?; Ok(()) } @@ -171,11 +166,10 @@ async fn wait_for_contact_to_be_online(world: &mut TariWorld, client: String, co let client = world.chat_clients.get(&client).unwrap(); let contact = world.chat_clients.get(&contact).unwrap(); - let address = TariAddress::from_public_key(contact.identity().public_key(), Network::LocalNet); let mut last_status = ContactOnlineStatus::Banned("No result came back".to_string()); for _ in 0..(TWO_MINUTES_WITH_HALF_SECOND_SLEEP / 4) { - last_status = client.check_online_status(&address).await?; + last_status = client.check_online_status(&contact.address()).await?; if ContactOnlineStatus::Online == last_status { return Ok(()); } @@ -185,7 +179,7 @@ async fn wait_for_contact_to_be_online(world: &mut TariWorld, client: String, co panic!( "Contact {} never came online, status is: {}", - contact.identity().node_id(), + contact.address(), last_status ) } @@ -199,7 +193,7 @@ async fn have_replied_message( ) -> anyhow::Result<()> { 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 address = sender.address(); for _ in 0..(TWO_MINUTES_WITH_HALF_SECOND_SLEEP) { let messages: Vec = (*receiver) @@ -254,8 +248,8 @@ async fn matching_delivery_timestamps( ) -> anyhow::Result<()> { let client_1 = world.chat_clients.get(&sender).unwrap(); let client_2 = world.chat_clients.get(&receiver).unwrap(); - let client_1_address = TariAddress::from_public_key(client_1.identity().public_key(), Network::LocalNet); - let client_2_address = TariAddress::from_public_key(client_2.identity().public_key(), Network::LocalNet); + let client_1_address = client_1.address(); + let client_2_address = client_2.address(); for _a in 0..(TWO_MINUTES_WITH_HALF_SECOND_SLEEP) { let client_1_messages: Vec = (*client_1) @@ -313,8 +307,8 @@ async fn matching_read_timestamps( ) -> anyhow::Result<()> { let client_1 = world.chat_clients.get(&sender).unwrap(); let client_2 = world.chat_clients.get(&receiver).unwrap(); - let client_1_address = TariAddress::from_public_key(client_1.identity().public_key(), Network::LocalNet); - let client_2_address = TariAddress::from_public_key(client_2.identity().public_key(), Network::LocalNet); + let client_1_address = client_1.address(); + let client_2_address = client_2.address(); for _a in 0..(TWO_MINUTES_WITH_HALF_SECOND_SLEEP) { let client_1_messages: Vec = (*client_1) @@ -371,11 +365,10 @@ async fn matching_read_timestamps( async fn send_read_receipt(world: &mut TariWorld, sender: String, receiver: String, msg: String) -> anyhow::Result<()> { let client_1 = world.chat_clients.get(&receiver).unwrap(); let client_2 = world.chat_clients.get(&sender).unwrap(); - let client_2_address = TariAddress::from_public_key(client_2.identity().public_key(), Network::LocalNet); for _a in 0..(TWO_MINUTES_WITH_HALF_SECOND_SLEEP) { let messages: Vec = (*client_1) - .get_messages(&client_2_address, DEFAULT_MESSAGE_LIMIT, DEFAULT_MESSAGE_PAGE) + .get_messages(&client_2.address(), DEFAULT_MESSAGE_LIMIT, DEFAULT_MESSAGE_PAGE) .await?; if messages.is_empty() {