diff --git a/base_layer/wallet/src/base_node_service/monitor.rs b/base_layer/wallet/src/base_node_service/monitor.rs index e573fe1f89..9b36b555af 100644 --- a/base_layer/wallet/src/base_node_service/monitor.rs +++ b/base_layer/wallet/src/base_node_service/monitor.rs @@ -91,6 +91,7 @@ where Err(e @ BaseNodeMonitorError::RpcFailed(_)) => { warn!(target: LOG_TARGET, "Connectivity failure to base node: {}", e); self.update_state(BaseNodeState { + node_id: None, chain_metadata: None, is_synced: None, updated: None, @@ -169,6 +170,7 @@ where let height_of_longest_chain = chain_metadata.height_of_longest_chain(); self.update_state(BaseNodeState { + node_id: Some(base_node_id.clone()), chain_metadata: Some(chain_metadata), is_synced: Some(is_synced), updated: Some(Utc::now().naive_utc()), diff --git a/base_layer/wallet/src/base_node_service/service.rs b/base_layer/wallet/src/base_node_service/service.rs index cdd8ba0d71..67c2aec2f2 100644 --- a/base_layer/wallet/src/base_node_service/service.rs +++ b/base_layer/wallet/src/base_node_service/service.rs @@ -26,6 +26,7 @@ use chrono::NaiveDateTime; use futures::{future, StreamExt}; use log::*; use tari_common_types::chain_metadata::ChainMetadata; +use tari_comms::peer_manager::NodeId; use tari_service_framework::reply_channel::Receiver; use tari_shutdown::ShutdownSignal; use tokio::sync::RwLock; @@ -46,6 +47,7 @@ const LOG_TARGET: &str = "wallet::base_node_service::service"; /// State determined from Base Node Service Requests #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct BaseNodeState { + pub node_id: Option, pub chain_metadata: Option, pub is_synced: Option, pub updated: Option, diff --git a/base_layer/wallet/tests/support/base_node_service_mock.rs b/base_layer/wallet/tests/support/base_node_service_mock.rs index 33eea93822..c9c7544bfb 100644 --- a/base_layer/wallet/tests/support/base_node_service_mock.rs +++ b/base_layer/wallet/tests/support/base_node_service_mock.rs @@ -86,6 +86,7 @@ impl MockBaseNodeService { }; self.state = BaseNodeState { + node_id: None, chain_metadata, is_synced, updated: None, @@ -96,6 +97,7 @@ impl MockBaseNodeService { pub fn set_default_base_node_state(&mut self) { let metadata = ChainMetadata::new(i64::MAX as u64, FixedHash::zero(), 0, 0, 0, 0); self.state = BaseNodeState { + node_id: None, chain_metadata: Some(metadata), is_synced: Some(true), updated: None, diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index 6c442e0805..3265520b29 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -38,11 +38,15 @@ use std::{ops::Deref, sync::Arc}; use log::*; -use tari_common_types::{tari_address::TariAddress, transaction::TxId}; +use tari_common_types::{tari_address::TariAddress, transaction::TxId, types::BlockHash}; use tari_comms_dht::event::{DhtEvent, DhtEventReceiver}; use tari_contacts::contacts_service::handle::{ContactsLivenessData, ContactsLivenessEvent}; use tari_shutdown::ShutdownSignal; use tari_wallet::{ + base_node_service::{ + handle::{BaseNodeEvent, BaseNodeEventReceiver}, + service::BaseNodeState, + }, connectivity_service::OnlineStatus, output_manager_service::{ handle::{OutputManagerEvent, OutputManagerEventReceiver, OutputManagerHandle}, @@ -58,6 +62,8 @@ use tari_wallet::{ }; use tokio::sync::{broadcast, watch}; +use crate::ffi_basenode_state::TariBaseNodeState; + const LOG_TARGET: &str = "wallet::transaction_service::callback_handler"; pub struct CallbackHandler @@ -79,7 +85,9 @@ where TBackend: TransactionBackend + 'static callback_transaction_validation_complete: unsafe extern "C" fn(u64, u64), callback_saf_messages_received: unsafe extern "C" fn(), callback_connectivity_status: unsafe extern "C" fn(u64), + callback_base_node_state: unsafe extern "C" fn(*mut TariBaseNodeState), db: TransactionDatabase, + base_node_service_event_stream: BaseNodeEventReceiver, transaction_service_event_stream: TransactionEventReceiver, output_manager_service_event_stream: OutputManagerEventReceiver, output_manager_service: OutputManagerHandle, @@ -97,6 +105,7 @@ where TBackend: TransactionBackend + 'static #[allow(clippy::too_many_arguments)] pub fn new( db: TransactionDatabase, + base_node_service_event_stream: BaseNodeEventReceiver, transaction_service_event_stream: TransactionEventReceiver, output_manager_service_event_stream: OutputManagerEventReceiver, output_manager_service: OutputManagerHandle, @@ -121,6 +130,7 @@ where TBackend: TransactionBackend + 'static callback_transaction_validation_complete: unsafe extern "C" fn(u64, u64), callback_saf_messages_received: unsafe extern "C" fn(), callback_connectivity_status: unsafe extern "C" fn(u64), + callback_base_node_state: unsafe extern "C" fn(*mut TariBaseNodeState), ) -> Self { info!( target: LOG_TARGET, @@ -204,7 +214,9 @@ where TBackend: TransactionBackend + 'static callback_transaction_validation_complete, callback_saf_messages_received, callback_connectivity_status, + callback_base_node_state, db, + base_node_service_event_stream, transaction_service_event_stream, output_manager_service_event_stream, output_manager_service, @@ -295,6 +307,7 @@ where TBackend: TransactionBackend + 'static Err(_e) => error!(target: LOG_TARGET, "Error reading from Transaction Service event broadcast channel"), } }, + result = self.output_manager_service_event_stream.recv() => { match result { Ok(msg) => { @@ -318,6 +331,7 @@ where TBackend: TransactionBackend + 'static Err(_e) => error!(target: LOG_TARGET, "Error reading from Output Manager Service event broadcast channel"), } }, + result = self.dht_event_stream.recv() => { match result { Ok(msg) => { @@ -329,11 +343,32 @@ where TBackend: TransactionBackend + 'static Err(_e) => error!(target: LOG_TARGET, "Error reading from DHT event broadcast channel"), } } + Ok(_) = self.connectivity_status_watch.changed() => { let status = *self.connectivity_status_watch.borrow(); trace!(target: LOG_TARGET, "Connectivity status change detected: {:?}", status); self.connectivity_status_changed(status); }, + + event = self.base_node_service_event_stream.recv() => { + match event { + Ok(msg) => { + trace!(target: LOG_TARGET, "Base Node Service Callback Handler event {:?}", msg); + match (*msg).clone() { + BaseNodeEvent::BaseNodeStateChanged(state) => { + trace!("base node state changed: {:#?}", state); + self.base_node_state_changed(state); + }, + + BaseNodeEvent::NewBlockDetected(_new_block_number) => { + // + }, + } + }, + Err(_e) => error!(target: LOG_TARGET, "failed to receive base node state event"), + } + }, + event = self.contacts_liveness_events.recv() => { match event { Ok(liveness_event) => { @@ -613,4 +648,38 @@ where TBackend: TransactionBackend + 'static (self.callback_connectivity_status)(status as u64); } } + + fn base_node_state_changed(&mut self, state: BaseNodeState) { + debug!(target: LOG_TARGET, "Calling Base Node State changed callback function"); + + let state = match state.chain_metadata { + None => TariBaseNodeState { + node_id: state.node_id, + height_of_longest_chain: 0, + best_block: BlockHash::zero(), + best_block_timestamp: 0, + pruning_horizon: 0, + pruned_height: 0, + is_node_synced: false, + updated_at: 0, + latency: 0, + }, + + Some(chain_metadata) => TariBaseNodeState { + node_id: state.node_id, + height_of_longest_chain: chain_metadata.height_of_longest_chain(), + best_block: *chain_metadata.best_block(), + best_block_timestamp: chain_metadata.timestamp(), + pruning_horizon: chain_metadata.pruning_horizon(), + pruned_height: chain_metadata.pruned_height(), + is_node_synced: state.is_synced.unwrap_or(false), + updated_at: state.updated.map(|ts| ts.timestamp_millis() as u64).unwrap_or(0), + latency: state.latency.map(|d| d.as_millis() as u64).unwrap_or(0), + }, + }; + + unsafe { + (self.callback_base_node_state)(Box::into_raw(Box::new(state))); + } + } } diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index 13fbf90639..0fc474ca4c 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -7,7 +7,7 @@ mod test { mem::size_of, sync::{Arc, Mutex}, thread, - time::Duration, + time::{Duration, SystemTime}, }; use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305}; @@ -15,10 +15,12 @@ mod test { use rand::{rngs::OsRng, RngCore}; use tari_common::configuration::Network; use tari_common_types::{ + chain_metadata::ChainMetadata, tari_address::TariAddress, transaction::{TransactionDirection, TransactionStatus}, types::{BlindingFactor, PrivateKey, PublicKey}, }; + use tari_comms::peer_manager::NodeId; use tari_comms_dht::event::DhtEvent; use tari_contacts::contacts_service::{ handle::{ContactsLivenessData, ContactsLivenessEvent}, @@ -35,6 +37,7 @@ mod test { use tari_service_framework::reply_channel; use tari_shutdown::Shutdown; use tari_wallet::{ + base_node_service::{handle::BaseNodeEvent, service::BaseNodeState}, connectivity_service::OnlineStatus, output_manager_service::{ handle::{OutputManagerEvent, OutputManagerHandle}, @@ -56,7 +59,11 @@ mod test { time::Instant, }; - use crate::{callback_handler::CallbackHandler, output_manager_service_mock::MockOutputManagerService}; + use crate::{ + callback_handler::CallbackHandler, + ffi_basenode_state::TariBaseNodeState, + output_manager_service_mock::MockOutputManagerService, + }; #[derive(Debug)] #[allow(clippy::struct_excessive_bools)] @@ -84,6 +91,7 @@ mod test { pub callback_transaction_validation_complete: u32, pub saf_messages_received: bool, pub connectivity_status_callback_called: u64, + pub base_node_state_changed_callback_invoked: bool, } impl CallbackState { @@ -112,6 +120,7 @@ mod test { tx_cancellation_callback_called_outbound: false, saf_messages_received: false, connectivity_status_callback_called: 0, + base_node_state_changed_callback_invoked: false, } } } @@ -245,6 +254,13 @@ mod test { drop(lock); } + unsafe extern "C" fn base_node_state_changed_callback(state: *mut TariBaseNodeState) { + let mut lock = CALLBACK_STATE.lock().unwrap(); + lock.base_node_state_changed_callback_invoked = true; + drop(lock); + drop(Box::from_raw(state)) + } + #[test] #[allow(clippy::too_many_lines)] fn test_callback_handler() { @@ -408,6 +424,7 @@ mod test { db.insert_completed_transaction(7u64.into(), faux_confirmed_tx.clone()) .unwrap(); + let (base_node_event_sender, base_node_event_receiver) = broadcast::channel(20); let (transaction_event_sender, transaction_event_receiver) = broadcast::channel(20); let (oms_event_sender, oms_event_receiver) = broadcast::channel(20); let (dht_event_sender, dht_event_receiver) = broadcast::channel(20); @@ -442,6 +459,7 @@ mod test { let callback_handler = CallbackHandler::new( db, + base_node_event_receiver, transaction_event_receiver, oms_event_receiver, oms_handle, @@ -466,14 +484,49 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_changed_callback, ); runtime.spawn(callback_handler.start()); - let mut callback_balance_updated = 0; + + let ts_now = NaiveDateTime::from_timestamp_millis( + SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64, + ) + .unwrap(); + + let chain_metadata = ChainMetadata::new(1, Default::default(), 0, 0, 123, ts_now.timestamp_millis() as u64); + + base_node_event_sender + .send(Arc::new(BaseNodeEvent::BaseNodeStateChanged(BaseNodeState { + node_id: Some(NodeId::new()), + chain_metadata: Some(chain_metadata), + is_synced: Some(true), + updated: Some(NaiveDateTime::from_timestamp_millis( + ts_now.timestamp_millis() - (60 * 1000), + )) + .unwrap(), + latency: Some(Duration::from_micros(500)), + }))) + .unwrap(); + + let start = Instant::now(); + while start.elapsed().as_secs() < 10 { + let lock = CALLBACK_STATE.lock().unwrap(); + + if lock.base_node_state_changed_callback_invoked { + break; + } + } + assert!(CALLBACK_STATE.lock().unwrap().base_node_state_changed_callback_invoked); // The balance updated callback is bundled with other callbacks and will only fire if the balance actually // changed from an initial zero balance. // Balance updated should be detected with following event, total = 1 times + let mut callback_balance_updated = 0; + transaction_event_sender .send(Arc::new(TransactionEvent::ReceivedTransaction(1u64.into()))) .unwrap(); diff --git a/base_layer/wallet_ffi/src/ffi_basenode_state.rs b/base_layer/wallet_ffi/src/ffi_basenode_state.rs new file mode 100644 index 0000000000..eb0956b685 --- /dev/null +++ b/base_layer/wallet_ffi/src/ffi_basenode_state.rs @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2020. 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::{ + ffi::{c_int, c_ulonglong}, + ptr, +}; + +use tari_common_types::types::BlockHash; +use tari_comms::peer_manager::NodeId; +use tari_utilities::ByteArray; + +use crate::{ + error::{InterfaceError, LibWalletError}, + ByteVector, +}; + +#[derive(Debug)] +#[repr(C)] +pub struct TariBaseNodeState { + /// The ID of the base node this wallet is connected to + pub node_id: Option, + + /// The current chain height, or the block number of the longest valid chain, or zero if there is no chain + pub height_of_longest_chain: u64, + + /// The block hash of the current tip of the longest valid chain + pub best_block: BlockHash, + + /// Timestamp of the tip block in the longest valid chain + pub best_block_timestamp: u64, + + /// The configured number of blocks back from the tip that this database tracks. A value of 0 indicates that + /// pruning mode is disabled and the node will keep full blocks from the time it was set. If pruning horizon + /// was previously enabled, previously pruned blocks will remain pruned. If set from initial sync, full blocks + /// are preserved from genesis (i.e. the database is in full archival mode). + pub pruning_horizon: u64, + + /// The height of the pruning horizon. This indicates from what height a full block can be provided + /// (exclusive). If `pruned_height` is equal to the `height_of_longest_chain` no blocks can be + /// provided. Archival nodes wil always have an `pruned_height` of zero. + pub pruned_height: u64, + + pub is_node_synced: bool, + pub updated_at: u64, + pub latency: u64, +} + +/// Extracts a `NodeId` represented as a vector of bytes wrapped into a `ByteVector` +/// +/// ## Arguments +/// `ptr` - The pointer to a `TariBaseNodeState` +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `*mut ByteVector` - Returns a ByteVector or null if the NodeId is None. +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_node_id( + ptr: *mut TariBaseNodeState, + error_out: *mut c_int, +) -> *mut ByteVector { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + } + + match (*ptr).node_id { + None => ptr::null_mut(), + Some(ref node_id) => Box::into_raw(Box::new(ByteVector(node_id.to_vec()))), + } +} + +/// Extracts height of th elongest chain from the `TariBaseNodeState` +/// +/// ## Arguments +/// `ptr` - The pointer to a TariBaseNodeState +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `c_ulonglong` - The current chain height, or the block number of the longest valid chain, or `None` if there is no +/// chain +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_height_of_the_longest_chain( + ptr: *mut TariBaseNodeState, + error_out: *mut c_int, +) -> c_ulonglong { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return 0; + } + + (*ptr).height_of_longest_chain +} + +/// Extracts a best block hash [`FixedHash`] represented as a vector of bytes wrapped into a `ByteVector` +/// +/// ## Arguments +/// `ptr` - The pointer to a `TariBaseNodeState` +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `*mut ByteVector` - The block hash of the current tip of the longest valid chain. Returns a ByteVector or null if +/// the NodeId is None. +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_best_block( + ptr: *mut TariBaseNodeState, + error_out: *mut c_int, +) -> *mut ByteVector { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return ptr::null_mut(); + } + + Box::into_raw(Box::new(ByteVector((*ptr).best_block.to_vec()))) +} + +/// Extracts a timestamp of the best block +/// +/// ## Arguments +/// `ptr` - The pointer to a `TariBaseNodeState` +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `c_ulonglong` - Timestamp of the tip block in the longest valid chain +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_best_block_timestamp( + ptr: *mut TariBaseNodeState, + error_out: *mut c_int, +) -> c_ulonglong { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return 0; + } + + (*ptr).best_block_timestamp +} + +/// Extracts a pruning horizon +/// +/// ## Arguments +/// `ptr` - The pointer to a `TariBaseNodeState` +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `c_ulonglong` - The configured number of blocks back from the tip that this database tracks. A value of 0 indicates +/// that pruning mode is disabled and the node will keep full blocks from the time it was set. If pruning horizon +/// was previously enabled, previously pruned blocks will remain pruned. If set from initial sync, full blocks +/// are preserved from genesis (i.e. the database is in full archival mode). +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_pruning_horizon( + ptr: *mut TariBaseNodeState, + error_out: *mut c_int, +) -> c_ulonglong { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return 0; + } + + (*ptr).pruning_horizon +} + +/// Extracts a pruned height +/// +/// ## Arguments +/// `ptr` - The pointer to a `TariBaseNodeState` +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `c_ulonglong` - The height of the pruning horizon. This indicates from what height a full block can be provided +/// (exclusive). If `pruned_height` is equal to the `height_of_longest_chain` no blocks can be +/// provided. Archival nodes wil always have an `pruned_height` of zero. +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_pruned_height( + ptr: *mut TariBaseNodeState, + error_out: *mut c_int, +) -> c_ulonglong { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return 0; + } + + (*ptr).pruned_height +} + +/// Denotes whether a base node is fully synced or not. +/// +/// ## Arguments +/// `ptr` - The pointer to a `TariBaseNodeState` +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `*mut c_ulonglong` - An array of the length of 2 `c_ulonglong` +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_is_node_synced(ptr: *mut TariBaseNodeState, error_out: *mut c_int) -> bool { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return false; + } + + (*ptr).is_node_synced +} + +/// Extracts the timestamp of when the base node was last updated. +/// +/// ## Arguments +/// `ptr` - The pointer to a `TariBaseNodeState` +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `c_ulonglong` - Timestamp. +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_node_updated_at( + ptr: *mut TariBaseNodeState, + error_out: *mut c_int, +) -> c_ulonglong { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return 0; + } + + (*ptr).updated_at +} + +/// Extracts the connection latency to the base node. +/// +/// ## Arguments +/// `ptr` - The pointer to a `TariBaseNodeState` +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `c_ulonglong` - Latency value measured in microseconds. +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn basenode_state_get_latency(ptr: *mut TariBaseNodeState, error_out: *mut c_int) -> c_ulonglong { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + + if ptr.is_null() { + error = LibWalletError::from(InterfaceError::NullError("ptr".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + return 0; + } + + (*ptr).latency +} + +#[cfg(test)] +mod tests { + use tari_common_types::types::FixedHash; + use tari_comms::types::CommsPublicKey; + + use super::*; + + #[test] + fn test_basenode_state_ffi_accessors() { + let mut error_code = 0; + let original_node_id = NodeId::from_key(&CommsPublicKey::new_generator("test").unwrap()); + let original_best_block = BlockHash::zero(); + + let boxed_state = Box::into_raw(Box::new(TariBaseNodeState { + node_id: Some(original_node_id.clone()), + height_of_longest_chain: 123, + best_block: original_best_block, + best_block_timestamp: 12345, + pruning_horizon: 456, + pruned_height: 789, + is_node_synced: true, + updated_at: 135, + latency: 115, + })); + + unsafe { + // ---------------------------------------------------------------------------- + // node id + + let wrapped_node_id = basenode_state_get_node_id(boxed_state, &mut error_code); + + assert_eq!( + original_node_id, + NodeId::from_bytes((*wrapped_node_id).0.as_bytes()).unwrap() + ); + assert_eq!(error_code, 0); + + // ---------------------------------------------------------------------------- + // best block + + let mut block_hash = [0u8; FixedHash::byte_size()]; + block_hash.copy_from_slice( + (*basenode_state_get_best_block(boxed_state, &mut error_code)) + .0 + .as_bytes(), + ); + + let best_block = FixedHash::from(block_hash); + + assert_eq!(best_block, original_best_block); + assert_eq!(error_code, 0); + + // ---------------------------------------------------------------------------- + // other scalars + + assert_eq!( + basenode_state_get_height_of_the_longest_chain(boxed_state, &mut error_code), + 123 + ); + assert_eq!(error_code, 0); + + assert_eq!( + basenode_state_get_best_block_timestamp(boxed_state, &mut error_code), + 12345 + ); + assert_eq!(error_code, 0); + + assert_eq!(basenode_state_get_pruning_horizon(boxed_state, &mut error_code), 456); + assert_eq!(error_code, 0); + + assert_eq!(basenode_state_get_pruned_height(boxed_state, &mut error_code), 789); + assert_eq!(error_code, 0); + + assert!(basenode_state_get_is_node_synced(boxed_state, &mut error_code)); + assert_eq!(error_code, 0); + + assert_eq!(basenode_state_get_node_updated_at(boxed_state, &mut error_code), 135); + assert_eq!(error_code, 0); + + assert_eq!(basenode_state_get_latency(boxed_state, &mut error_code), 115); + assert_eq!(error_code, 0); + } + } +} diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index c0024565cb..b697bd12eb 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -68,6 +68,7 @@ use std::{ use chrono::{DateTime, Local}; use error::LibWalletError; +use ffi_basenode_state::TariBaseNodeState; use itertools::Itertools; use libc::{c_char, c_int, c_uchar, c_uint, c_ulonglong, c_ushort, c_void}; use log::{LevelFilter, *}; @@ -180,6 +181,7 @@ mod callback_handler; mod callback_handler_tests; mod enums; mod error; +mod ffi_basenode_state; #[cfg(test)] mod output_manager_service_mock; mod tasks; @@ -5172,6 +5174,7 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_validation_complete: unsafe extern "C" fn(u64, u64), callback_saf_messages_received: unsafe extern "C" fn(), callback_connectivity_status: unsafe extern "C" fn(u64), + callback_base_node_state: unsafe extern "C" fn(*mut TariBaseNodeState), recovery_in_progress: *mut bool, error_out: *mut c_int, ) -> *mut TariWallet { @@ -5393,9 +5396,11 @@ pub unsafe extern "C" fn wallet_create( } } let wallet_address = TariAddress::new(w.comms.node_identity().public_key().clone(), w.network.as_network()); + // Start Callback Handler let callback_handler = CallbackHandler::new( TransactionDatabase::new(transaction_backend), + w.base_node_service.get_event_stream(), w.transaction_service.get_event_stream(), w.output_manager_service.get_event_stream(), w.output_manager_service.clone(), @@ -5420,6 +5425,7 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_validation_complete, callback_saf_messages_received, callback_connectivity_status, + callback_base_node_state, ); runtime.spawn(callback_handler.start()); @@ -8539,6 +8545,7 @@ mod test { pub callback_contacts_liveness_data_updated: bool, pub callback_balance_updated: bool, pub callback_transaction_validation_complete: bool, + pub callback_basenode_state_updated: bool, } impl CallbackState { @@ -8558,6 +8565,7 @@ mod test { callback_contacts_liveness_data_updated: false, callback_balance_updated: false, callback_transaction_validation_complete: false, + callback_basenode_state_updated: false, } } } @@ -8743,6 +8751,10 @@ mod test { // assert!(true); //optimized out by compiler } + unsafe extern "C" fn base_node_state_callback(_state: *mut TariBaseNodeState) { + // assert!(true); //optimized out by compiler + } + const NETWORK_STRING: &str = "dibbler"; #[test] @@ -9328,6 +9340,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -9370,6 +9383,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -9481,6 +9495,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -9703,6 +9718,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -9764,6 +9780,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -9839,6 +9856,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -9984,6 +10002,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -10096,6 +10115,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -10290,6 +10310,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -10492,6 +10513,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); @@ -10723,6 +10745,7 @@ mod test { transaction_validation_complete_callback, saf_messages_received_callback, connectivity_status_callback, + base_node_state_callback, recovery_in_progress_ptr, error_ptr, ); diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index 24084cabe9..0307916370 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -85,8 +85,12 @@ struct FeePerGramStat; struct FeePerGramStatsResponse; +struct FixedHash; + struct InboundTransaction; +struct Option_NodeId; + struct OutboundTransaction; /** @@ -320,6 +324,43 @@ typedef struct P2pConfig TariCommsConfig; typedef struct Balance TariBalance; +typedef struct FixedHash BlockHash; + +struct TariBaseNodeState { + /** + * The ID of the base node this wallet is connected to + */ + struct Option_NodeId node_id; + /** + * The current chain height, or the block number of the longest valid chain, or zero if there is no chain + */ + uint64_t height_of_longest_chain; + /** + * The block hash of the current tip of the longest valid chain + */ + BlockHash best_block; + /** + * Timestamp of the tip block in the longest valid chain + */ + uint64_t best_block_timestamp; + /** + * The configured number of blocks back from the tip that this database tracks. A value of 0 indicates that + * pruning mode is disabled and the node will keep full blocks from the time it was set. If pruning horizon + * was previously enabled, previously pruned blocks will remain pruned. If set from initial sync, full blocks + * are preserved from genesis (i.e. the database is in full archival mode). + */ + uint64_t pruning_horizon; + /** + * The height of the pruning horizon. This indicates from what height a full block can be provided + * (exclusive). If `pruned_height` is equal to the `height_of_longest_chain` no blocks can be + * provided. Archival nodes wil always have an `pruned_height` of zero. + */ + uint64_t pruned_height; + bool is_node_synced; + uint64_t updated_at; + uint64_t latency; +}; + typedef struct FeePerGramStatsResponse TariFeePerGramStats; typedef struct FeePerGramStat TariFeePerGramStat; @@ -2698,6 +2739,7 @@ struct TariWallet *wallet_create(TariCommsConfig *config, void (*callback_transaction_validation_complete)(uint64_t, uint64_t), void (*callback_saf_messages_received)(void), void (*callback_connectivity_status)(uint64_t), + void (*callback_base_node_state)(struct TariBaseNodeState*), bool *recovery_in_progress, int *error_out); @@ -3917,6 +3959,166 @@ 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); +/** + * Extracts a `NodeId` represented as a vector of bytes wrapped into a `ByteVector` + * + * ## Arguments + * `ptr` - The pointer to a `TariBaseNodeState` + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `*mut ByteVector` - Returns a ByteVector or null if the NodeId is None. + * + * # Safety + * None + */ +struct ByteVector *basenode_state_get_node_id(struct TariBaseNodeState *ptr, + int *error_out); + +/** + * Extracts height of th elongest chain from the `TariBaseNodeState` + * + * ## Arguments + * `ptr` - The pointer to a TariBaseNodeState + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `c_ulonglong` - The current chain height, or the block number of the longest valid chain, or `None` if there is no + * chain + * + * # Safety + * None + */ +unsigned long long basenode_state_get_height_of_the_longest_chain(struct TariBaseNodeState *ptr, + int *error_out); + +/** + * Extracts a best block hash [`FixedHash`] represented as a vector of bytes wrapped into a `ByteVector` + * + * ## Arguments + * `ptr` - The pointer to a `TariBaseNodeState` + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `*mut ByteVector` - The block hash of the current tip of the longest valid chain. Returns a ByteVector or null if + * the NodeId is None. + * + * # Safety + * None + */ +struct ByteVector *basenode_state_get_best_block(struct TariBaseNodeState *ptr, + int *error_out); + +/** + * Extracts a timestamp of the best block + * + * ## Arguments + * `ptr` - The pointer to a `TariBaseNodeState` + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `c_ulonglong` - Timestamp of the tip block in the longest valid chain + * + * # Safety + * None + */ +unsigned long long basenode_state_get_best_block_timestamp(struct TariBaseNodeState *ptr, + int *error_out); + +/** + * Extracts a pruning horizon + * + * ## Arguments + * `ptr` - The pointer to a `TariBaseNodeState` + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `c_ulonglong` - The configured number of blocks back from the tip that this database tracks. A value of 0 indicates + * that pruning mode is disabled and the node will keep full blocks from the time it was set. If pruning horizon + * was previously enabled, previously pruned blocks will remain pruned. If set from initial sync, full blocks + * are preserved from genesis (i.e. the database is in full archival mode). + * + * # Safety + * None + */ +unsigned long long basenode_state_get_pruning_horizon(struct TariBaseNodeState *ptr, + int *error_out); + +/** + * Extracts a pruned height + * + * ## Arguments + * `ptr` - The pointer to a `TariBaseNodeState` + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `c_ulonglong` - The height of the pruning horizon. This indicates from what height a full block can be provided + * (exclusive). If `pruned_height` is equal to the `height_of_longest_chain` no blocks can be + * provided. Archival nodes wil always have an `pruned_height` of zero. + * + * # Safety + * None + */ +unsigned long long basenode_state_get_pruned_height(struct TariBaseNodeState *ptr, + int *error_out); + +/** + * Denotes whether a base node is fully synced or not. + * + * ## Arguments + * `ptr` - The pointer to a `TariBaseNodeState` + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `*mut c_ulonglong` - An array of the length of 2 `c_ulonglong` + * + * # Safety + * None + */ +bool basenode_state_get_is_node_synced(struct TariBaseNodeState *ptr, + int *error_out); + +/** + * Extracts the timestamp of when the base node was last updated. + * + * ## Arguments + * `ptr` - The pointer to a `TariBaseNodeState` + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `c_ulonglong` - Timestamp. + * + * # Safety + * None + */ +unsigned long long basenode_state_get_node_updated_at(struct TariBaseNodeState *ptr, + int *error_out); + +/** + * Extracts the connection latency to the base node. + * + * ## Arguments + * `ptr` - The pointer to a `TariBaseNodeState` + * `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions + * as an out parameter. + * + * ## Returns + * `c_ulonglong` - Latency value measured in microseconds. + * + * # Safety + * None + */ +unsigned long long basenode_state_get_latency(struct TariBaseNodeState *ptr, + int *error_out); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/integration_tests/tests/utils/ffi/callbacks.rs b/integration_tests/tests/utils/ffi/callbacks.rs index d5658a0fd6..afaa70ff81 100644 --- a/integration_tests/tests/utils/ffi/callbacks.rs +++ b/integration_tests/tests/utils/ffi/callbacks.rs @@ -44,6 +44,7 @@ pub struct Callbacks { tx_validation_result: Mutex, transaction_saf_message_received: Mutex, contacts_liveness_data_updated: Mutex, + basenode_state_updated: Mutex, pub wallet: Option>>, } @@ -295,6 +296,15 @@ impl Callbacks { ); } + pub fn on_basenode_state_update(&mut self, state: *mut c_void) { + *self.basenode_state_updated.lock().unwrap() += 1; + println!( + "{} Base node state changed to {:#?}.", + chrono::Local::now().format("%Y/%m/%d %H:%M:%S"), + state + ); + } + pub fn reset(&mut self, wallet: Arc>) { *self.transaction_received.lock().unwrap() = 0; *self.transaction_reply_received.lock().unwrap() = 0; @@ -311,6 +321,7 @@ impl Callbacks { *self.tx_validation_result.lock().unwrap() = 0; *self.transaction_saf_message_received.lock().unwrap() = 0; *self.contacts_liveness_data_updated.lock().unwrap() = 0; + *self.basenode_state_updated.lock().unwrap() = 0; self.wallet = Some(wallet); println!("wallet {:?}", self.wallet); } diff --git a/integration_tests/tests/utils/ffi/ffi_import.rs b/integration_tests/tests/utils/ffi/ffi_import.rs index 7ab9639fa5..982a6fc588 100644 --- a/integration_tests/tests/utils/ffi/ffi_import.rs +++ b/integration_tests/tests/utils/ffi/ffi_import.rs @@ -69,6 +69,7 @@ pub type TariUtxoSort = c_void; pub type EmojiSet = c_void; #[allow(dead_code)] pub type TariFeePerGramStats = c_void; +pub type TariBaseNodeState = c_void; #[cfg_attr(windows, link(name = "tari_wallet_ffi.dll"))] #[cfg_attr(not(windows), link(name = "tari_wallet_ffi"))] @@ -400,6 +401,7 @@ extern "C" { callback_transaction_validation_complete: unsafe extern "C" fn(u64, u64), callback_saf_messages_received: unsafe extern "C" fn(), callback_connectivity_status: unsafe extern "C" fn(u64), + callback_base_node_state_updated: unsafe extern "C" fn(*mut TariBaseNodeState), recovery_in_progress: *mut bool, error_out: *mut c_int, ) -> *mut TariWallet; diff --git a/integration_tests/tests/utils/ffi/wallet.rs b/integration_tests/tests/utils/ffi/wallet.rs index 316ee99cff..62347c9d57 100644 --- a/integration_tests/tests/utils/ffi/wallet.rs +++ b/integration_tests/tests/utils/ffi/wallet.rs @@ -54,7 +54,7 @@ use super::{ PublicKeys, WalletAddress, }; -use crate::utils::ffi::callbacks; +use crate::utils::ffi::{callbacks, ffi_import::TariBaseNodeState}; extern "C" fn callback_received_transaction(ptr: *mut TariPendingInboundTransaction) { let callbacks = Callbacks::instance(); @@ -136,6 +136,11 @@ extern "C" fn callback_connectivity_status(status: u64) { callbacks.on_connectivity_status(status); // println!("callback_connectivity_status"); } +extern "C" fn callback_base_node_state(state: *mut TariBaseNodeState) { + let callbacks = Callbacks::instance(); + callbacks.on_basenode_state_update(state); + // println!("callback_base_node_state"); +} #[derive(Default, Debug)] struct CachedBalance { @@ -188,6 +193,7 @@ impl Wallet { callback_transaction_validation_complete, callback_saf_messages_received, callback_connectivity_status, + callback_base_node_state, &mut recovery_in_progress, &mut error, );