diff --git a/Cargo.lock b/Cargo.lock index fabf53bbab..56bc2e3e32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7218,6 +7218,7 @@ dependencies = [ "libc", "log", "log4rs", + "num-traits", "openssl", "rand 0.8.5", "security-framework", diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index 176e5dbb30..675bc56aa6 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -41,6 +41,7 @@ use tari_p2p::{initialization::CommsInitializationError, peer_seeds::SeedPeer, T use tari_shutdown::ShutdownSignal; use tari_wallet::{ error::{WalletError, WalletStorageError}, + output_manager_service::storage::database::OutputManagerDatabase, storage::{ database::{WalletBackend, WalletDatabase}, sqlite_utilities::initialize_sqlite_database_backends, @@ -260,6 +261,7 @@ pub async fn init_wallet( }; let (wallet_backend, transaction_backend, output_manager_backend, contacts_backend, key_manager_backend) = backends; let wallet_db = WalletDatabase::new(wallet_backend); + let output_db = OutputManagerDatabase::new(output_manager_backend.clone()); debug!( target: LOG_TARGET, @@ -306,6 +308,7 @@ pub async fn init_wallet( node_identity, factories, wallet_db, + output_db, transaction_backend, output_manager_backend, contacts_backend, diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 7711e98c47..bb18c92b94 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -53,7 +53,10 @@ use tower::Service; use crate::output_manager_service::{ error::OutputManagerError, service::{Balance, OutputStatusesByTxId}, - storage::models::{KnownOneSidedPaymentScript, SpendingPriority}, + storage::{ + database::OutputBackendQuery, + models::{KnownOneSidedPaymentScript, SpendingPriority}, + }, }; /// API Request enum @@ -101,6 +104,7 @@ pub enum OutputManagerRequest { CancelTransaction(TxId), GetSpentOutputs, GetUnspentOutputs, + GetOutputsBy(OutputBackendQuery), GetInvalidOutputs, ValidateUtxos, RevalidateTxos, @@ -164,6 +168,7 @@ impl fmt::Display for OutputManagerRequest { CancelTransaction(v) => write!(f, "CancelTransaction ({})", v), GetSpentOutputs => write!(f, "GetSpentOutputs"), GetUnspentOutputs => write!(f, "GetUnspentOutputs"), + GetOutputsBy(q) => write!(f, "GetOutputs({:#?})", q), GetInvalidOutputs => write!(f, "GetInvalidOutputs"), ValidateUtxos => write!(f, "ValidateUtxos"), RevalidateTxos => write!(f, "RevalidateTxos"), @@ -234,6 +239,7 @@ pub enum OutputManagerResponse { TransactionCancelled, SpentOutputs(Vec), UnspentOutputs(Vec), + Outputs(Vec), InvalidOutputs(Vec), BaseNodePublicKeySet, TxoValidationStarted(u64), @@ -604,6 +610,13 @@ impl OutputManagerHandle { } } + pub async fn get_outputs_by(&mut self, q: OutputBackendQuery) -> Result, OutputManagerError> { + match self.handle.call(OutputManagerRequest::GetOutputsBy(q)).await?? { + OutputManagerResponse::Outputs(s) => Ok(s), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + pub async fn get_invalid_outputs(&mut self) -> Result, OutputManagerError> { match self.handle.call(OutputManagerRequest::GetInvalidOutputs).await?? { OutputManagerResponse::InvalidOutputs(s) => Ok(s), diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index bdc37b2124..a8f8587964 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -83,7 +83,7 @@ use crate::{ recovery::StandardUtxoRecoverer, resources::{OutputManagerKeyManagerBranch, OutputManagerResources}, storage::{ - database::{OutputManagerBackend, OutputManagerDatabase}, + database::{OutputBackendQuery, OutputManagerBackend, OutputManagerDatabase}, models::{DbUnblindedOutput, KnownOneSidedPaymentScript, SpendingPriority}, OutputStatus, }, @@ -362,6 +362,10 @@ where let outputs = self.fetch_unspent_outputs()?.into_iter().map(|v| v.into()).collect(); Ok(OutputManagerResponse::UnspentOutputs(outputs)) }, + OutputManagerRequest::GetOutputsBy(q) => { + let outputs = self.fetch_outputs_by(q)?.into_iter().map(|v| v.into()).collect(); + Ok(OutputManagerResponse::Outputs(outputs)) + }, OutputManagerRequest::ValidateUtxos => { self.validate_outputs().map(OutputManagerResponse::TxoValidationStarted) }, @@ -1569,6 +1573,10 @@ where Ok(self.resources.db.fetch_all_unspent_outputs()?) } + pub fn fetch_outputs_by(&self, q: OutputBackendQuery) -> Result, OutputManagerError> { + Ok(self.resources.db.fetch_outputs_by(q)?) + } + pub fn fetch_invalid_outputs(&self) -> Result, OutputManagerError> { Ok(self.resources.db.get_invalid_outputs()?) } diff --git a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs index a9f6929180..69e74f001b 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs @@ -12,7 +12,7 @@ use crate::output_manager_service::{ error::OutputManagerStorageError, service::{Balance, UTXOSelectionStrategy}, storage::{ - database::{DbKey, DbValue, WriteOperation}, + database::{DbKey, DbValue, OutputBackendQuery, WriteOperation}, models::DbUnblindedOutput, }, }; @@ -114,4 +114,5 @@ pub trait OutputManagerBackend: Send + Sync + Clone { current_tip_height: Option, ) -> Result, OutputManagerStorageError>; fn fetch_outputs_by_tx_id(&self, tx_id: TxId) -> Result, OutputManagerStorageError>; + fn fetch_outputs_by(&self, q: OutputBackendQuery) -> Result, OutputManagerStorageError>; } diff --git a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs index fd5c5e568d..aa3c8a2537 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs @@ -22,7 +22,7 @@ mod backend; use std::{ - fmt::{Display, Error, Formatter}, + fmt::{Debug, Display, Error, Formatter}, sync::Arc, }; @@ -50,6 +50,35 @@ use crate::output_manager_service::{ const LOG_TARGET: &str = "wallet::output_manager_service::database"; +#[derive(Debug, Copy, Clone)] +pub enum SortDirection { + Asc, + Desc, +} + +#[derive(Debug, Clone)] +pub struct OutputBackendQuery { + pub tip_height: i64, + pub status: Vec, + pub pagination: Option<(i64, i64)>, + pub value_min: Option<(i64, bool)>, + pub value_max: Option<(i64, bool)>, + pub sorting: Vec<(&'static str, SortDirection)>, +} + +impl Default for OutputBackendQuery { + fn default() -> Self { + Self { + tip_height: i64::MAX, + status: vec![OutputStatus::Spent], + pagination: None, + value_min: None, + value_max: None, + sorting: vec![], + } + } +} + #[derive(Debug, Clone, PartialEq)] pub enum DbKey { SpentOutput(BlindingFactor), @@ -419,6 +448,10 @@ where T: OutputManagerBackend + 'static let outputs = self.db.fetch_outputs_by_tx_id(tx_id)?; Ok(outputs) } + + pub fn fetch_outputs_by(&self, q: OutputBackendQuery) -> Result, OutputManagerStorageError> { + self.db.fetch_outputs_by(q) + } } fn unexpected_result(req: DbKey, res: DbValue) -> Result { diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs index 5c73fe59a0..31cf388ab3 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs @@ -45,7 +45,7 @@ use crate::{ error::OutputManagerStorageError, service::{Balance, UTXOSelectionStrategy}, storage::{ - database::{DbKey, DbKeyValuePair, DbValue, OutputManagerBackend, WriteOperation}, + database::{DbKey, DbKeyValuePair, DbValue, OutputBackendQuery, OutputManagerBackend, WriteOperation}, models::{DbUnblindedOutput, KnownOneSidedPaymentScript}, OutputStatus, }, @@ -1227,6 +1227,29 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { .map(|o| DbUnblindedOutput::try_from(o.clone())) .collect::, _>>() } + + fn fetch_outputs_by(&self, q: OutputBackendQuery) -> Result, OutputManagerStorageError> { + let conn = self.database_connection.get_pooled_connection()?; + Ok(OutputSql::fetch_outputs_by(q, &conn)? + .into_iter() + .filter_map(|mut x| { + if let Err(e) = self.decrypt_if_necessary(&mut x) { + error!(target: LOG_TARGET, "failed to `decrypt_if_necessary`: {:#?}", e); + return None; + } + + DbUnblindedOutput::try_from(x) + .map_err(|e| { + error!( + target: LOG_TARGET, + "failed to convert `OutputSql` to `DbUnblindedOutput`: {:#?}", e + ); + e + }) + .ok() + }) + .collect()) + } } /// These are the fields that can be updated for an Output diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs index 27d5b74e8b..53e4b90c41 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs @@ -47,6 +47,7 @@ use crate::{ error::OutputManagerStorageError, service::{Balance, UTXOSelectionStrategy}, storage::{ + database::{OutputBackendQuery, SortDirection}, models::DbUnblindedOutput, sqlite_db::{UpdateOutput, UpdateOutputSql}, OutputStatus, @@ -114,6 +115,66 @@ impl OutputSql { Ok(outputs::table.filter(outputs::status.eq(status as i32)).load(conn)?) } + /// Retrieves UTXOs by a set of given rules + // TODO: maybe use a shorthand macros + #[allow(clippy::cast_sign_loss)] + pub fn fetch_outputs_by( + q: OutputBackendQuery, + conn: &SqliteConnection, + ) -> Result, OutputManagerStorageError> { + let mut query = outputs::table + .into_boxed() + .filter(outputs::script_lock_height.le(q.tip_height)) + .filter(outputs::maturity.le(q.tip_height)) + .filter(outputs::features_unique_id.is_null()) + .filter(outputs::features_parent_public_key.is_null()); + + if let Some((offset, limit)) = q.pagination { + query = query.offset(offset).limit(limit); + } + + // filtering by OutputStatus + query = match q.status.len() { + 0 => query, + 1 => query.filter(outputs::status.eq(q.status[0] as i32)), + _ => query.filter(outputs::status.eq_any::>(q.status.into_iter().map(|s| s as i32).collect())), + }; + + // if set, filtering by minimum value + if let Some((min, is_inclusive)) = q.value_min { + query = if is_inclusive { + query.filter(outputs::value.ge(min)) + } else { + query.filter(outputs::value.gt(min)) + }; + } + + // if set, filtering by max value + if let Some((max, is_inclusive)) = q.value_max { + query = if is_inclusive { + query.filter(outputs::value.le(max)) + } else { + query.filter(outputs::value.lt(max)) + }; + } + + use SortDirection::{Asc, Desc}; + Ok(q.sorting + .into_iter() + .fold(query, |query, s| match s { + ("value", d) => match d { + Asc => query.then_order_by(outputs::value.asc()), + Desc => query.then_order_by(outputs::value.desc()), + }, + ("mined_height", d) => match d { + Asc => query.then_order_by(outputs::mined_height.asc()), + Desc => query.then_order_by(outputs::mined_height.desc()), + }, + _ => query, + }) + .load(conn)?) + } + /// Retrieves UTXOs than can be spent, sorted by priority, then value from smallest to largest. #[allow(clippy::cast_sign_loss)] pub fn fetch_unspent_outputs_for_spending( diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index 86f9da1252..f00c1a4404 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -86,7 +86,10 @@ use crate::{ output_manager_service::{ error::OutputManagerError, handle::OutputManagerHandle, - storage::{database::OutputManagerBackend, models::KnownOneSidedPaymentScript}, + storage::{ + database::{OutputManagerBackend, OutputManagerDatabase}, + models::KnownOneSidedPaymentScript, + }, OutputManagerServiceInitializer, }, storage::database::{WalletBackend, WalletDatabase}, @@ -121,6 +124,7 @@ pub struct Wallet { pub token_manager: TokenManagerHandle, pub updater_service: Option, pub db: WalletDatabase, + pub output_db: OutputManagerDatabase, pub factories: CryptoFactories, _u: PhantomData, _v: PhantomData, @@ -142,6 +146,7 @@ where node_identity: Arc, factories: CryptoFactories, wallet_database: WalletDatabase, + output_manager_database: OutputManagerDatabase, transaction_backend: U, output_manager_backend: V, contacts_backend: W, @@ -290,6 +295,7 @@ where updater_service: updater_handle, wallet_connectivity, db: wallet_database, + output_db: output_manager_database, factories, asset_manager: asset_manager_handle, token_manager: token_manager_handle, diff --git a/base_layer/wallet/tests/wallet.rs b/base_layer/wallet/tests/wallet.rs index b923843ef7..08de1ea1f7 100644 --- a/base_layer/wallet/tests/wallet.rs +++ b/base_layer/wallet/tests/wallet.rs @@ -190,19 +190,22 @@ async fn create_wallet( ..Default::default() }; - let metadata = ChainMetadata::new(std::i64::MAX as u64, Vec::new(), 0, 0, 0); + let metadata = ChainMetadata::new(i64::MAX as u64, Vec::new(), 0, 0, 0); let _db_value = wallet_backend.write(WriteOperation::Insert(DbKeyValuePair::BaseNodeChainMetadata(metadata))); let wallet_db = WalletDatabase::new(wallet_backend); let master_seed = read_or_create_master_seed(recovery_seed, &wallet_db).await?; + let output_db = OutputManagerDatabase::new(output_manager_backend.clone()); + Wallet::start( config, PeerSeedsConfig::default(), Arc::new(node_identity.clone()), factories, wallet_db, + output_db, transaction_backend, output_manager_backend, contacts_backend, @@ -700,14 +703,17 @@ async fn test_import_utxo() { ..Default::default() }; + let output_manager_backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let mut alice_wallet = Wallet::start( config, PeerSeedsConfig::default(), alice_identity.clone(), factories.clone(), WalletDatabase::new(WalletSqliteDatabase::new(connection.clone(), None).unwrap()), + OutputManagerDatabase::new(output_manager_backend.clone()), TransactionServiceSqliteDatabase::new(connection.clone(), None), - OutputManagerSqliteDatabase::new(connection.clone(), None), + output_manager_backend, ContactsServiceSqliteDatabase::new(connection.clone()), KeyManagerSqliteDatabase::new(connection.clone(), None).unwrap(), shutdown.to_signal(), diff --git a/base_layer/wallet_ffi/Cargo.toml b/base_layer/wallet_ffi/Cargo.toml index 683dce861c..858a322fef 100644 --- a/base_layer/wallet_ffi/Cargo.toml +++ b/base_layer/wallet_ffi/Cargo.toml @@ -30,6 +30,7 @@ rand = "0.8" thiserror = "1.0.26" tokio = "1.11" env_logger = "0.7.0" +num-traits = "0.2.15" # # Temporary workaround until crates utilizing openssl have been updated from security-framework 2.4.0 diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 9930ae854c..f3894ecb95 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -57,6 +57,7 @@ use std::{ boxed::Box, convert::TryFrom, ffi::{CStr, CString}, + mem::ManuallyDrop, num::NonZeroU16, path::PathBuf, slice, @@ -81,6 +82,7 @@ use log4rs::{ config::{Appender, Config, Root}, encode::pattern::PatternEncoder, }; +use num_traits::FromPrimitive; use rand::rngs::OsRng; use tari_common::configuration::StringList; use tari_common_types::{ @@ -131,6 +133,13 @@ use tari_wallet::{ connectivity_service::WalletConnectivityInterface, contacts_service::storage::database::Contact, error::{WalletError, WalletStorageError}, + output_manager_service::{ + error::OutputManagerError, + storage::{ + database::{OutputBackendQuery, OutputManagerDatabase, SortDirection}, + OutputStatus, + }, + }, storage::{ database::WalletDatabase, sqlite_db::wallet::WalletSqliteDatabase, @@ -218,20 +227,28 @@ pub struct TariWallet { shutdown: Shutdown, } -#[allow(unused)] #[derive(Debug, Clone)] -// #[repr(C)] -pub struct Utxo { - commitment: Commitment, - value: c_ulonglong, +#[repr(C)] +pub struct TariUtxo { + pub commitment: *mut c_char, + pub value: u64, } -#[allow(unused)] #[derive(Debug, Clone)] -// #[repr(C)] -pub struct GetUtxosView { - outputs: Vec, - unlisted_dust_sum: c_ulonglong, +#[repr(C)] +pub struct TariOutputs { + pub len: usize, + pub cap: usize, + pub ptr: *mut TariUtxo, +} + +#[derive(Debug)] +#[repr(C)] +pub enum TariUtxoSort { + ValueAsc, + ValueDesc, + MinedHeightAsc, + MinedHeightDesc, } /// -------------------------------- Strings ------------------------------------------------ /// @@ -3896,6 +3913,7 @@ pub unsafe extern "C" fn wallet_create( }, }; let wallet_database = WalletDatabase::new(wallet_backend); + let output_manager_database = OutputManagerDatabase::new(output_manager_backend.clone()); debug!(target: LOG_TARGET, "Databases Initialized"); @@ -3987,6 +4005,7 @@ pub unsafe extern "C" fn wallet_create( node_identity, factories, wallet_database, + output_manager_database, transaction_backend.clone(), output_manager_backend, contacts_backend, @@ -4093,32 +4112,34 @@ pub unsafe extern "C" fn wallet_get_balance(wallet: *mut TariWallet, error_out: /// This function returns a list of unspent UTXO values and commitments. /// /// ## Arguments -/// `wallet` - The TariWallet pointer -/// `page` - Page offset -/// `page_size` - A number of items per page -/// `sort_ascending` - Sorting order -/// `dust_threshold` - A value filtering threshold. Outputs whose values are <= `dust_threshold`, are not listed in the -/// result, but are added to the `GetUtxosView.unlisted_dust_sum`. -/// `error_out` - Pointer to an int which will be modified to an error +/// `wallet` - The TariWallet pointer, +/// `page` - Page offset, +/// `page_size` - A number of items per page, +/// `sorting` - An enum representing desired sorting, +/// `dust_threshold` - A value filtering threshold. Outputs whose values are <= `dust_threshold` are not listed in the +/// result. +/// `error_out` - A 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 GetUtxosView` - Returns a struct with a list of unspent `outputs` and an `unlisted_dust_sum` holding a sum of -/// values that were filtered out by `dust_threshold`. +/// `*mut TariOutputs` - Returns a struct with an array pointer, length and capacity (needed for proper destruction +/// after use). /// /// # Safety +/// `destroy_tari_outputs()` must be called after use. /// Items that fail to produce `.as_transaction_output()` are omitted from the list and a `warn!()` message is logged to /// LOG_TARGET. #[no_mangle] pub unsafe extern "C" fn wallet_get_utxos( wallet: *mut TariWallet, - page: c_uint, - page_size: c_uint, - sort_ascending: bool, - dust_threshold: c_ulonglong, - error_out: *mut c_int, -) -> *mut GetUtxosView { + page: usize, + page_size: usize, + sorting: TariUtxoSort, + dust_threshold: u64, + error_out: *mut i32, +) -> *mut TariOutputs { if wallet.is_null() { + error!(target: LOG_TARGET, "wallet pointer is null"); ptr::replace( error_out, LibWalletError::from(InterfaceError::NullError("wallet".to_string())).code as c_int, @@ -4126,69 +4147,95 @@ pub unsafe extern "C" fn wallet_get_utxos( return ptr::null_mut(); } - let factories = CryptoFactories::default(); - let page = (page as usize).max(1) - 1; - let page_size = (page_size as usize).max(1); - - debug!( - target: LOG_TARGET, - "page = {:#?} page_size = {:#?} sort_asc = {:#?} dust_threshold = {:#?}", - page, - page_size, - sort_ascending, - dust_threshold - ); - - match (*wallet) - .runtime - .block_on((*wallet).wallet.output_manager_service.get_unspent_outputs()) - { - Ok(mut unblinded_outputs) => { - unblinded_outputs.sort_by(|a, b| { - if sort_ascending { - Ord::cmp(&a.value, &b.value) - } else { - Ord::cmp(&b.value, &a.value) - } - }); + let page = i64::from_usize(page).unwrap_or(i64::MAX).max(1) - 1; + let page_size = i64::from_usize(page_size).unwrap_or(i64::MAX).max(1); + let dust_threshold = i64::from_u64(dust_threshold).unwrap_or(0); + + use SortDirection::{Asc, Desc}; + let q = OutputBackendQuery { + tip_height: i64::MAX, + status: vec![OutputStatus::Unspent], + pagination: Some((page, page_size)), + value_min: Some((dust_threshold, false)), + value_max: None, + sorting: vec![match sorting { + TariUtxoSort::MinedHeightAsc => ("mined_height", Asc), + TariUtxoSort::MinedHeightDesc => ("mined_height", Desc), + TariUtxoSort::ValueAsc => ("value", Asc), + TariUtxoSort::ValueDesc => ("value", Desc), + }], + }; - let (outputs, dust): (Vec, Vec) = unblinded_outputs + match (*wallet).wallet.output_db.fetch_outputs_by(q) { + Ok(unblinded_outputs) => { + let outputs: Vec = unblinded_outputs .into_iter() - .skip(page * page_size) - .take(page_size) .filter_map(|out| { - Some(Utxo { - value: out.value.as_u64(), - commitment: match out.as_transaction_output(&factories) { - Ok(commitment) => commitment.commitment, + Some(TariUtxo { + value: out.unblinded_output.value.as_u64(), + commitment: match out.unblinded_output.as_transaction_output(&CryptoFactories::default()) { + Ok(tout) => match CString::new(tout.commitment.to_hex()) { + Ok(cstr) => cstr.into_raw(), + Err(e) => { + error!( + target: LOG_TARGET, + "failed to convert commitment hex String into CString: {:#?}", e + ); + return None; + }, + }, Err(e) => { - warn!( + error!( target: LOG_TARGET, - "failed to obtain commitment from the unblinded output: {:#?}", e + "failed to obtain commitment from the transaction output: {:#?}", e ); return None; }, }, }) }) - .partition(|out| out.value.gt(&(dust_threshold as c_ulonglong))); + .collect(); - Box::into_raw(Box::new(GetUtxosView { - outputs, - unlisted_dust_sum: dust.into_iter().fold(0, |acc, x| acc + x.value), + let mut outputs = ManuallyDrop::new(outputs); + Box::into_raw(Box::new(TariOutputs { + len: outputs.len(), + cap: outputs.capacity(), + ptr: outputs.as_mut_ptr(), })) }, Err(e) => { + error!(target: LOG_TARGET, "failed to obtain outputs: {:#?}", e); ptr::replace( error_out, - LibWalletError::from(WalletError::OutputManagerError(e)).code as c_int, + LibWalletError::from(WalletError::OutputManagerError( + OutputManagerError::OutputManagerStorageError(e), + )) + .code as c_int, ); ptr::null_mut() }, } } +/// Frees memory for a `TariOutputs` +/// +/// ## Arguments +/// `x` - The pointer to `TariOutputs` +/// +/// ## Returns +/// `()` - Does not return a value, equivalent to void in C +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn destroy_tari_outputs(x: *mut TariOutputs) { + if !x.is_null() { + Vec::from_raw_parts((*x).ptr, (*x).len, (*x).cap); + Box::from_raw(x); + } +} + /// Signs a message using the public key of the TariWallet /// /// ## Arguments @@ -8784,43 +8831,42 @@ mod test { }); // ascending order - let utxos = wallet_get_utxos(alice_wallet, 1, 20, true, 3000, error_ptr); + let outputs = wallet_get_utxos(alice_wallet, 1, 20, TariUtxoSort::ValueAsc, 3000, error_ptr); + let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); assert_eq!(error, 0); - assert_eq!((*utxos).outputs.len(), 6); - assert_eq!((*utxos).unlisted_dust_sum, 6000); + assert_eq!((*outputs).len, 6); + assert_eq!(utxos.len(), 6); assert!( - (*utxos) - .outputs + utxos .iter() .skip(1) - .fold((true, (*utxos).outputs[0].value), |acc, x| ( - acc.0 && x.value > acc.1, - x.value - )) + .fold((true, utxos[0].value), |acc, x| { (acc.0 && x.value > acc.1, x.value) }) .0 ); + destroy_tari_outputs(outputs); // descending order - let utxos = wallet_get_utxos(alice_wallet, 1, 20, false, 3000, error_ptr); + let outputs = wallet_get_utxos(alice_wallet, 1, 20, TariUtxoSort::ValueDesc, 3000, error_ptr); + let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); assert_eq!(error, 0); - assert_eq!((*utxos).outputs.len(), 6); - assert_eq!((*utxos).unlisted_dust_sum, 6000); + assert_eq!((*outputs).len, 6); + assert_eq!(utxos.len(), 6); assert!( - (*utxos) - .outputs + utxos .iter() .skip(1) - .fold((true, (*utxos).outputs[0].value), |acc, x| ( - acc.0 && x.value < acc.1, - x.value - )) + .fold((true, utxos[0].value), |acc, x| (acc.0 && x.value < acc.1, x.value)) .0 ); + destroy_tari_outputs(outputs); // result must be empty due to high dust threshold - let utxos = wallet_get_utxos(alice_wallet, 1, 20, true, 15000, error_ptr); + let outputs = wallet_get_utxos(alice_wallet, 1, 20, TariUtxoSort::ValueAsc, 15000, error_ptr); + let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); assert_eq!(error, 0); - assert_eq!((*utxos).outputs.len(), 0); + assert_eq!((*outputs).len, 0); + assert_eq!(utxos.len(), 0); + destroy_tari_outputs(outputs); string_destroy(network_str as *mut c_char); string_destroy(db_name_alice_str as *mut c_char); @@ -8828,7 +8874,6 @@ mod test { string_destroy(address_alice_str as *mut c_char); private_key_destroy(secret_key_alice); transport_config_destroy(transport_config_alice); - comms_config_destroy(alice_config); wallet_destroy(alice_wallet); } diff --git a/base_layer/wallet_ffi/tari_wallet_ffi.h b/base_layer/wallet_ffi/tari_wallet_ffi.h index 756b44921b..31ff6774b5 100644 --- a/base_layer/wallet_ffi/tari_wallet_ffi.h +++ b/base_layer/wallet_ffi/tari_wallet_ffi.h @@ -11,6 +11,13 @@ */ #define OutputFields_NUM_FIELDS 10 +enum TariUtxoSort { + ValueAsc, + ValueDesc, + MinedHeightAsc, + MinedHeightDesc, +}; + /** * This struct holds the detailed balance of the Output Manager Service. */ @@ -56,8 +63,6 @@ struct FeePerGramStat; struct FeePerGramStatsResponse; -struct GetUtxosView; - struct InboundTransaction; struct OutboundTransaction; @@ -271,6 +276,17 @@ typedef struct P2pConfig TariCommsConfig; typedef struct Balance TariBalance; +struct TariUtxo { + char *commitment; + uint64_t value; +}; + +struct TariOutputs { + uintptr_t len; + uintptr_t cap; + struct TariUtxo *ptr; +}; + typedef struct FeePerGramStatsResponse TariFeePerGramStats; typedef struct FeePerGramStat TariFeePerGramStat; @@ -2151,29 +2167,44 @@ TariBalance *wallet_get_balance(struct TariWallet *wallet, * This function returns a list of unspent UTXO values and commitments. * * ## Arguments - * `wallet` - The TariWallet pointer - * `page` - Page offset - * `page_size` - A number of items per page - * `sort_ascending` - Sorting order - * `dust_threshold` - A value filtering threshold. Outputs whose values are <= `dust_threshold`, are not listed in the - * result, but are added to the `GetUtxosView.unlisted_dust_sum`. - * `error_out` - Pointer to an int which will be modified to an error + * `wallet` - The TariWallet pointer, + * `page` - Page offset, + * `page_size` - A number of items per page, + * `sorting` - An enum representing desired sorting, + * `dust_threshold` - A value filtering threshold. Outputs whose values are <= `dust_threshold` are not listed in the + * result. + * `error_out` - A 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 GetUtxosView` - Returns a struct with a list of unspent `outputs` and an `unlisted_dust_sum` holding a sum of - * values that were filtered out by `dust_threshold`. + * `*mut TariOutputs` - Returns a struct with an array pointer, length and capacity (needed for proper destruction + * after use). * * # Safety + * `destroy_tari_outputs()` must be called after use. * Items that fail to produce `.as_transaction_output()` are omitted from the list and a `warn!()` message is logged to * LOG_TARGET. */ -struct GetUtxosView *wallet_get_utxos(struct TariWallet *wallet, - unsigned int page, - unsigned int page_size, - bool sort_ascending, - unsigned long long dust_threshold, - int *error_out); +struct TariOutputs *wallet_get_utxos(struct TariWallet *wallet, + uintptr_t page, + uintptr_t page_size, + enum TariUtxoSort sorting, + uint64_t dust_threshold, + int32_t *error_out); + +/** + * Frees memory for a `TariOutputs` + * + * ## Arguments + * `x` - The pointer to `TariOutputs` + * + * ## Returns + * `()` - Does not return a value, equivalent to void in C + * + * # Safety + * None + */ +void destroy_tari_outputs(struct TariOutputs *x); /** * Signs a message using the public key of the TariWallet