diff --git a/Cargo.lock b/Cargo.lock index f1e82d25f4..6b511d1f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7174,6 +7174,7 @@ dependencies = [ "env_logger 0.7.1", "fs2 0.3.0", "futures 0.3.21", + "itertools 0.10.3", "libsqlite3-sys", "lmdb-zero", "log", @@ -7214,6 +7215,7 @@ dependencies = [ "chrono", "env_logger 0.7.1", "futures 0.3.21", + "itertools 0.10.3", "lazy_static", "libc", "log", diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index a37a4b1a01..64029bf2f6 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -249,7 +249,7 @@ pub async fn coin_split( }?; let (tx_id, tx, amount) = output_service - .create_coin_split(amount_per_split, num_splits as usize, fee_per_gram, None) + .create_coin_split(vec![], amount_per_split, num_splits as usize, fee_per_gram) .await?; transaction_service .submit_transaction(tx_id, tx, amount, "Coin split".into()) diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index 1d15e69e7e..8df5ee26bb 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -599,21 +599,15 @@ impl wallet_server::Wallet for WalletGrpcServer { async fn coin_split(&self, request: Request) -> Result, Status> { let message = request.into_inner(); - let lock_height = if message.lock_height == 0 { - None - } else { - Some(message.lock_height) - }; - let mut wallet = self.wallet.clone(); let tx_id = wallet .coin_split( + vec![], // TODO: refactor grpc to accept and use commitments MicroTari::from(message.amount_per_split), message.split_count as usize, MicroTari::from(message.fee_per_gram), message.message, - lock_height, ) .await .map_err(|e| Status::internal(format!("{:?}", e)))?; diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml index 20fe3c78cd..730aac94bb 100644 --- a/base_layer/wallet/Cargo.toml +++ b/base_layer/wallet/Cargo.toml @@ -55,6 +55,7 @@ tempfile = "3.1.0" thiserror = "1.0.26" tower = "0.4" prost = "0.9" +itertools = "0.10.3" [dependencies.tari_core] path = "../../base_layer/core" diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs index 0cb6ab98cf..76cf0ace9f 100644 --- a/base_layer/wallet/src/output_manager_service/error.rs +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -126,6 +126,10 @@ pub enum OutputManagerError { InvalidMessageError(String), #[error("Key manager service error : {0}")] KeyManagerServiceError(#[from] KeyManagerServiceError), + #[error("No commitments were provided")] + NoCommitmentsProvided, + #[error("Invalid argument: {0}")] + InvalidArgument(String), } #[derive(Debug, Error)] diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index bb18c92b94..9b3893dd3f 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -25,7 +25,7 @@ use std::{fmt, fmt::Formatter, sync::Arc}; use aes_gcm::Aes256Gcm; use tari_common_types::{ transaction::TxId, - types::{HashOutput, PrivateKey, PublicKey}, + types::{Commitment, HashOutput, PrivateKey, PublicKey}, }; use tari_core::{ covenants::Covenant, @@ -108,7 +108,11 @@ pub enum OutputManagerRequest { GetInvalidOutputs, ValidateUtxos, RevalidateTxos, - CreateCoinSplit((MicroTari, usize, MicroTari, Option)), + CreateCoinSplit((Vec, MicroTari, usize, MicroTari)), + CreateCoinJoin { + commitments: Vec, + fee_per_gram: MicroTari, + }, ApplyEncryption(Box), RemoveEncryption, GetPublicRewindKeys, @@ -172,7 +176,15 @@ impl fmt::Display for OutputManagerRequest { GetInvalidOutputs => write!(f, "GetInvalidOutputs"), ValidateUtxos => write!(f, "ValidateUtxos"), RevalidateTxos => write!(f, "RevalidateTxos"), - CreateCoinSplit(v) => write!(f, "CreateCoinSplit ({})", v.0), + CreateCoinSplit(v) => write!(f, "CreateCoinSplit ({:?})", v.0), + CreateCoinJoin { + commitments, + fee_per_gram, + } => write!( + f, + "CreateCoinJoin: commitments={:#?}, fee_per_gram={}", + commitments, fee_per_gram, + ), ApplyEncryption(_) => write!(f, "ApplyEncryption"), RemoveEncryption => write!(f, "RemoveEncryption"), GetCoinbaseTransaction(_) => write!(f, "GetCoinbaseTransaction"), @@ -663,18 +675,18 @@ impl OutputManagerHandle { /// Returns (tx_id, tx, utxos_total_value). pub async fn create_coin_split( &mut self, + commitments: Vec, amount_per_split: MicroTari, split_count: usize, fee_per_gram: MicroTari, - lock_height: Option, ) -> Result<(TxId, Transaction, MicroTari), OutputManagerError> { match self .handle .call(OutputManagerRequest::CreateCoinSplit(( + commitments, amount_per_split, split_count, fee_per_gram, - lock_height, ))) .await?? { @@ -683,6 +695,24 @@ impl OutputManagerHandle { } } + pub async fn create_coin_join( + &mut self, + commitments: Vec, + fee_per_gram: MicroTari, + ) -> Result<(TxId, Transaction, MicroTari), OutputManagerError> { + match self + .handle + .call(OutputManagerRequest::CreateCoinJoin { + commitments, + fee_per_gram, + }) + .await?? + { + OutputManagerResponse::Transaction(result) => Ok(result), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + pub async fn create_htlc_refund_transaction( &mut self, output: HashOutput, diff --git a/base_layer/wallet/src/output_manager_service/input_selection.rs b/base_layer/wallet/src/output_manager_service/input_selection.rs index 026ebe5616..2264e38d8d 100644 --- a/base_layer/wallet/src/output_manager_service/input_selection.rs +++ b/base_layer/wallet/src/output_manager_service/input_selection.rs @@ -25,9 +25,7 @@ use std::{ fmt::{Display, Formatter}, }; -use tari_common_types::types::PublicKey; - -use crate::output_manager_service::storage::models::DbUnblindedOutput; +use tari_common_types::types::{Commitment, PublicKey}; #[derive(Debug, Clone, Default)] pub struct UtxoSelectionCriteria { @@ -43,6 +41,13 @@ impl UtxoSelectionCriteria { } } + pub fn smallest_first() -> Self { + Self { + filter: UtxoSelectionFilter::Standard, + ordering: UtxoSelectionOrdering::SmallestFirst, + } + } + pub fn for_token(unique_id: Vec, parent_public_key: Option) -> Self { Self { filter: UtxoSelectionFilter::TokenOutput { @@ -52,6 +57,13 @@ impl UtxoSelectionCriteria { ordering: UtxoSelectionOrdering::Default, } } + + pub fn specific(commitments: Vec) -> Self { + Self { + filter: UtxoSelectionFilter::SpecificOutputs { commitments }, + ordering: UtxoSelectionOrdering::Default, + } + } } impl Display for UtxoSelectionCriteria { @@ -100,7 +112,7 @@ pub enum UtxoSelectionFilter { parent_public_key: Option, }, /// Selects specific outputs. All outputs must be exist and be spendable. - SpecificOutputs { outputs: Vec }, + SpecificOutputs { commitments: Vec }, } impl Default for UtxoSelectionFilter { @@ -118,7 +130,7 @@ impl Display for UtxoSelectionFilter { UtxoSelectionFilter::TokenOutput { .. } => { write!(f, "TokenOutput{{..}}") }, - UtxoSelectionFilter::SpecificOutputs { outputs } => { + UtxoSelectionFilter::SpecificOutputs { commitments: outputs } => { write!(f, "Specific({} output(s))", outputs.len()) }, } diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 772b6ae135..1389d4d9b9 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -25,11 +25,15 @@ use std::{convert::TryInto, fmt, sync::Arc}; use blake2::Digest; use diesel::result::{DatabaseErrorKind, Error as DieselError}; use futures::{pin_mut, StreamExt}; +use itertools::{ + FoldWhile::{Continue, Done}, + Itertools, +}; use log::*; use rand::{rngs::OsRng, RngCore}; use tari_common_types::{ transaction::TxId, - types::{BlockHash, HashOutput, PrivateKey, PublicKey}, + types::{BlockHash, Commitment, HashOutput, PrivateKey, PublicKey}, }; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_core::{ @@ -43,6 +47,7 @@ use tari_core::{ KernelFeatures, OutputFeatures, Transaction, + TransactionInput, TransactionOutput, TransactionOutputVersion, UnblindedOutput, @@ -377,8 +382,22 @@ where let outputs = self.fetch_invalid_outputs()?.into_iter().map(|v| v.into()).collect(); Ok(OutputManagerResponse::InvalidOutputs(outputs)) }, - OutputManagerRequest::CreateCoinSplit((amount_per_split, split_count, fee_per_gram, lock_height)) => self - .create_coin_split(amount_per_split, split_count, fee_per_gram, lock_height) + OutputManagerRequest::CreateCoinSplit((commitments, amount_per_split, split_count, fee_per_gram)) => { + if commitments.is_empty() { + self.create_coin_split_auto(amount_per_split, split_count, fee_per_gram) + .await + .map(OutputManagerResponse::Transaction) + } else { + self.create_coin_split_with_commitments(commitments, amount_per_split, split_count, fee_per_gram) + .await + .map(OutputManagerResponse::Transaction) + } + }, + OutputManagerRequest::CreateCoinJoin { + commitments, + fee_per_gram, + } => self + .create_coin_join(commitments, fee_per_gram) .await .map(OutputManagerResponse::Transaction), OutputManagerRequest::ApplyEncryption(cipher) => self @@ -1423,6 +1442,137 @@ where Ok(()) } + // NOTE: WIP + #[allow(dead_code)] + async fn select_utxos2( + &mut self, + target_amount: MicroTari, + fee_per_gram: MicroTari, + num_outputs: usize, + total_output_metadata_byte_size: usize, + selection_criteria: UtxoSelectionCriteria, + ) -> Result { + debug!( + target: LOG_TARGET, + "select_utxos target_amount: {}, fee_per_gram: {}, num_outputs: {}, output_metadata_byte_size: {}, \ + selection_criteria: {:?}", + target_amount, + fee_per_gram, + num_outputs, + total_output_metadata_byte_size, + selection_criteria + ); + + let tip_height = self + .base_node_service + .get_chain_metadata() + .await? + .as_ref() + .map(|m| m.height_of_longest_chain()); + let balance = self.get_balance(tip_height)?; + + // collecting UTXOs sufficient to cover the target amount + let (_, accumulated_amount, utxos) = self + .resources + .db + .fetch_unspent_outputs_for_spending(selection_criteria.clone(), target_amount, tip_height)? + .into_iter() + .fold_while( + (1usize, MicroTari::zero(), Vec::::new()), + |mut acc, out| { + let fee = self.get_fee_calc().calculate( + fee_per_gram, + 1, + acc.0, + num_outputs, + total_output_metadata_byte_size, + ); + + let next = acc.1 + out.unblinded_output.value; + let target_amount_with_fee = target_amount + fee; + + // if next < target_amount_with_fee || acc.1 < target_amount_with_fee && next >= + // target_amount_with_fee + if next < target_amount_with_fee || acc.1 < target_amount_with_fee { + acc.0 += 1; + acc.1 = next; + acc.2.push(out); + Continue(acc) + } else { + Done(acc) + } + }, + ) + .into_inner(); + + // let accumulated_amount = utxos + // .iter() + // .fold(MicroTari::zero(), |acc, x| acc + x.unblinded_output.value); + + if accumulated_amount <= target_amount { + return Err(OutputManagerError::NotEnoughFunds); + } + + let fee_without_change = self.get_fee_calc().calculate( + fee_per_gram, + 1, + utxos.len(), + num_outputs, + total_output_metadata_byte_size, + ); + + // checking whether the total output value is enough + if accumulated_amount < (target_amount + fee_without_change) { + return Err(OutputManagerError::NotEnoughFunds); + } + + let fee_with_change = match accumulated_amount + .saturating_sub(target_amount + fee_without_change) + .as_u64() + { + 0 => fee_without_change, + _ => self.get_fee_calc().calculate( + fee_per_gram, + 1, + utxos.len(), + num_outputs + 1, + total_output_metadata_byte_size + self.default_metadata_size(), + ), + }; + + // this is how much it would require in the end + let target_amount_with_fee = target_amount + fee_with_change; + + // checking, again, whether a total output value is enough + if accumulated_amount < target_amount_with_fee { + return Err(OutputManagerError::NotEnoughFunds); + } + + // balance check + if balance.available_balance < target_amount_with_fee { + return if accumulated_amount + balance.pending_incoming_balance >= target_amount_with_fee { + Err(OutputManagerError::FundsPending) + } else { + Err(OutputManagerError::NotEnoughFunds) + }; + } + + trace!( + target: LOG_TARGET, + "select_utxos selection criteria: {}\noutputs found: {}", + selection_criteria, + utxos.len() + ); + + Ok(UtxoSelection { + utxos, + requires_change_output: accumulated_amount.saturating_sub(target_amount_with_fee) > MicroTari::zero(), + total_value: accumulated_amount, + fee_without_change, + fee_with_change, + }) + } + /// Select which unspent transaction outputs to use to send a transaction of the specified amount. Use the specified /// selection strategy to choose the outputs. It also determines if a change output is required. async fn select_utxos( @@ -1430,7 +1580,7 @@ where amount: MicroTari, fee_per_gram: MicroTari, num_outputs: usize, - output_metadata_byte_size: usize, + total_output_metadata_byte_size: usize, selection_criteria: UtxoSelectionCriteria, ) -> Result { debug!( @@ -1440,7 +1590,7 @@ where amount, fee_per_gram, num_outputs, - output_metadata_byte_size, + total_output_metadata_byte_size, selection_criteria ); let mut utxos = Vec::new(); @@ -1474,8 +1624,13 @@ where utxos_total_value += o.unblinded_output.value; utxos.push(o); // The assumption here is that the only output will be the payment output and change if required - fee_without_change = - fee_calc.calculate(fee_per_gram, 1, utxos.len(), num_outputs, output_metadata_byte_size); + fee_without_change = fee_calc.calculate( + fee_per_gram, + 1, + utxos.len(), + num_outputs, + total_output_metadata_byte_size, + ); if utxos_total_value == amount + fee_without_change { break; } @@ -1484,7 +1639,7 @@ where 1, utxos.len(), num_outputs + 1, - output_metadata_byte_size + default_metadata_size, + total_output_metadata_byte_size + default_metadata_size, ); if utxos_total_value > amount + fee_with_change { requires_change_output = true; @@ -1536,98 +1691,200 @@ where Ok(()) } - #[allow(clippy::too_many_lines)] - async fn create_coin_split( + fn default_metadata_size(&self) -> usize { + self.resources + .consensus_constants + .transaction_weight() + .round_up_metadata_size( + script!(Nop).consensus_encode_exact_size() + OutputFeatures::default().consensus_encode_exact_size(), + ) + } + + async fn create_coin_split_with_commitments( &mut self, + commitments: Vec, amount_per_split: MicroTari, - split_count: usize, + number_of_splits: usize, fee_per_gram: MicroTari, - lock_height: Option, ) -> Result<(TxId, Transaction, MicroTari), OutputManagerError> { - trace!( - target: LOG_TARGET, - "Select UTXOs and estimate coin split transaction fee." - ); - let output_count = split_count; - let script = script!(Nop); - let covenant = Covenant::default(); - let output_features_estimate = OutputFeatures::default(); - let metadata_byte_size = self - .resources - .consensus_constants - .transaction_weight() - .round_up_metadata_size( - output_features_estimate.consensus_encode_exact_size() + - script.consensus_encode_exact_size() + - covenant.consensus_encode_exact_size(), - ); + if commitments.is_empty() { + return Err(OutputManagerError::NoCommitmentsProvided); + } - let total_split_amount = amount_per_split * split_count as u64; - let input_selection = self + let src_outputs = self.resources.db.fetch_unspent_outputs_for_spending( + UtxoSelectionCriteria::specific(commitments), + MicroTari::zero(), + None, + )?; + + self.create_coin_split(src_outputs, amount_per_split, number_of_splits, fee_per_gram) + .await + } + + async fn create_coin_split_auto( + &mut self, + amount_per_split: MicroTari, + number_of_splits: usize, + fee_per_gram: MicroTari, + ) -> Result<(TxId, Transaction, MicroTari), OutputManagerError> { + let src_outputs = self .select_utxos( - total_split_amount, + MicroTari::from(amount_per_split.as_u64() * number_of_splits as u64), fee_per_gram, - output_count, - output_count * metadata_byte_size, + number_of_splits, + self.default_metadata_size() * number_of_splits, UtxoSelectionCriteria::largest_first(), ) .await?; - trace!(target: LOG_TARGET, "Construct coin split transaction."); - let offset = PrivateKey::random(&mut OsRng); - let nonce = PrivateKey::random(&mut OsRng); + self.create_coin_split(src_outputs.utxos, amount_per_split, number_of_splits, fee_per_gram) + .await + } - let mut builder = SenderTransactionProtocol::builder(0, self.resources.consensus_constants.clone()); - builder - .with_lock_height(lock_height.unwrap_or(0)) + #[allow(clippy::too_many_lines)] + async fn create_coin_split( + &mut self, + src_outputs: Vec, + amount_per_split: MicroTari, + number_of_splits: usize, + fee_per_gram: MicroTari, + ) -> Result<(TxId, Transaction, MicroTari), OutputManagerError> { + if number_of_splits == 0 { + return Err(OutputManagerError::InvalidArgument( + "number_of_splits must be greater than 0".to_string(), + )); + } + + let covenant = Covenant::default(); + let default_metadata_size = self.default_metadata_size(); + let mut dest_outputs = Vec::with_capacity(number_of_splits + 1); + let total_split_amount = MicroTari::from(amount_per_split.as_u64() * number_of_splits as u64); + + // accumulated value amount from given source outputs + let accumulated_amount = src_outputs + .iter() + .fold(MicroTari::zero(), |acc, x| acc + x.unblinded_output.value); + + if total_split_amount >= accumulated_amount { + return Err(OutputManagerError::NotEnoughFunds); + } + + let fee_without_change = self.get_fee_calc().calculate( + fee_per_gram, + 1, + src_outputs.len(), + number_of_splits, + default_metadata_size * number_of_splits, + ); + + // checking whether a total output value is enough + if accumulated_amount < (total_split_amount + fee_without_change) { + error!( + target: LOG_TARGET, + "failed to split coins, not enough funds with `fee_without_change` included" + ); + return Err(OutputManagerError::NotEnoughFunds); + } + + let final_fee = match accumulated_amount + .saturating_sub(total_split_amount + fee_without_change) + .as_u64() + { + 0 => fee_without_change, + _ => self.get_fee_calc().calculate( + fee_per_gram, + 1, + src_outputs.len(), + number_of_splits + 1, + default_metadata_size * (number_of_splits + 1), + ), + }; + + // checking, again, whether a total output value is enough + if accumulated_amount < (total_split_amount + final_fee) { + error!( + target: LOG_TARGET, + "failed to split coins, not enough funds with `final_fee` included" + ); + return Err(OutputManagerError::NotEnoughFunds); + } + + // preliminary balance check + if self.get_balance(None)?.available_balance < (total_split_amount + final_fee) { + return Err(OutputManagerError::NotEnoughFunds); + } + + // NOTE: called `leftover` to remove possible brainlag by confusing `change` as a verb + let leftover_change = accumulated_amount.saturating_sub(total_split_amount + final_fee); + + // ---------------------------------------------------------------------------- + // initializing new transaction + + trace!(target: LOG_TARGET, "initializing new split transaction"); + + let mut tx_builder = SenderTransactionProtocol::builder(0, self.resources.consensus_constants.clone()); + tx_builder + .with_lock_height(0) .with_fee_per_gram(fee_per_gram) - .with_offset(offset.clone()) - .with_private_nonce(nonce.clone()) + .with_offset(PrivateKey::random(&mut OsRng)) + .with_private_nonce(PrivateKey::random(&mut OsRng)) .with_rewindable_outputs(self.resources.rewind_data.clone()); - trace!(target: LOG_TARGET, "Add inputs to coin split transaction."); - for uo in input_selection.iter() { - builder.with_input( - uo.unblinded_output - .as_transaction_input(&self.resources.factories.commitment)?, - uo.unblinded_output.clone(), + // collecting inputs from source outputs + let inputs: Vec = src_outputs + .iter() + .map(|src_out| { + src_out + .unblinded_output + .as_transaction_input(&self.resources.factories.commitment) + }) + .try_collect()?; + + // adding inputs to the transaction + src_outputs.iter().zip(inputs).for_each(|(src_output, input)| { + trace!( + target: LOG_TARGET, + "adding transaction input: output_hash=: {:?}", + src_output.hash ); - } + tx_builder.with_input(input, src_output.unblinded_output.clone()); + }); - let utxos_total_value = input_selection.total_value(); - trace!(target: LOG_TARGET, "Add outputs to coin split transaction."); - let mut outputs: Vec = Vec::with_capacity(output_count); - for _ in 0..output_count { - let output_amount = amount_per_split; + // ---------------------------------------------------------------------------- + // initializing primary outputs + for _ in 0..number_of_splits { + let noop_script = script!(Nop); let (spending_key, script_private_key) = self.get_spend_and_script_keys().await?; - let recovery_byte = self.calculate_recovery_byte(spending_key.clone(), output_amount.as_u64(), true)?; let output_features = OutputFeatures { - recovery_byte, + recovery_byte: self.calculate_recovery_byte(spending_key.clone(), total_split_amount.as_u64(), true)?, ..Default::default() }; + // generating sender's keypair let sender_offset_private_key = PrivateKey::random(&mut OsRng); let sender_offset_public_key = PublicKey::from_secret_key(&sender_offset_private_key); - let metadata_signature = TransactionOutput::create_final_metadata_signature( + + let commitment_signature = TransactionOutput::create_final_metadata_signature( TransactionOutputVersion::get_current_version(), - output_amount, + amount_per_split, &spending_key, - &script, + &noop_script, &output_features, &sender_offset_private_key, &covenant, )?; - let utxo = DbUnblindedOutput::rewindable_from_unblinded_output( + + let output = DbUnblindedOutput::rewindable_from_unblinded_output( UnblindedOutput::new_current_version( - output_amount, + amount_per_split, spending_key, output_features, - script.clone(), + noop_script, inputs!(PublicKey::from_secret_key(&script_private_key)), script_private_key, sender_offset_public_key, - metadata_signature, + commitment_signature, 0, covenant.clone(), ), @@ -1636,48 +1893,58 @@ where None, None, )?; - builder - .with_output(utxo.unblinded_output.clone(), sender_offset_private_key) + + tx_builder + .with_output(output.unblinded_output.clone(), sender_offset_private_key) .map_err(|e| OutputManagerError::BuildError(e.message))?; - outputs.push(utxo); + + dest_outputs.push(output); } - if input_selection.requires_change_output() { + let has_leftover_change = leftover_change > MicroTari::zero(); + + // extending transaction if there is some `change` left over + if has_leftover_change { let (spending_key, script_private_key) = self.get_spend_and_script_keys().await?; - builder.with_change_secret(spending_key); - builder.with_rewindable_outputs(self.resources.rewind_data.clone()); - builder.with_change_script( + tx_builder.with_change_secret(spending_key); + tx_builder.with_rewindable_outputs(self.resources.rewind_data.clone()); + tx_builder.with_change_script( script!(Nop), inputs!(PublicKey::from_secret_key(&script_private_key)), script_private_key, ); } - let factories = CryptoFactories::default(); - let mut stp = builder + let mut stp = tx_builder .build::( &self.resources.factories, None, self.last_seen_tip_height.unwrap_or(u64::MAX), ) .map_err(|e| OutputManagerError::BuildError(e.message))?; + // The Transaction Protocol built successfully so we will pull the unspent outputs out of the unspent list and // store them until the transaction times out OR is confirmed let tx_id = stp.get_tx_id()?; + trace!( target: LOG_TARGET, - "Encumber coin split transaction ({}) outputs.", + "Encumber coin split transaction (tx_id={}) outputs", tx_id ); - if input_selection.requires_change_output() { - let unblinded_output = stp.get_change_unblinded_output()?.ok_or_else(|| { + // again, to obtain output for leftover change + if has_leftover_change { + // obtaining output for the `change` + let unblinded_output_for_change = stp.get_change_unblinded_output()?.ok_or_else(|| { OutputManagerError::BuildError( - "There should be a change output metadata signature available".to_string(), + "There should be a `change` output metadata signature available".to_string(), ) })?; - outputs.push(DbUnblindedOutput::rewindable_from_unblinded_output( - unblinded_output, + + // appending `change` output to the result + dest_outputs.push(DbUnblindedOutput::rewindable_from_unblinded_output( + unblinded_output_for_change, &self.resources.factories, &self.resources.rewind_data.clone(), None, @@ -1685,19 +1952,188 @@ where )?); } + // encumbering transaction self.resources .db - .encumber_outputs(tx_id, input_selection.into_selected(), outputs)?; + .encumber_outputs(tx_id, src_outputs.clone(), dest_outputs)?; self.confirm_encumberance(tx_id)?; - trace!(target: LOG_TARGET, "Finalize coin split transaction ({}).", tx_id); + + trace!( + target: LOG_TARGET, + "finalizing coin split transaction (tx_id={}).", + tx_id + ); + + // finalizing transaction stp.finalize( KernelFeatures::empty(), - &factories, + &self.resources.factories, None, self.last_seen_tip_height.unwrap_or(u64::MAX), )?; - let tx = stp.take_transaction()?; - Ok((tx_id, tx, utxos_total_value)) + + let value = if has_leftover_change { + total_split_amount + } else { + total_split_amount + final_fee + }; + + Ok((tx_id, stp.take_transaction()?, value)) + } + + #[allow(clippy::too_many_lines)] + pub async fn create_coin_join( + &mut self, + commitments: Vec, + fee_per_gram: MicroTari, + ) -> Result<(TxId, Transaction, MicroTari), OutputManagerError> { + let covenant = Covenant::default(); + let noop_script = script!(Nop); + let default_metadata_size = self.default_metadata_size(); + + let src_outputs = self.resources.db.fetch_unspent_outputs_for_spending( + UtxoSelectionCriteria::specific(commitments), + MicroTari::zero(), + None, + )?; + + let accumulated_amount = src_outputs + .iter() + .fold(MicroTari::zero(), |acc, x| acc + x.unblinded_output.value); + + let fee = self + .get_fee_calc() + .calculate(fee_per_gram, 1, src_outputs.len(), 1, default_metadata_size); + + let aftertax_amount = accumulated_amount.saturating_sub(fee); + + // checking, again, whether a total output value is enough + if aftertax_amount == MicroTari::zero() { + error!(target: LOG_TARGET, "failed to join coins, not enough funds"); + return Err(OutputManagerError::NotEnoughFunds); + } + + // preliminary balance check + if self.get_balance(None)?.available_balance < aftertax_amount { + return Err(OutputManagerError::NotEnoughFunds); + } + + // ---------------------------------------------------------------------------- + // initializing new transaction + + trace!(target: LOG_TARGET, "initializing new join transaction"); + + let mut tx_builder = SenderTransactionProtocol::builder(0, self.resources.consensus_constants.clone()); + tx_builder + .with_lock_height(0) + .with_fee_per_gram(fee_per_gram) + .with_offset(PrivateKey::random(&mut OsRng)) + .with_private_nonce(PrivateKey::random(&mut OsRng)) + .with_rewindable_outputs(self.resources.rewind_data.clone()); + + // collecting inputs from source outputs + let inputs: Vec = src_outputs + .iter() + .map(|src_out| { + src_out + .unblinded_output + .as_transaction_input(&self.resources.factories.commitment) + }) + .try_collect()?; + + // adding inputs to the transaction + src_outputs.iter().zip(inputs).for_each(|(src_output, input)| { + trace!( + target: LOG_TARGET, + "adding transaction input: output_hash=: {:?}", + src_output.hash + ); + tx_builder.with_input(input, src_output.unblinded_output.clone()); + }); + + // initializing primary output + let (spending_key, script_private_key) = self.get_spend_and_script_keys().await?; + let output_features = OutputFeatures { + recovery_byte: self.calculate_recovery_byte(spending_key.clone(), accumulated_amount.as_u64(), true)?, + ..Default::default() + }; + + // generating sender's keypair + let sender_offset_private_key = PrivateKey::random(&mut OsRng); + let sender_offset_public_key = PublicKey::from_secret_key(&sender_offset_private_key); + + let commitment_signature = TransactionOutput::create_final_metadata_signature( + TransactionOutputVersion::get_current_version(), + aftertax_amount, + &spending_key, + &noop_script, + &output_features, + &sender_offset_private_key, + &covenant, + )?; + + let output = DbUnblindedOutput::rewindable_from_unblinded_output( + UnblindedOutput::new_current_version( + aftertax_amount, + spending_key, + output_features, + noop_script, + inputs!(PublicKey::from_secret_key(&script_private_key)), + script_private_key, + sender_offset_public_key, + commitment_signature, + 0, + covenant.clone(), + ), + &self.resources.factories, + &self.resources.rewind_data.clone(), + None, + None, + )?; + + tx_builder + .with_output(output.unblinded_output.clone(), sender_offset_private_key) + .map_err(|e| OutputManagerError::BuildError(e.message))?; + + let mut stp = tx_builder + .build::( + &self.resources.factories, + None, + self.last_seen_tip_height.unwrap_or(u64::MAX), + ) + .map_err(|e| OutputManagerError::BuildError(e.message))?; + + // The Transaction Protocol built successfully so we will pull the unspent outputs out of the unspent list and + // store them until the transaction times out OR is confirmed + let tx_id = stp.get_tx_id()?; + + trace!( + target: LOG_TARGET, + "Encumber coin join transaction (tx_id={}) outputs", + tx_id + ); + + // encumbering transaction + self.resources + .db + .encumber_outputs(tx_id, src_outputs.clone(), vec![output])?; + self.confirm_encumberance(tx_id)?; + + trace!( + target: LOG_TARGET, + "finalizing coin join transaction (tx_id={}).", + tx_id + ); + + // finalizing transaction + stp.finalize( + KernelFeatures::empty(), + &self.resources.factories, + None, + self.last_seen_tip_height.unwrap_or(u64::MAX), + )?; + + Ok((tx_id, stp.take_transaction()?, aftertax_amount + fee)) } async fn fetch_outputs_from_node( @@ -1868,7 +2304,6 @@ where ); let factories = CryptoFactories::default(); - println!("he`"); let mut stp = builder .build::( &self.resources.factories, @@ -2077,6 +2512,7 @@ struct UtxoSelection { fee_with_change: MicroTari, } +#[allow(dead_code)] impl UtxoSelection { pub fn as_final_fee(&self) -> MicroTari { if self.requires_change_output { 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 0e55dafacd..31bbf86ed0 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 @@ -61,6 +61,7 @@ pub enum SortDirection { pub struct OutputBackendQuery { pub tip_height: i64, pub status: Vec, + pub commitments: Vec, pub pagination: Option<(i64, i64)>, pub value_min: Option<(i64, bool)>, pub value_max: Option<(i64, bool)>, @@ -72,6 +73,7 @@ impl Default for OutputBackendQuery { Self { tip_height: i64::MAX, status: vec![OutputStatus::Spent], + commitments: vec![], pagination: None, value_min: None, value_max: None, 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 8ea2a6103b..9674504c3d 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 @@ -143,6 +143,17 @@ impl OutputSql { _ => query.filter(outputs::status.eq_any::>(q.status.into_iter().map(|s| s as i32).collect())), }; + // filtering by Commitment + if !q.commitments.is_empty() { + query = match q.commitments.len() { + 0 => query, + 1 => query.filter(outputs::commitment.eq(q.commitments[0].to_vec())), + _ => query.filter( + outputs::commitment.eq_any::>>(q.commitments.into_iter().map(|c| c.to_vec()).collect()), + ), + }; + } + // if set, filtering by minimum value if let Some((min, is_inclusive)) = q.value_min { query = if is_inclusive { @@ -205,8 +216,15 @@ impl OutputSql { .filter(outputs::features_unique_id.eq(unique_id)) .filter(outputs::features_parent_public_key.eq(parent_public_key.as_ref().map(|pk| pk.to_vec()))); }, - UtxoSelectionFilter::SpecificOutputs { outputs } => { - query = query.filter(outputs::hash.eq_any(outputs.into_iter().map(|o| o.hash))) + UtxoSelectionFilter::SpecificOutputs { commitments } => { + query = match commitments.len() { + 0 => query, + 1 => query.filter(outputs::commitment.eq(commitments[0].to_vec())), + _ => query.filter( + outputs::commitment + .eq_any::>>(commitments.into_iter().map(|c| c.to_vec()).collect()), + ), + }; }, } diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index f00c1a4404..59e2e3234e 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -27,7 +27,7 @@ use log::*; use tari_common::configuration::bootstrap::ApplicationType; use tari_common_types::{ transaction::{ImportStatus, TxId}, - types::{ComSignature, PrivateKey, PublicKey}, + types::{ComSignature, Commitment, PrivateKey, PublicKey}, }; use tari_comms::{ multiaddr::Multiaddr, @@ -539,15 +539,15 @@ where /// Do a coin split pub async fn coin_split( &mut self, + commitments: Vec, amount_per_split: MicroTari, split_count: usize, fee_per_gram: MicroTari, message: String, - lock_height: Option, ) -> Result { let coin_split_tx = self .output_manager_service - .create_coin_split(amount_per_split, split_count, fee_per_gram, lock_height) + .create_coin_split(commitments, amount_per_split, split_count, fee_per_gram) .await; match coin_split_tx { @@ -565,6 +565,33 @@ where } } + pub async fn coin_join( + &mut self, + commitments: Vec, + fee_per_gram: MicroTari, + msg: Option, + ) -> Result { + let coin_join_tx = self + .output_manager_service + .create_coin_join(commitments, fee_per_gram) + .await; + + match coin_join_tx { + Ok((tx_id, tx, output_value)) => { + let coin_tx = self + .transaction_service + .submit_transaction(tx_id, tx, output_value, msg.unwrap_or_default()) + .await; + + match coin_tx { + Ok(_) => Ok(tx_id), + Err(e) => Err(WalletError::TransactionServiceError(e)), + } + }, + Err(e) => Err(WalletError::OutputManagerError(e)), + } + } + /// Apply encryption to all the Wallet db backends. The Wallet backend will test if the db's are already encrypted /// in which case this will fail. pub async fn apply_encryption(&mut self, passphrase: String) -> Result<(), WalletError> { diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index fe4e8d085d..1ad87ccb50 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -105,9 +105,8 @@ use crate::support::{ }; fn default_metadata_byte_size() -> usize { - let output_features = OutputFeatures { ..Default::default() }; TransactionWeight::latest().round_up_metadata_size( - output_features.consensus_encode_exact_size() + script![Nop].consensus_encode_exact_size(), + OutputFeatures::default().consensus_encode_exact_size() + script![Nop].consensus_encode_exact_size(), ) } @@ -369,1831 +368,1843 @@ async fn generate_sender_transaction_message( ) } -#[tokio::test] -async fn fee_estimate() { - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); +#[cfg(test)] +mod tests { + use super::*; - let factories = CryptoFactories::default(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - - let (_, uo) = make_input(&mut OsRng.clone(), MicroTari::from(3000), &factories.commitment, None).await; - oms.output_manager_handle.add_output(uo, None).await.unwrap(); - let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); - // minimum fpg - let fee_per_gram = MicroTari::from(1); - let fee = oms - .output_manager_handle - .fee_estimate(MicroTari::from(100), fee_per_gram, 1, 1) - .await - .unwrap(); - assert_eq!( - fee, - fee_calc.calculate(fee_per_gram, 1, 1, 2, 2 * default_metadata_byte_size()) - ); + #[tokio::test] + async fn fee_estimate() { + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + + let factories = CryptoFactories::default(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - let fee_per_gram = MicroTari::from(5); - for outputs in 1..5 { + let (_, uo) = make_input(&mut OsRng.clone(), MicroTari::from(3000), &factories.commitment, None).await; + oms.output_manager_handle.add_output(uo, None).await.unwrap(); + let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); + // minimum fpg + let fee_per_gram = MicroTari::from(1); let fee = oms .output_manager_handle - .fee_estimate(MicroTari::from(100), fee_per_gram, 1, outputs) + .fee_estimate(MicroTari::from(100), fee_per_gram, 1, 1) .await .unwrap(); - assert_eq!( fee, - fee_calc.calculate( - fee_per_gram, - 1, - 1, - outputs + 1, - default_metadata_byte_size() * (outputs + 1) - ) + fee_calc.calculate(fee_per_gram, 1, 1, 2, 2 * default_metadata_byte_size()) ); - } - // not enough funds - let err = oms - .output_manager_handle - .fee_estimate(MicroTari::from(2750), fee_per_gram, 1, 1) - .await - .unwrap_err(); - assert!(matches!(err, OutputManagerError::NotEnoughFunds)); -} + let fee_per_gram = MicroTari::from(5); + for outputs in 1..5 { + let fee = oms + .output_manager_handle + .fee_estimate(MicroTari::from(100), fee_per_gram, 1, outputs) + .await + .unwrap(); + + assert_eq!( + fee, + fee_calc.calculate( + fee_per_gram, + 1, + 1, + outputs + 1, + default_metadata_byte_size() * (outputs + 1) + ) + ); + } -#[allow(clippy::identity_op)] -#[tokio::test] -async fn test_utxo_selection_no_chain_metadata() { - let factories = CryptoFactories::default(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let server_node_identity = build_node_identity(PeerFeatures::COMMUNICATION_NODE); - // no chain metadata - let (mut oms, _shutdown, _, _, _) = setup_oms_with_bn_state( - OutputManagerSqliteDatabase::new(connection, None), - None, - server_node_identity, - ) - .await; - - let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); - // no utxos - not enough funds - let amount = MicroTari::from(1000); - let fee_per_gram = MicroTari::from(2); - let err = oms - .prepare_transaction_to_send( - TxId::new_random(), - amount, - None, - None, - fee_per_gram, - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap_err(); - assert!(matches!(err, OutputManagerError::NotEnoughFunds)); + // not enough funds + let err = oms + .output_manager_handle + .fee_estimate(MicroTari::from(2750), fee_per_gram, 1, 1) + .await + .unwrap_err(); + assert!(matches!(err, OutputManagerError::NotEnoughFunds)); + } - // create 10 utxos with maturity at heights from 1 to 10 - for i in 1..=10 { - let (_, uo) = make_input_with_features( - &mut OsRng.clone(), - i * amount, - &factories.commitment, - Some(OutputFeatures { - maturity: i, - ..Default::default() - }), - oms.clone(), + #[ignore] + #[allow(clippy::identity_op)] + #[tokio::test] + async fn test_utxo_selection_no_chain_metadata() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let server_node_identity = build_node_identity(PeerFeatures::COMMUNICATION_NODE); + // no chain metadata + let (mut oms, _shutdown, _, _, _) = setup_oms_with_bn_state( + OutputManagerSqliteDatabase::new(connection, None), + None, + server_node_identity, ) .await; - oms.add_rewindable_output(uo.clone(), None, None).await.unwrap(); + + let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); + // no utxos - not enough funds + let amount = MicroTari::from(1000); + let fee_per_gram = MicroTari::from(2); + let err = oms + .prepare_transaction_to_send( + TxId::new_random(), + amount, + None, + None, + fee_per_gram, + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap_err(); + assert!(matches!(err, OutputManagerError::NotEnoughFunds)); + + // create 10 utxos with maturity at heights from 1 to 10 + for i in 1..=10 { + let (_, uo) = make_input_with_features( + &mut OsRng.clone(), + i * amount, + &factories.commitment, + Some(OutputFeatures { + maturity: i, + ..Default::default() + }), + oms.clone(), + ) + .await; + oms.add_rewindable_output(uo.clone(), None, None).await.unwrap(); + } + + // but we have no chain state so the lowest maturity should be used + let stp = oms + .prepare_transaction_to_send( + TxId::new_random(), + amount, + None, + None, + fee_per_gram, + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); + assert!(stp.get_tx_id().is_ok()); + + // test that lowest 2 maturities were encumbered + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 8); + for (index, utxo) in utxos.iter().enumerate() { + let i = index as u64 + 3; + assert_eq!(utxo.features.maturity, i); + assert_eq!(utxo.value, i * amount); + } + + // test that we can get a fee estimate with no chain metadata + let fee = oms.fee_estimate(amount, fee_per_gram, 1, 2).await.unwrap(); + let expected_fee = fee_calc.calculate(fee_per_gram, 1, 1, 3, default_metadata_byte_size() * 3); + assert_eq!(fee, expected_fee); + + // test if a fee estimate would be possible with pending funds included + // at this point 52000 uT is still spendable, with pending change incoming of 1690 uT + // so instead of returning "not enough funds", return "funds pending" + let spendable_amount = (3..=10).sum::() * amount; + let err = oms + .fee_estimate(spendable_amount, fee_per_gram, 1, 2) + .await + .unwrap_err(); + assert!(matches!(err, OutputManagerError::FundsPending)); + + // test not enough funds + let broke_amount = spendable_amount + MicroTari::from(2000); + let err = oms.fee_estimate(broke_amount, fee_per_gram, 1, 2).await.unwrap_err(); + assert!(matches!(err, OutputManagerError::NotEnoughFunds)); + + // coin split uses the "Largest" selection strategy + let (_, tx, utxos_total_value) = oms.create_coin_split(vec![], amount, 5, fee_per_gram).await.unwrap(); + let expected_fee = fee_calc.calculate(fee_per_gram, 1, 1, 6, default_metadata_byte_size() * 6); + assert_eq!(tx.body.get_total_fee(), expected_fee); + assert_eq!(utxos_total_value, MicroTari::from(10_000)); + + // test that largest utxo was encumbered + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 7); + for (index, utxo) in utxos.iter().enumerate() { + let i = index as u64 + 3; + assert_eq!(utxo.features.maturity, i); + assert_eq!(utxo.value, i * amount); + } } - // but we have no chain state so the lowest maturity should be used - let stp = oms - .prepare_transaction_to_send( - TxId::new_random(), - amount, - None, - None, - fee_per_gram, - None, - "".to_string(), - script!(Nop), - Covenant::default(), + #[tokio::test] + #[allow(clippy::identity_op)] + #[allow(clippy::too_many_lines)] + #[ignore] + async fn test_utxo_selection_with_chain_metadata() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + + let server_node_identity = build_node_identity(PeerFeatures::COMMUNICATION_NODE); + // setup with chain metadata at a height of 6 + let (mut oms, _shutdown, _, _, _) = setup_oms_with_bn_state( + OutputManagerSqliteDatabase::new(connection, None), + Some(6), + server_node_identity, ) - .await - .unwrap(); - assert!(stp.get_tx_id().is_ok()); - - // test that lowest 2 maturities were encumbered - let utxos = oms.get_unspent_outputs().await.unwrap(); - assert_eq!(utxos.len(), 8); - for (index, utxo) in utxos.iter().enumerate() { - let i = index as u64 + 3; - assert_eq!(utxo.features.maturity, i); - assert_eq!(utxo.value, i * amount); - } + .await; + let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); + + // no utxos - not enough funds + let amount = MicroTari::from(1000); + let fee_per_gram = MicroTari::from(2); + let err = oms + .prepare_transaction_to_send( + TxId::new_random(), + amount, + None, + None, + fee_per_gram, + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap_err(); + assert!(matches!(err, OutputManagerError::NotEnoughFunds)); + + // create 10 utxos with maturity at heights from 1 to 10 + for i in 1..=10 { + let (_, uo) = make_input_with_features( + &mut OsRng.clone(), + i * amount, + &factories.commitment, + Some(OutputFeatures { + maturity: i, + ..Default::default() + }), + oms.clone(), + ) + .await; + oms.add_rewindable_output(uo.clone(), None, None).await.unwrap(); + } - // test that we can get a fee estimate with no chain metadata - let fee = oms.fee_estimate(amount, fee_per_gram, 1, 2).await.unwrap(); - let expected_fee = fee_calc.calculate(fee_per_gram, 1, 1, 3, default_metadata_byte_size() * 3); - assert_eq!(fee, expected_fee); - - // test if a fee estimate would be possible with pending funds included - // at this point 52000 uT is still spendable, with pending change incoming of 1690 uT - // so instead of returning "not enough funds", return "funds pending" - let spendable_amount = (3..=10).sum::() * amount; - let err = oms - .fee_estimate(spendable_amount, fee_per_gram, 1, 2) - .await - .unwrap_err(); - assert!(matches!(err, OutputManagerError::FundsPending)); - - // test not enough funds - let broke_amount = spendable_amount + MicroTari::from(2000); - let err = oms.fee_estimate(broke_amount, fee_per_gram, 1, 2).await.unwrap_err(); - assert!(matches!(err, OutputManagerError::NotEnoughFunds)); - - // coin split uses the "Largest" selection strategy - let (_, tx, utxos_total_value) = oms.create_coin_split(amount, 5, fee_per_gram, None).await.unwrap(); - let expected_fee = fee_calc.calculate(fee_per_gram, 1, 1, 6, default_metadata_byte_size() * 6); - assert_eq!(tx.body.get_total_fee(), expected_fee); - assert_eq!(utxos_total_value, MicroTari::from(10_000)); - - // test that largest utxo was encumbered - let utxos = oms.get_unspent_outputs().await.unwrap(); - assert_eq!(utxos.len(), 7); - for (index, utxo) in utxos.iter().enumerate() { - let i = index as u64 + 3; - assert_eq!(utxo.features.maturity, i); - assert_eq!(utxo.value, i * amount); + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 10); + + // test fee estimates + let fee = oms.fee_estimate(amount, fee_per_gram, 1, 2).await.unwrap(); + let expected_fee = fee_calc.calculate(fee_per_gram, 1, 2, 3, default_metadata_byte_size() * 3); + assert_eq!(fee, expected_fee); + + // test fee estimates are maturity aware + // even though we have utxos for the fee, they can't be spent because they are not mature yet + let spendable_amount = (1..=6).sum::() * amount; + let err = oms + .fee_estimate(spendable_amount, fee_per_gram, 1, 2) + .await + .unwrap_err(); + assert!(matches!(err, OutputManagerError::NotEnoughFunds)); + + // test coin split is maturity aware + let (_, tx, utxos_total_value) = oms.create_coin_split(vec![], amount, 5, fee_per_gram).await.unwrap(); + assert_eq!(utxos_total_value, MicroTari::from(6_000)); + let expected_fee = fee_calc.calculate(fee_per_gram, 1, 1, 6, default_metadata_byte_size() * 6); + assert_eq!(tx.body.get_total_fee(), expected_fee); + + // test that largest spendable utxo was encumbered + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 9); + let found = utxos.iter().any(|u| u.value == 6 * amount); + assert!(!found, "An unspendable utxo was selected"); + + // test transactions + let stp = oms + .prepare_transaction_to_send( + TxId::new_random(), + amount, + None, + None, + fee_per_gram, + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); + assert!(stp.get_tx_id().is_ok()); + + // test that utxos with the lowest 2 maturities were encumbered + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 7); + for utxo in &utxos { + assert_ne!(utxo.features.maturity, 1); + assert_ne!(utxo.value, amount); + assert_ne!(utxo.features.maturity, 2); + assert_ne!(utxo.value, 2 * amount); + } + + // when the amount is greater than the largest utxo, then "Largest" selection strategy is used + let stp = oms + .prepare_transaction_to_send( + TxId::new_random(), + 6 * amount, + None, + None, + fee_per_gram, + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); + assert!(stp.get_tx_id().is_ok()); + + // test that utxos with the highest spendable 2 maturities were encumbered + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 5); + for utxo in &utxos { + assert_ne!(utxo.features.maturity, 4); + assert_ne!(utxo.value, 4 * amount); + assert_ne!(utxo.features.maturity, 5); + assert_ne!(utxo.value, 5 * amount); + } } -} -#[tokio::test] -#[allow(clippy::identity_op)] -#[allow(clippy::too_many_lines)] -async fn test_utxo_selection_with_chain_metadata() { - let factories = CryptoFactories::default(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); + #[tokio::test] + async fn test_utxo_selection_with_tx_priority() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + + let server_node_identity = build_node_identity(PeerFeatures::COMMUNICATION_NODE); + // setup with chain metadata at a height of 6 + let (mut oms, _shutdown, _, _, _) = setup_oms_with_bn_state( + OutputManagerSqliteDatabase::new(connection, None), + Some(6), + server_node_identity, + ) + .await; - let server_node_identity = build_node_identity(PeerFeatures::COMMUNICATION_NODE); - // setup with chain metadata at a height of 6 - let (mut oms, _shutdown, _, _, _) = setup_oms_with_bn_state( - OutputManagerSqliteDatabase::new(connection, None), - Some(6), - server_node_identity, - ) - .await; - let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); - - // no utxos - not enough funds - let amount = MicroTari::from(1000); - let fee_per_gram = MicroTari::from(2); - let err = oms - .prepare_transaction_to_send( - TxId::new_random(), + let amount = MicroTari::from(2000); + let fee_per_gram = MicroTari::from(2); + + // we create two outputs, one as coinbase-high priority one as normal so we can track them + let (_, uo) = make_input_with_features( + &mut OsRng.clone(), amount, - None, - None, - fee_per_gram, - None, - "".to_string(), - script!(Nop), - Covenant::default(), + &factories.commitment, + Some(OutputFeatures::create_coinbase(1, rand::thread_rng().gen::())), + oms.clone(), ) - .await - .unwrap_err(); - assert!(matches!(err, OutputManagerError::NotEnoughFunds)); - - // create 10 utxos with maturity at heights from 1 to 10 - for i in 1..=10 { + .await; + oms.add_rewindable_output(uo, Some(SpendingPriority::HtlcSpendAsap), None) + .await + .unwrap(); let (_, uo) = make_input_with_features( &mut OsRng.clone(), - i * amount, + amount, &factories.commitment, Some(OutputFeatures { - maturity: i, + maturity: 1, ..Default::default() }), oms.clone(), ) .await; - oms.add_rewindable_output(uo.clone(), None, None).await.unwrap(); - } - - let utxos = oms.get_unspent_outputs().await.unwrap(); - assert_eq!(utxos.len(), 10); + oms.add_rewindable_output(uo, None, None).await.unwrap(); + + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 2); + + // test transactions + let stp = oms + .prepare_transaction_to_send( + TxId::new_random(), + MicroTari::from(1000), + None, + None, + fee_per_gram, + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); + assert!(stp.get_tx_id().is_ok()); - // test fee estimates - let fee = oms.fee_estimate(amount, fee_per_gram, 1, 2).await.unwrap(); - let expected_fee = fee_calc.calculate(fee_per_gram, 1, 2, 3, default_metadata_byte_size() * 3); - assert_eq!(fee, expected_fee); + // test that the utxo with the lowest priority was left + let utxos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(utxos.len(), 1); - // test fee estimates are maturity aware - // even though we have utxos for the fee, they can't be spent because they are not mature yet - let spendable_amount = (1..=6).sum::() * amount; - let err = oms - .fee_estimate(spendable_amount, fee_per_gram, 1, 2) - .await - .unwrap_err(); - assert!(matches!(err, OutputManagerError::NotEnoughFunds)); - - // test coin split is maturity aware - let (_, tx, utxos_total_value) = oms.create_coin_split(amount, 5, fee_per_gram, None).await.unwrap(); - assert_eq!(utxos_total_value, MicroTari::from(6_000)); - let expected_fee = fee_calc.calculate(fee_per_gram, 1, 1, 6, default_metadata_byte_size() * 6); - assert_eq!(tx.body.get_total_fee(), expected_fee); - - // test that largest spendable utxo was encumbered - let utxos = oms.get_unspent_outputs().await.unwrap(); - assert_eq!(utxos.len(), 9); - let found = utxos.iter().any(|u| u.value == 6 * amount); - assert!(!found, "An unspendable utxo was selected"); - - // test transactions - let stp = oms - .prepare_transaction_to_send( - TxId::new_random(), - amount, - None, - None, - fee_per_gram, - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); - assert!(stp.get_tx_id().is_ok()); - - // test that utxos with the lowest 2 maturities were encumbered - let utxos = oms.get_unspent_outputs().await.unwrap(); - assert_eq!(utxos.len(), 7); - for utxo in &utxos { - assert_ne!(utxo.features.maturity, 1); - assert_ne!(utxo.value, amount); - assert_ne!(utxo.features.maturity, 2); - assert_ne!(utxo.value, 2 * amount); + assert!(!utxos[0].features.flags.contains(OutputFlags::COINBASE_OUTPUT)); } - // when the amount is greater than the largest utxo, then "Largest" selection strategy is used - let stp = oms - .prepare_transaction_to_send( - TxId::new_random(), - 6 * amount, - None, - None, - fee_per_gram, - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); - assert!(stp.get_tx_id().is_ok()); - - // test that utxos with the highest spendable 2 maturities were encumbered - let utxos = oms.get_unspent_outputs().await.unwrap(); - assert_eq!(utxos.len(), 5); - for utxo in &utxos { - assert_ne!(utxo.features.maturity, 4); - assert_ne!(utxo.value, 4 * amount); - assert_ne!(utxo.features.maturity, 5); - assert_ne!(utxo.value, 5 * amount); + #[tokio::test] + async fn send_not_enough_funds() { + let factories = CryptoFactories::default(); + + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + let num_outputs = 20; + for _i in 0..num_outputs { + let (_ti, uo) = make_input( + &mut OsRng.clone(), + MicroTari::from(200 + OsRng.next_u64() % 1000), + &factories.commitment, + None, + ) + .await; + oms.output_manager_handle.add_output(uo, None).await.unwrap(); + } + + match oms + .output_manager_handle + .prepare_transaction_to_send( + TxId::new_random(), + MicroTari::from(num_outputs * 2000), + None, + None, + MicroTari::from(4), + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + { + Err(OutputManagerError::NotEnoughFunds) => {}, + _ => panic!(), + } } -} -#[tokio::test] -async fn test_utxo_selection_with_tx_priority() { - let factories = CryptoFactories::default(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); + #[tokio::test] + async fn send_no_change() { + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let server_node_identity = build_node_identity(PeerFeatures::COMMUNICATION_NODE); - // setup with chain metadata at a height of 6 - let (mut oms, _shutdown, _, _, _) = setup_oms_with_bn_state( - OutputManagerSqliteDatabase::new(connection, None), - Some(6), - server_node_identity, - ) - .await; - - let amount = MicroTari::from(2000); - let fee_per_gram = MicroTari::from(2); - - // we create two outputs, one as coinbase-high priority one as normal so we can track them - let (_, uo) = make_input_with_features( - &mut OsRng.clone(), - amount, - &factories.commitment, - Some(OutputFeatures::create_coinbase(1, rand::thread_rng().gen::())), - oms.clone(), - ) - .await; - oms.add_rewindable_output(uo, Some(SpendingPriority::HtlcSpendAsap), None) - .await - .unwrap(); - let (_, uo) = make_input_with_features( - &mut OsRng.clone(), - amount, - &factories.commitment, - Some(OutputFeatures { - maturity: 1, - ..Default::default() - }), - oms.clone(), - ) - .await; - oms.add_rewindable_output(uo, None, None).await.unwrap(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - let utxos = oms.get_unspent_outputs().await.unwrap(); - assert_eq!(utxos.len(), 2); + let fee_per_gram = MicroTari::from(4); + let constants = create_consensus_constants(0); + let fee_without_change = + Fee::new(*constants.transaction_weight()).calculate(fee_per_gram, 1, 2, 1, default_metadata_byte_size()); + let value1 = 5000; + oms.output_manager_handle + .add_output( + create_unblinded_output( + script!(Nop), + OutputFeatures::default(), + &TestParamsHelpers::new(), + MicroTari::from(value1), + ), + None, + ) + .await + .unwrap(); + let value2 = 8000; + oms.output_manager_handle + .add_output( + create_unblinded_output( + script!(Nop), + OutputFeatures::default(), + &TestParamsHelpers::new(), + MicroTari::from(value2), + ), + None, + ) + .await + .unwrap(); - // test transactions - let stp = oms - .prepare_transaction_to_send( - TxId::new_random(), - MicroTari::from(1000), - None, - None, - fee_per_gram, - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); - assert!(stp.get_tx_id().is_ok()); + let stp = oms + .output_manager_handle + .prepare_transaction_to_send( + TxId::new_random(), + MicroTari::from(value1 + value2) - fee_without_change, + None, + None, + fee_per_gram, + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); - // test that the utxo with the lowest priority was left - let utxos = oms.get_unspent_outputs().await.unwrap(); - assert_eq!(utxos.len(), 1); + assert_eq!(stp.get_amount_to_self().unwrap(), MicroTari::from(0)); + assert_eq!( + oms.output_manager_handle + .get_balance() + .await + .unwrap() + .pending_incoming_balance, + MicroTari::from(0) + ); + } - assert!(!utxos[0].features.flags.contains(OutputFlags::COINBASE_OUTPUT)); -} + #[tokio::test] + async fn send_not_enough_for_change() { + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); -#[tokio::test] -async fn send_not_enough_funds() { - let factories = CryptoFactories::default(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let fee_per_gram = MicroTari::from(4); + let constants = create_consensus_constants(0); + let fee_without_change = Fee::new(*constants.transaction_weight()).calculate(fee_per_gram, 1, 2, 1, 0); + let value1 = MicroTari(500); + oms.output_manager_handle + .add_output( + create_unblinded_output( + TariScript::default(), + OutputFeatures::default(), + &TestParamsHelpers::new(), + value1, + ), + None, + ) + .await + .unwrap(); + let value2 = MicroTari(800); + oms.output_manager_handle + .add_output( + create_unblinded_output( + TariScript::default(), + OutputFeatures::default(), + &TestParamsHelpers::new(), + value2, + ), + None, + ) + .await + .unwrap(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - let num_outputs = 20; - for _i in 0..num_outputs { - let (_ti, uo) = make_input( - &mut OsRng.clone(), - MicroTari::from(200 + OsRng.next_u64() % 1000), - &factories.commitment, - None, - ) - .await; - oms.output_manager_handle.add_output(uo, None).await.unwrap(); + match oms + .output_manager_handle + .prepare_transaction_to_send( + TxId::new_random(), + value1 + value2 + uT - fee_without_change, + None, + None, + fee_per_gram, + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + { + Err(OutputManagerError::NotEnoughFunds) => {}, + _ => panic!(), + } } - match oms - .output_manager_handle - .prepare_transaction_to_send( - TxId::new_random(), - MicroTari::from(num_outputs * 2000), - None, - None, - MicroTari::from(4), - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - { - Err(OutputManagerError::NotEnoughFunds) => {}, - _ => panic!(), - } -} + #[tokio::test] + async fn cancel_transaction() { + let factories = CryptoFactories::default(); -#[tokio::test] -async fn send_no_change() { - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - let fee_per_gram = MicroTari::from(4); - let constants = create_consensus_constants(0); - let fee_without_change = - Fee::new(*constants.transaction_weight()).calculate(fee_per_gram, 1, 2, 1, default_metadata_byte_size()); - let value1 = 5000; - oms.output_manager_handle - .add_output( - create_unblinded_output( - script!(Nop), - OutputFeatures::default(), - &TestParamsHelpers::new(), - MicroTari::from(value1), - ), - None, - ) - .await - .unwrap(); - let value2 = 8000; - oms.output_manager_handle - .add_output( - create_unblinded_output( + let num_outputs = 20; + for _i in 0..num_outputs { + let (_ti, uo) = make_input( + &mut OsRng.clone(), + MicroTari::from(100 + OsRng.next_u64() % 1000), + &factories.commitment, + None, + ) + .await; + oms.output_manager_handle.add_output(uo, None).await.unwrap(); + } + let stp = oms + .output_manager_handle + .prepare_transaction_to_send( + TxId::new_random(), + MicroTari::from(1000), + None, + None, + MicroTari::from(4), + None, + "".to_string(), script!(Nop), - OutputFeatures::default(), - &TestParamsHelpers::new(), - MicroTari::from(value2), - ), - None, - ) - .await - .unwrap(); + Covenant::default(), + ) + .await + .unwrap(); - let stp = oms - .output_manager_handle - .prepare_transaction_to_send( - TxId::new_random(), - MicroTari::from(value1 + value2) - fee_without_change, - None, - None, - fee_per_gram, - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); + match oms.output_manager_handle.cancel_transaction(1u64.into()).await { + Err(OutputManagerError::OutputManagerStorageError(OutputManagerStorageError::ValueNotFound)) => {}, + _ => panic!("Value should not exist"), + } - assert_eq!(stp.get_amount_to_self().unwrap(), MicroTari::from(0)); - assert_eq!( oms.output_manager_handle - .get_balance() + .cancel_transaction(stp.get_tx_id().unwrap()) .await - .unwrap() - .pending_incoming_balance, - MicroTari::from(0) - ); -} -#[tokio::test] -async fn send_not_enough_for_change() { - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + .unwrap(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + assert_eq!( + oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), + num_outputs + ); + } - let fee_per_gram = MicroTari::from(4); - let constants = create_consensus_constants(0); - let fee_without_change = Fee::new(*constants.transaction_weight()).calculate(fee_per_gram, 1, 2, 1, 0); - let value1 = MicroTari(500); - oms.output_manager_handle - .add_output( - create_unblinded_output( - TariScript::default(), - OutputFeatures::default(), - &TestParamsHelpers::new(), - value1, - ), - None, - ) - .await - .unwrap(); - let value2 = MicroTari(800); - oms.output_manager_handle - .add_output( - create_unblinded_output( - TariScript::default(), - OutputFeatures::default(), - &TestParamsHelpers::new(), - value2, - ), - None, - ) - .await - .unwrap(); + #[tokio::test] + async fn cancel_transaction_and_reinstate_inbound_tx() { + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - match oms - .output_manager_handle - .prepare_transaction_to_send( - TxId::new_random(), - value1 + value2 + uT - fee_without_change, - None, - None, - fee_per_gram, - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - { - Err(OutputManagerError::NotEnoughFunds) => {}, - _ => panic!(), - } -} + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; -#[tokio::test] -async fn cancel_transaction() { - let factories = CryptoFactories::default(); + let value = MicroTari::from(5000); + let (tx_id, sender_message) = + generate_sender_transaction_message(value, Some(oms.output_manager_handle.clone())).await; + let _rtp = oms + .output_manager_handle + .get_recipient_transaction(sender_message) + .await + .unwrap(); + assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!(balance.pending_incoming_balance, value); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + oms.output_manager_handle.cancel_transaction(tx_id).await.unwrap(); - let num_outputs = 20; - for _i in 0..num_outputs { - let (_ti, uo) = make_input( - &mut OsRng.clone(), - MicroTari::from(100 + OsRng.next_u64() % 1000), - &factories.commitment, - None, - ) - .await; - oms.output_manager_handle.add_output(uo, None).await.unwrap(); - } - let stp = oms - .output_manager_handle - .prepare_transaction_to_send( - TxId::new_random(), - MicroTari::from(1000), - None, - None, - MicroTari::from(4), - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!(balance.pending_incoming_balance, MicroTari::from(0)); + + oms.output_manager_handle + .reinstate_cancelled_inbound_transaction_outputs(tx_id) + .await + .unwrap(); - match oms.output_manager_handle.cancel_transaction(1u64.into()).await { - Err(OutputManagerError::OutputManagerStorageError(OutputManagerStorageError::ValueNotFound)) => {}, - _ => panic!("Value should not exist"), + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + + assert_eq!(balance.pending_incoming_balance, value); } - oms.output_manager_handle - .cancel_transaction(stp.get_tx_id().unwrap()) - .await - .unwrap(); + #[tokio::test] + async fn test_get_balance() { + let factories = CryptoFactories::default(); - assert_eq!( - oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), - num_outputs - ); -} + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); -#[tokio::test] -async fn cancel_transaction_and_reinstate_inbound_tx() { - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + let balance = oms.output_manager_handle.get_balance().await.unwrap(); - let value = MicroTari::from(5000); - let (tx_id, sender_message) = - generate_sender_transaction_message(value, Some(oms.output_manager_handle.clone())).await; - let _rtp = oms - .output_manager_handle - .get_recipient_transaction(sender_message) - .await - .unwrap(); - assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); + assert_eq!(MicroTari::from(0), balance.available_balance); - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!(balance.pending_incoming_balance, value); + let mut total = MicroTari::from(0); + let output_val = MicroTari::from(2000); + let (_ti, uo) = make_input(&mut OsRng.clone(), output_val, &factories.commitment, None).await; + total += uo.value; + oms.output_manager_handle.add_output(uo, None).await.unwrap(); - oms.output_manager_handle.cancel_transaction(tx_id).await.unwrap(); + let (_ti, uo) = make_input(&mut OsRng.clone(), output_val, &factories.commitment, None).await; + total += uo.value; + oms.output_manager_handle.add_output(uo, None).await.unwrap(); - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!(balance.pending_incoming_balance, MicroTari::from(0)); + let send_value = MicroTari::from(1000); + let stp = oms + .output_manager_handle + .prepare_transaction_to_send( + TxId::new_random(), + send_value, + None, + None, + MicroTari::from(4), + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); - oms.output_manager_handle - .reinstate_cancelled_inbound_transaction_outputs(tx_id) - .await - .unwrap(); + let change_val = stp.get_change_amount().unwrap(); - let balance = oms.output_manager_handle.get_balance().await.unwrap(); + let recv_value = MicroTari::from(1500); + let (_tx_id, sender_message) = generate_sender_transaction_message(recv_value, None).await; + let _rtp = oms + .output_manager_handle + .get_recipient_transaction(sender_message) + .await + .unwrap(); - assert_eq!(balance.pending_incoming_balance, value); -} + let balance = oms.output_manager_handle.get_balance().await.unwrap(); -#[tokio::test] -async fn test_get_balance() { - let factories = CryptoFactories::default(); + assert_eq!(output_val, balance.available_balance); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); + assert_eq!(recv_value + change_val, balance.pending_incoming_balance); + assert_eq!(output_val, balance.pending_outgoing_balance); + } - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + #[tokio::test] + async fn sending_transaction_persisted_while_offline() { + let factories = CryptoFactories::default(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let balance = oms.output_manager_handle.get_balance().await.unwrap(); + let mut oms = setup_output_manager_service(backend.clone(), ks_backend.clone(), true).await; - assert_eq!(MicroTari::from(0), balance.available_balance); + let available_balance = 20_000 * uT; + let (_ti, uo) = make_input(&mut OsRng.clone(), available_balance / 2, &factories.commitment, None).await; + oms.output_manager_handle.add_output(uo, None).await.unwrap(); + let (_ti, uo) = make_input(&mut OsRng.clone(), available_balance / 2, &factories.commitment, None).await; + oms.output_manager_handle.add_output(uo, None).await.unwrap(); - let mut total = MicroTari::from(0); - let output_val = MicroTari::from(2000); - let (_ti, uo) = make_input(&mut OsRng.clone(), output_val, &factories.commitment, None).await; - total += uo.value; - oms.output_manager_handle.add_output(uo, None).await.unwrap(); + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!(balance.available_balance, available_balance); + assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); + assert_eq!(balance.pending_outgoing_balance, MicroTari::from(0)); - let (_ti, uo) = make_input(&mut OsRng.clone(), output_val, &factories.commitment, None).await; - total += uo.value; - oms.output_manager_handle.add_output(uo, None).await.unwrap(); + // Check that funds are encumbered and stay encumbered if the pending tx is not confirmed before restart + let _stp = oms + .output_manager_handle + .prepare_transaction_to_send( + TxId::new_random(), + MicroTari::from(1000), + None, + None, + MicroTari::from(4), + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); - let send_value = MicroTari::from(1000); - let stp = oms - .output_manager_handle - .prepare_transaction_to_send( - TxId::new_random(), - send_value, - None, - None, - MicroTari::from(4), - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!(balance.available_balance, available_balance / 2); + assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); + assert_eq!(balance.pending_outgoing_balance, available_balance / 2); - let change_val = stp.get_change_amount().unwrap(); + // This simulates an offline wallet with a queued transaction that has not been sent to the receiving wallet + // yet + drop(oms.output_manager_handle); + let mut oms = setup_output_manager_service(backend.clone(), ks_backend.clone(), true).await; - let recv_value = MicroTari::from(1500); - let (_tx_id, sender_message) = generate_sender_transaction_message(recv_value, None).await; - let _rtp = oms - .output_manager_handle - .get_recipient_transaction(sender_message) - .await - .unwrap(); + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!(balance.available_balance, available_balance / 2); + assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); + assert_eq!(balance.pending_outgoing_balance, available_balance / 2); - let balance = oms.output_manager_handle.get_balance().await.unwrap(); + // Check that is the pending tx is confirmed that the encumberance persists after restart + let stp = oms + .output_manager_handle + .prepare_transaction_to_send( + TxId::new_random(), + MicroTari::from(1000), + None, + None, + MicroTari::from(4), + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); + let sender_tx_id = stp.get_tx_id().unwrap(); + oms.output_manager_handle + .confirm_pending_transaction(sender_tx_id) + .await + .unwrap(); - assert_eq!(output_val, balance.available_balance); - assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); - assert_eq!(recv_value + change_val, balance.pending_incoming_balance); - assert_eq!(output_val, balance.pending_outgoing_balance); -} + drop(oms.output_manager_handle); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; -#[tokio::test] -async fn sending_transaction_persisted_while_offline() { - let factories = CryptoFactories::default(); + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!(balance.available_balance, MicroTari::from(0)); + assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); + assert_eq!(balance.pending_outgoing_balance, available_balance); + } - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - - let mut oms = setup_output_manager_service(backend.clone(), ks_backend.clone(), true).await; - - let available_balance = 20_000 * uT; - let (_ti, uo) = make_input(&mut OsRng.clone(), available_balance / 2, &factories.commitment, None).await; - oms.output_manager_handle.add_output(uo, None).await.unwrap(); - let (_ti, uo) = make_input(&mut OsRng.clone(), available_balance / 2, &factories.commitment, None).await; - oms.output_manager_handle.add_output(uo, None).await.unwrap(); - - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!(balance.available_balance, available_balance); - assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); - assert_eq!(balance.pending_outgoing_balance, MicroTari::from(0)); - - // Check that funds are encumbered and stay encumbered if the pending tx is not confirmed before restart - let _stp = oms - .output_manager_handle - .prepare_transaction_to_send( - TxId::new_random(), - MicroTari::from(1000), - None, - None, - MicroTari::from(4), - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); + #[tokio::test] + async fn coin_split_with_change() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + + let val1 = 6_000 * uT; + let val2 = 7_000 * uT; + let val3 = 8_000 * uT; + let (_ti, uo1) = make_input(&mut OsRng, val1, &factories.commitment, None).await; + let (_ti, uo2) = make_input(&mut OsRng, val2, &factories.commitment, None).await; + let (_ti, uo3) = make_input(&mut OsRng, val3, &factories.commitment, None).await; + assert!(oms.output_manager_handle.add_output(uo1, None).await.is_ok()); + assert!(oms.output_manager_handle.add_output(uo2, None).await.is_ok()); + assert!(oms.output_manager_handle.add_output(uo3, None).await.is_ok()); + + let fee_per_gram = MicroTari::from(5); + let split_count = 8; + let (_tx_id, coin_split_tx, amount) = oms + .output_manager_handle + .create_coin_split(vec![], 1000.into(), split_count, fee_per_gram) + .await + .unwrap(); + assert_eq!(coin_split_tx.body.inputs().len(), 2); + assert_eq!(coin_split_tx.body.outputs().len(), split_count + 1); + let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); + let expected_fee = fee_calc.calculate( + fee_per_gram, + 1, + 2, + split_count + 1, + (split_count + 1) * default_metadata_byte_size(), + ); + assert_eq!(coin_split_tx.body.get_total_fee(), expected_fee); + // NOTE: assuming the LargestFirst strategy is used + assert_eq!(amount, val3); + } - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!(balance.available_balance, available_balance / 2); - assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); - assert_eq!(balance.pending_outgoing_balance, available_balance / 2); - - // This simulates an offline wallet with a queued transaction that has not been sent to the receiving wallet yet - drop(oms.output_manager_handle); - let mut oms = setup_output_manager_service(backend.clone(), ks_backend.clone(), true).await; - - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!(balance.available_balance, available_balance / 2); - assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); - assert_eq!(balance.pending_outgoing_balance, available_balance / 2); - - // Check that is the pending tx is confirmed that the encumberance persists after restart - let stp = oms - .output_manager_handle - .prepare_transaction_to_send( - TxId::new_random(), - MicroTari::from(1000), - None, - None, - MicroTari::from(4), - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); - let sender_tx_id = stp.get_tx_id().unwrap(); - oms.output_manager_handle - .confirm_pending_transaction(sender_tx_id) - .await - .unwrap(); + #[tokio::test] + async fn coin_split_no_change() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + + let fee_per_gram = MicroTari::from(4); + let split_count = 15; + let constants = create_consensus_constants(0); + let fee_calc = Fee::new(*constants.transaction_weight()); + let expected_fee = fee_calc.calculate( + fee_per_gram, + 1, + 3, + split_count, + split_count * default_metadata_byte_size(), + ); - drop(oms.output_manager_handle); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + let val1 = 4_000 * uT; + let val2 = 5_000 * uT; + let val3 = 6_000 * uT + expected_fee; + let (_ti, uo1) = make_input(&mut OsRng, val1, &factories.commitment, None).await; + let (_ti, uo2) = make_input(&mut OsRng, val2, &factories.commitment, None).await; + let (_ti, uo3) = make_input(&mut OsRng, val3, &factories.commitment, None).await; + assert!(oms.output_manager_handle.add_output(uo1, None).await.is_ok()); + assert!(oms.output_manager_handle.add_output(uo2, None).await.is_ok()); + assert!(oms.output_manager_handle.add_output(uo3, None).await.is_ok()); + + let (_tx_id, coin_split_tx, amount) = oms + .output_manager_handle + .create_coin_split(vec![], 1000.into(), split_count, fee_per_gram) + .await + .unwrap(); + assert_eq!(coin_split_tx.body.inputs().len(), 3); + assert_eq!(coin_split_tx.body.outputs().len(), split_count); + assert_eq!(coin_split_tx.body.get_total_fee(), expected_fee); + assert_eq!(amount, val1 + val2 + val3); + } - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!(balance.available_balance, MicroTari::from(0)); - assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); - assert_eq!(balance.pending_outgoing_balance, available_balance); -} + #[tokio::test] + async fn handle_coinbase() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + + let reward1 = MicroTari::from(1000); + let fees1 = MicroTari::from(500); + let value1 = reward1 + fees1; + let reward2 = MicroTari::from(2000); + let fees2 = MicroTari::from(500); + let value2 = reward2 + fees2; + let reward3 = MicroTari::from(3000); + let fees3 = MicroTari::from(500); + let value3 = reward3 + fees3; + + let _transaction = oms + .output_manager_handle + .get_coinbase_transaction(1u64.into(), reward1, fees1, 1) + .await + .unwrap(); + assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); + assert_eq!( + oms.output_manager_handle + .get_balance() + .await + .unwrap() + .pending_incoming_balance, + value1 + ); + let _tx2 = oms + .output_manager_handle + .get_coinbase_transaction(2u64.into(), reward2, fees2, 1) + .await + .unwrap(); + assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); + assert_eq!( + oms.output_manager_handle + .get_balance() + .await + .unwrap() + .pending_incoming_balance, + value2 + ); + let tx3 = oms + .output_manager_handle + .get_coinbase_transaction(3u64.into(), reward3, fees3, 2) + .await + .unwrap(); + assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); + assert_eq!( + oms.output_manager_handle + .get_balance() + .await + .unwrap() + .pending_incoming_balance, + value2 + value3 + ); -#[tokio::test] -async fn coin_split_with_change() { - let factories = CryptoFactories::default(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - - let val1 = 6_000 * uT; - let val2 = 7_000 * uT; - let val3 = 8_000 * uT; - let (_ti, uo1) = make_input(&mut OsRng, val1, &factories.commitment, None).await; - let (_ti, uo2) = make_input(&mut OsRng, val2, &factories.commitment, None).await; - let (_ti, uo3) = make_input(&mut OsRng, val3, &factories.commitment, None).await; - assert!(oms.output_manager_handle.add_output(uo1, None).await.is_ok()); - assert!(oms.output_manager_handle.add_output(uo2, None).await.is_ok()); - assert!(oms.output_manager_handle.add_output(uo3, None).await.is_ok()); - - let fee_per_gram = MicroTari::from(5); - let split_count = 8; - let (_tx_id, coin_split_tx, amount) = oms - .output_manager_handle - .create_coin_split(1000.into(), split_count, fee_per_gram, None) - .await - .unwrap(); - assert_eq!(coin_split_tx.body.inputs().len(), 2); - assert_eq!(coin_split_tx.body.outputs().len(), split_count + 1); - let fee_calc = Fee::new(*create_consensus_constants(0).transaction_weight()); - let expected_fee = fee_calc.calculate( - fee_per_gram, - 1, - 2, - split_count + 1, - (split_count + 1) * default_metadata_byte_size(), - ); - assert_eq!(coin_split_tx.body.get_total_fee(), expected_fee); - assert_eq!(amount, val2 + val3); -} + let output = tx3.body.outputs()[0].clone(); -#[tokio::test] -async fn coin_split_no_change() { - let factories = CryptoFactories::default(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + let rewind_public_keys = oms.output_manager_handle.get_rewind_public_keys().await.unwrap(); + let rewind_result = output + .rewind_range_proof_value_only( + &factories.range_proof, + &rewind_public_keys.rewind_public_key, + &rewind_public_keys.rewind_blinding_public_key, + ) + .unwrap(); + assert_eq!(rewind_result.committed_value, value3); + } - let fee_per_gram = MicroTari::from(4); - let split_count = 15; - let constants = create_consensus_constants(0); - let fee_calc = Fee::new(*constants.transaction_weight()); - let expected_fee = fee_calc.calculate( - fee_per_gram, - 1, - 3, - split_count, - split_count * default_metadata_byte_size(), - ); - let val1 = 4_000 * uT; - let val2 = 5_000 * uT; - let val3 = 6_000 * uT + expected_fee; - let (_ti, uo1) = make_input(&mut OsRng, val1, &factories.commitment, None).await; - let (_ti, uo2) = make_input(&mut OsRng, val2, &factories.commitment, None).await; - let (_ti, uo3) = make_input(&mut OsRng, val3, &factories.commitment, None).await; - assert!(oms.output_manager_handle.add_output(uo1, None).await.is_ok()); - assert!(oms.output_manager_handle.add_output(uo2, None).await.is_ok()); - assert!(oms.output_manager_handle.add_output(uo3, None).await.is_ok()); - - let (_tx_id, coin_split_tx, amount) = oms - .output_manager_handle - .create_coin_split(1000.into(), split_count, fee_per_gram, None) - .await - .unwrap(); - assert_eq!(coin_split_tx.body.inputs().len(), 3); - assert_eq!(coin_split_tx.body.outputs().len(), split_count); - assert_eq!(coin_split_tx.body.get_total_fee(), expected_fee); - assert_eq!(amount, val1 + val2 + val3); -} + #[tokio::test] + #[allow(clippy::too_many_lines)] + async fn test_txo_validation() { + let factories = CryptoFactories::default(); + + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let oms_db = backend.clone(); + + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + + oms.wallet_connectivity_mock.notify_base_node_set(oms.node_id.to_peer()); + // Now we add the connection + let mut connection = oms + .mock_rpc_service + .create_connection(oms.node_id.to_peer(), "t/bnwallet/1".into()) + .await; + oms.wallet_connectivity_mock + .set_base_node_wallet_rpc_client(connect_rpc_client(&mut connection).await); + + let output1_value = 1_000_000; + let (_, output1) = make_input( + &mut OsRng, + MicroTari::from(output1_value), + &factories.commitment, + Some(oms.output_manager_handle.clone()), + ) + .await; + let output1_tx_output = oms + .output_manager_handle + .convert_to_rewindable_transaction_output(output1.clone()) + .await + .unwrap(); -#[tokio::test] -async fn handle_coinbase() { - let factories = CryptoFactories::default(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - - let reward1 = MicroTari::from(1000); - let fees1 = MicroTari::from(500); - let value1 = reward1 + fees1; - let reward2 = MicroTari::from(2000); - let fees2 = MicroTari::from(500); - let value2 = reward2 + fees2; - let reward3 = MicroTari::from(3000); - let fees3 = MicroTari::from(500); - let value3 = reward3 + fees3; - - let _transaction = oms - .output_manager_handle - .get_coinbase_transaction(1u64.into(), reward1, fees1, 1) - .await - .unwrap(); - assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); - assert_eq!( oms.output_manager_handle - .get_balance() - .await - .unwrap() - .pending_incoming_balance, - value1 - ); - let _tx2 = oms - .output_manager_handle - .get_coinbase_transaction(2u64.into(), reward2, fees2, 1) - .await - .unwrap(); - assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); - assert_eq!( + .add_rewindable_output_with_tx_id(TxId::from(1u64), output1.clone(), None, None) + .await + .unwrap(); + + let output2_value = 2_000_000; + let (_, output2) = make_input( + &mut OsRng, + MicroTari::from(output2_value), + &factories.commitment, + Some(oms.output_manager_handle.clone()), + ) + .await; + let output2_tx_output = oms + .output_manager_handle + .convert_to_rewindable_transaction_output(output1.clone()) + .await + .unwrap(); + oms.output_manager_handle - .get_balance() - .await - .unwrap() - .pending_incoming_balance, - value2 - ); - let tx3 = oms - .output_manager_handle - .get_coinbase_transaction(3u64.into(), reward3, fees3, 2) - .await - .unwrap(); - assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 0); - assert_eq!( + .add_rewindable_output_with_tx_id(TxId::from(2u64), output2.clone(), None, None) + .await + .unwrap(); + + let output3_value = 4_000_000; + let (_, output3) = make_input( + &mut OsRng, + MicroTari::from(output3_value), + &factories.commitment, + Some(oms.output_manager_handle.clone()), + ) + .await; + oms.output_manager_handle - .get_balance() + .add_rewindable_output_with_tx_id(TxId::from(3u64), output3.clone(), None, None) .await - .unwrap() - .pending_incoming_balance, - value2 + value3 - ); + .unwrap(); - let output = tx3.body.outputs()[0].clone(); + let mut block1_header = BlockHeader::new(1); + block1_header.height = 1; + let mut block4_header = BlockHeader::new(1); + block4_header.height = 4; + + let mut block_headers = HashMap::new(); + block_headers.insert(1, block1_header.clone()); + block_headers.insert(4, block4_header.clone()); + oms.base_node_wallet_rpc_mock_state.set_blocks(block_headers.clone()); + + // These responses will mark outputs 1 and 2 and mined confirmed + let responses = vec![ + UtxoQueryResponse { + output: Some(output1_tx_output.clone().into()), + mmr_position: 1, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output1_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output2_tx_output.clone().into()), + mmr_position: 2, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output2_tx_output.hash(), + }, + ]; + + let utxo_query_responses = UtxoQueryResponses { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + responses, + }; + + oms.base_node_wallet_rpc_mock_state + .set_utxo_query_response(utxo_query_responses.clone()); + + // This response sets output1 as spent in the transaction that produced output4 + let query_deleted_response = QueryDeletedResponse { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + deleted_positions: vec![], + not_deleted_positions: vec![1, 2], + heights_deleted_at: vec![], + blocks_deleted_in: vec![], + }; + + oms.base_node_wallet_rpc_mock_state + .set_query_deleted_response(query_deleted_response.clone()); + oms.output_manager_handle.validate_txos().await.unwrap(); + let _utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); + let _query_deleted_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); - let rewind_public_keys = oms.output_manager_handle.get_rewind_public_keys().await.unwrap(); - let rewind_result = output - .rewind_range_proof_value_only( - &factories.range_proof, - &rewind_public_keys.rewind_public_key, - &rewind_public_keys.rewind_blinding_public_key, - ) - .unwrap(); - assert_eq!(rewind_result.committed_value, value3); -} + oms.output_manager_handle + .prepare_transaction_to_send( + 4u64.into(), + MicroTari::from(900_000), + None, + None, + MicroTari::from(10), + None, + "".to_string(), + script!(Nop), + Covenant::default(), + ) + .await + .unwrap(); -#[tokio::test] -#[allow(clippy::too_many_lines)] -async fn test_txo_validation() { - let factories = CryptoFactories::default(); + let recv_value = MicroTari::from(8_000_000); + let (_recv_tx_id, sender_message) = + generate_sender_transaction_message(recv_value, Some(oms.output_manager_handle.clone())).await; - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let oms_db = backend.clone(); + let _receiver_transaction_protocal = oms + .output_manager_handle + .get_recipient_transaction(sender_message) + .await + .unwrap(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + oms.output_manager_handle + .get_coinbase_transaction(6u64.into(), MicroTari::from(15_000_000), MicroTari::from(1_000_000), 2) + .await + .unwrap(); - oms.wallet_connectivity_mock.notify_base_node_set(oms.node_id.to_peer()); - // Now we add the connection - let mut connection = oms - .mock_rpc_service - .create_connection(oms.node_id.to_peer(), "t/bnwallet/1".into()) - .await; - oms.wallet_connectivity_mock - .set_base_node_wallet_rpc_client(connect_rpc_client(&mut connection).await); - - let output1_value = 1_000_000; - let (_, output1) = make_input( - &mut OsRng, - MicroTari::from(output1_value), - &factories.commitment, - Some(oms.output_manager_handle.clone()), - ) - .await; - let output1_tx_output = oms - .output_manager_handle - .convert_to_rewindable_transaction_output(output1.clone()) - .await - .unwrap(); + let mut outputs = oms_db.fetch_pending_incoming_outputs().unwrap(); + assert_eq!(outputs.len(), 3); - oms.output_manager_handle - .add_rewindable_output_with_tx_id(TxId::from(1u64), output1.clone(), None, None) - .await - .unwrap(); + let o5_pos = outputs + .iter() + .position(|o| o.unblinded_output.value == MicroTari::from(8_000_000)) + .unwrap(); + let output5 = outputs.remove(o5_pos); + let o6_pos = outputs + .iter() + .position(|o| o.unblinded_output.value == MicroTari::from(16_000_000)) + .unwrap(); + let output6 = outputs.remove(o6_pos); + let output4 = outputs[0].clone(); - let output2_value = 2_000_000; - let (_, output2) = make_input( - &mut OsRng, - MicroTari::from(output2_value), - &factories.commitment, - Some(oms.output_manager_handle.clone()), - ) - .await; - let output2_tx_output = oms - .output_manager_handle - .convert_to_rewindable_transaction_output(output1.clone()) - .await - .unwrap(); + let output4_tx_output = oms + .output_manager_handle + .convert_to_rewindable_transaction_output(output4.unblinded_output.clone()) + .await + .unwrap(); + let output5_tx_output = oms + .output_manager_handle + .convert_to_rewindable_transaction_output(output5.unblinded_output.clone()) + .await + .unwrap(); + let output6_tx_output = oms + .output_manager_handle + .convert_to_rewindable_transaction_output(output6.unblinded_output.clone()) + .await + .unwrap(); - oms.output_manager_handle - .add_rewindable_output_with_tx_id(TxId::from(2u64), output2.clone(), None, None) - .await - .unwrap(); + let balance = oms.output_manager_handle.get_balance().await.unwrap(); - let output3_value = 4_000_000; - let (_, output3) = make_input( - &mut OsRng, - MicroTari::from(output3_value), - &factories.commitment, - Some(oms.output_manager_handle.clone()), - ) - .await; + assert_eq!( + balance.available_balance, + MicroTari::from(output2_value) + MicroTari::from(output3_value) + ); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); + assert_eq!(balance.pending_outgoing_balance, MicroTari::from(output1_value)); + assert_eq!( + balance.pending_incoming_balance, + MicroTari::from(output1_value) - + MicroTari::from(900_000) - + MicroTari::from(1260) + //Output4 = output 1 -900_000 and 1260 for fees + MicroTari::from(8_000_000) + + MicroTari::from(16_000_000) + ); - oms.output_manager_handle - .add_rewindable_output_with_tx_id(TxId::from(3u64), output3.clone(), None, None) - .await - .unwrap(); + // Output 1: Spent in Block 5 - Unconfirmed + // Output 2: Mined block 1 Confirmed Block 4 + // Output 3: Imported so will have Unspent status. + // Output 4: Received in Block 5 - Unconfirmed - Change from spending Output 1 + // Output 5: Received in Block 5 - Unconfirmed + // Output 6: Coinbase from Block 5 - Unconfirmed + + let mut block5_header = BlockHeader::new(1); + block5_header.height = 5; + block_headers.insert(5, block5_header.clone()); + oms.base_node_wallet_rpc_mock_state.set_blocks(block_headers.clone()); + + let responses = vec![ + UtxoQueryResponse { + output: Some(output1_tx_output.clone().into()), + mmr_position: 1, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output1_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output2_tx_output.clone().into()), + mmr_position: 2, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output2_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output4_tx_output.clone().into()), + mmr_position: 4, + mined_height: 5, + mined_in_block: block5_header.hash(), + output_hash: output4_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output5_tx_output.clone().into()), + mmr_position: 5, + mined_height: 5, + mined_in_block: block5_header.hash(), + output_hash: output5_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output6_tx_output.clone().into()), + mmr_position: 6, + mined_height: 5, + mined_in_block: block5_header.hash(), + output_hash: output6_tx_output.hash(), + }, + ]; + + let mut utxo_query_responses = UtxoQueryResponses { + best_block: block5_header.hash(), + height_of_longest_chain: 5, + responses, + }; + + oms.base_node_wallet_rpc_mock_state + .set_utxo_query_response(utxo_query_responses.clone()); + + // This response sets output1 as spent in the transaction that produced output4 + let mut query_deleted_response = QueryDeletedResponse { + best_block: block5_header.hash(), + height_of_longest_chain: 5, + deleted_positions: vec![1], + not_deleted_positions: vec![2, 4, 5, 6], + heights_deleted_at: vec![5], + blocks_deleted_in: vec![block5_header.hash()], + }; + + oms.base_node_wallet_rpc_mock_state + .set_query_deleted_response(query_deleted_response.clone()); + + oms.output_manager_handle.validate_txos().await.unwrap(); + + let utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); - let mut block1_header = BlockHeader::new(1); - block1_header.height = 1; - let mut block4_header = BlockHeader::new(1); - block4_header.height = 4; - - let mut block_headers = HashMap::new(); - block_headers.insert(1, block1_header.clone()); - block_headers.insert(4, block4_header.clone()); - oms.base_node_wallet_rpc_mock_state.set_blocks(block_headers.clone()); - - // These responses will mark outputs 1 and 2 and mined confirmed - let responses = vec![ - UtxoQueryResponse { - output: Some(output1_tx_output.clone().into()), - mmr_position: 1, - mined_height: 1, - mined_in_block: block1_header.hash(), - output_hash: output1_tx_output.hash(), - }, - UtxoQueryResponse { - output: Some(output2_tx_output.clone().into()), - mmr_position: 2, - mined_height: 1, - mined_in_block: block1_header.hash(), - output_hash: output2_tx_output.hash(), - }, - ]; + assert_eq!(utxo_query_calls[0].len(), 5); - let utxo_query_responses = UtxoQueryResponses { - best_block: block4_header.hash(), - height_of_longest_chain: 4, - responses, - }; + let query_deleted_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); + assert_eq!(query_deleted_calls[0].mmr_positions.len(), 4); - oms.base_node_wallet_rpc_mock_state - .set_utxo_query_response(utxo_query_responses.clone()); - - // This response sets output1 as spent in the transaction that produced output4 - let query_deleted_response = QueryDeletedResponse { - best_block: block4_header.hash(), - height_of_longest_chain: 4, - deleted_positions: vec![], - not_deleted_positions: vec![1, 2], - heights_deleted_at: vec![], - blocks_deleted_in: vec![], - }; + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!( + balance.available_balance, + MicroTari::from(output2_value) + MicroTari::from(output3_value) + ); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); - oms.base_node_wallet_rpc_mock_state - .set_query_deleted_response(query_deleted_response.clone()); - oms.output_manager_handle.validate_txos().await.unwrap(); - let _utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); - let _query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); + assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 2); - oms.output_manager_handle - .prepare_transaction_to_send( - 4u64.into(), - MicroTari::from(900_000), - None, - None, - MicroTari::from(10), - None, - "".to_string(), - script!(Nop), - Covenant::default(), - ) - .await - .unwrap(); + assert!(oms.output_manager_handle.get_spent_outputs().await.unwrap().is_empty()); - let recv_value = MicroTari::from(8_000_000); - let (_recv_tx_id, sender_message) = - generate_sender_transaction_message(recv_value, Some(oms.output_manager_handle.clone())).await; + // Now we will update the mined_height in the responses so that the outputs are confirmed + // Output 1: Spent in Block 5 - Confirmed + // Output 2: Mined block 1 Confirmed Block 4 + // Output 3: Imported so will have Unspent status + // Output 4: Received in Block 5 - Confirmed - Change from spending Output 1 + // Output 5: Received in Block 5 - Confirmed + // Output 6: Coinbase from Block 5 - Confirmed - let _receiver_transaction_protocal = oms - .output_manager_handle - .get_recipient_transaction(sender_message) - .await - .unwrap(); + utxo_query_responses.height_of_longest_chain = 8; + utxo_query_responses.best_block = [8u8; 16].to_vec(); + oms.base_node_wallet_rpc_mock_state + .set_utxo_query_response(utxo_query_responses); - oms.output_manager_handle - .get_coinbase_transaction(6u64.into(), MicroTari::from(15_000_000), MicroTari::from(1_000_000), 2) - .await - .unwrap(); + query_deleted_response.height_of_longest_chain = 8; + query_deleted_response.best_block = [8u8; 16].to_vec(); + oms.base_node_wallet_rpc_mock_state + .set_query_deleted_response(query_deleted_response); - let mut outputs = oms_db.fetch_pending_incoming_outputs().unwrap(); - assert_eq!(outputs.len(), 3); + oms.output_manager_handle.validate_txos().await.unwrap(); - let o5_pos = outputs - .iter() - .position(|o| o.unblinded_output.value == MicroTari::from(8_000_000)) - .unwrap(); - let output5 = outputs.remove(o5_pos); - let o6_pos = outputs - .iter() - .position(|o| o.unblinded_output.value == MicroTari::from(16_000_000)) - .unwrap(); - let output6 = outputs.remove(o6_pos); - let output4 = outputs[0].clone(); + let utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); - let output4_tx_output = oms - .output_manager_handle - .convert_to_rewindable_transaction_output(output4.unblinded_output.clone()) - .await - .unwrap(); - let output5_tx_output = oms - .output_manager_handle - .convert_to_rewindable_transaction_output(output5.unblinded_output.clone()) - .await - .unwrap(); - let output6_tx_output = oms - .output_manager_handle - .convert_to_rewindable_transaction_output(output6.unblinded_output.clone()) - .await - .unwrap(); + // The spent transaction is not checked during this second validation + assert_eq!(utxo_query_calls[0].len(), 5); - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - - assert_eq!( - balance.available_balance, - MicroTari::from(output2_value) + MicroTari::from(output3_value) - ); - assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); - assert_eq!(balance.pending_outgoing_balance, MicroTari::from(output1_value)); - assert_eq!( - balance.pending_incoming_balance, - MicroTari::from(output1_value) - - MicroTari::from(900_000) - - MicroTari::from(1260) + //Output4 = output 1 -900_000 and 1260 for fees - MicroTari::from(8_000_000) + - MicroTari::from(16_000_000) - ); - - // Output 1: Spent in Block 5 - Unconfirmed - // Output 2: Mined block 1 Confirmed Block 4 - // Output 3: Imported so will have Unspent status. - // Output 4: Received in Block 5 - Unconfirmed - Change from spending Output 1 - // Output 5: Received in Block 5 - Unconfirmed - // Output 6: Coinbase from Block 5 - Unconfirmed - - let mut block5_header = BlockHeader::new(1); - block5_header.height = 5; - block_headers.insert(5, block5_header.clone()); - oms.base_node_wallet_rpc_mock_state.set_blocks(block_headers.clone()); - - let responses = vec![ - UtxoQueryResponse { - output: Some(output1_tx_output.clone().into()), - mmr_position: 1, - mined_height: 1, - mined_in_block: block1_header.hash(), - output_hash: output1_tx_output.hash(), - }, - UtxoQueryResponse { - output: Some(output2_tx_output.clone().into()), - mmr_position: 2, - mined_height: 1, - mined_in_block: block1_header.hash(), - output_hash: output2_tx_output.hash(), - }, - UtxoQueryResponse { - output: Some(output4_tx_output.clone().into()), - mmr_position: 4, - mined_height: 5, - mined_in_block: block5_header.hash(), - output_hash: output4_tx_output.hash(), - }, - UtxoQueryResponse { - output: Some(output5_tx_output.clone().into()), - mmr_position: 5, - mined_height: 5, - mined_in_block: block5_header.hash(), - output_hash: output5_tx_output.hash(), - }, - UtxoQueryResponse { - output: Some(output6_tx_output.clone().into()), - mmr_position: 6, - mined_height: 5, - mined_in_block: block5_header.hash(), - output_hash: output6_tx_output.hash(), - }, - ]; + let query_deleted_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); + assert_eq!(query_deleted_calls[0].mmr_positions.len(), 4); - let mut utxo_query_responses = UtxoQueryResponses { - best_block: block5_header.hash(), - height_of_longest_chain: 5, - responses, - }; + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!( + balance.available_balance, + MicroTari::from(output2_value) + MicroTari::from(output3_value) + MicroTari::from(output1_value) - + MicroTari::from(900_000) - + MicroTari::from(1260) + //spent 900_000 and 1260 for fees + MicroTari::from(8_000_000) + //output 5 + MicroTari::from(16_000_000) // output 6 + ); + assert_eq!(balance.pending_outgoing_balance, MicroTari::from(1000000)); + assert_eq!(balance.pending_incoming_balance, MicroTari::from(0)); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); - oms.base_node_wallet_rpc_mock_state - .set_utxo_query_response(utxo_query_responses.clone()); - - // This response sets output1 as spent in the transaction that produced output4 - let mut query_deleted_response = QueryDeletedResponse { - best_block: block5_header.hash(), - height_of_longest_chain: 5, - deleted_positions: vec![1], - not_deleted_positions: vec![2, 4, 5, 6], - heights_deleted_at: vec![5], - blocks_deleted_in: vec![block5_header.hash()], - }; + // Trigger another validation and only Output3 should be checked + oms.output_manager_handle.validate_txos().await.unwrap(); - oms.base_node_wallet_rpc_mock_state - .set_query_deleted_response(query_deleted_response.clone()); + let utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); + assert_eq!(utxo_query_calls.len(), 1); + assert_eq!(utxo_query_calls[0].len(), 2); + assert_eq!( + utxo_query_calls[0][1], + oms.output_manager_handle + .convert_to_rewindable_transaction_output(output3.clone()) + .await + .unwrap() + .hash() + ); - oms.output_manager_handle.validate_txos().await.unwrap(); + // Now we will create responses that result in a reorg of block 5, keeping block4 the same. + // Output 1: Spent in Block 5 - Unconfirmed + // Output 2: Mined block 1 Confirmed Block 4 + // Output 3: Imported so will have Unspent + // Output 4: Received in Block 5 - Unconfirmed - Change from spending Output 1 + // Output 5: Reorged out + // Output 6: Reorged out + let block5_header_reorg = BlockHeader::new(2); + block5_header.height = 5; + let mut block_headers = HashMap::new(); + block_headers.insert(1, block1_header.clone()); + block_headers.insert(4, block4_header.clone()); + block_headers.insert(5, block5_header_reorg.clone()); + oms.base_node_wallet_rpc_mock_state.set_blocks(block_headers.clone()); + + // Update UtxoResponses to not have the received output5 and coinbase output6 + let responses = vec![ + UtxoQueryResponse { + output: Some(output1_tx_output.clone().into()), + mmr_position: 1, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output1_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output2_tx_output.clone().into()), + mmr_position: 2, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output2_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output4_tx_output.clone().into()), + mmr_position: 4, + mined_height: 5, + mined_in_block: block5_header_reorg.hash(), + output_hash: output4_tx_output.hash(), + }, + ]; + + let mut utxo_query_responses = UtxoQueryResponses { + best_block: block5_header_reorg.hash(), + height_of_longest_chain: 5, + responses, + }; + + oms.base_node_wallet_rpc_mock_state + .set_utxo_query_response(utxo_query_responses.clone()); + + // This response sets output1 as spent in the transaction that produced output4 + let mut query_deleted_response = QueryDeletedResponse { + best_block: block5_header_reorg.hash(), + height_of_longest_chain: 5, + deleted_positions: vec![1], + not_deleted_positions: vec![2, 4, 5, 6], + heights_deleted_at: vec![5], + blocks_deleted_in: vec![block5_header_reorg.hash()], + }; + + oms.base_node_wallet_rpc_mock_state + .set_query_deleted_response(query_deleted_response.clone()); + + // Trigger validation through a base_node_service event + oms.node_event + .send(Arc::new(BaseNodeEvent::BaseNodeStateChanged(BaseNodeState::default()))) + .unwrap(); - let utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); + let _result = oms + .base_node_wallet_rpc_mock_state + .wait_pop_get_header_by_height_calls(2, Duration::from_secs(60)) + .await + .unwrap(); - assert_eq!(utxo_query_calls[0].len(), 5); + let _utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); - let query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); - assert_eq!(query_deleted_calls[0].mmr_positions.len(), 4); - - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!( - balance.available_balance, - MicroTari::from(output2_value) + MicroTari::from(output3_value) - ); - assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); - - assert_eq!(oms.output_manager_handle.get_unspent_outputs().await.unwrap().len(), 2); - - assert!(oms.output_manager_handle.get_spent_outputs().await.unwrap().is_empty()); - - // Now we will update the mined_height in the responses so that the outputs are confirmed - // Output 1: Spent in Block 5 - Confirmed - // Output 2: Mined block 1 Confirmed Block 4 - // Output 3: Imported so will have Unspent status - // Output 4: Received in Block 5 - Confirmed - Change from spending Output 1 - // Output 5: Received in Block 5 - Confirmed - // Output 6: Coinbase from Block 5 - Confirmed - - utxo_query_responses.height_of_longest_chain = 8; - utxo_query_responses.best_block = [8u8; 16].to_vec(); - oms.base_node_wallet_rpc_mock_state - .set_utxo_query_response(utxo_query_responses); - - query_deleted_response.height_of_longest_chain = 8; - query_deleted_response.best_block = [8u8; 16].to_vec(); - oms.base_node_wallet_rpc_mock_state - .set_query_deleted_response(query_deleted_response); - - oms.output_manager_handle.validate_txos().await.unwrap(); - - let utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); + let _query_deleted_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); - // The spent transaction is not checked during this second validation - assert_eq!(utxo_query_calls[0].len(), 5); + // This is needed on a fast computer, otherwise the balance have not been updated correctly yet with the next + // step + let mut event_stream = oms.output_manager_handle.get_event_stream(); + let delay = sleep(Duration::from_secs(10)); + tokio::pin!(delay); + loop { + tokio::select! { + event = event_stream.recv() => { + if let OutputManagerEvent::TxoValidationSuccess(_) = &*event.unwrap(){ + break; + } + }, + () = &mut delay => { + break; + }, + } + } - let query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); - assert_eq!(query_deleted_calls[0].mmr_positions.len(), 4); - - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!( - balance.available_balance, - MicroTari::from(output2_value) + MicroTari::from(output3_value) + MicroTari::from(output1_value) - - MicroTari::from(900_000) - - MicroTari::from(1260) + //spent 900_000 and 1260 for fees - MicroTari::from(8_000_000) + //output 5 - MicroTari::from(16_000_000) // output 6 - ); - assert_eq!(balance.pending_outgoing_balance, MicroTari::from(1000000)); - assert_eq!(balance.pending_incoming_balance, MicroTari::from(0)); - assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); - - // Trigger another validation and only Output3 should be checked - oms.output_manager_handle.validate_txos().await.unwrap(); - - let utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); - assert_eq!(utxo_query_calls.len(), 1); - assert_eq!(utxo_query_calls[0].len(), 2); - assert_eq!( - utxo_query_calls[0][1], - oms.output_manager_handle - .convert_to_rewindable_transaction_output(output3.clone()) - .await - .unwrap() - .hash() - ); - - // Now we will create responses that result in a reorg of block 5, keeping block4 the same. - // Output 1: Spent in Block 5 - Unconfirmed - // Output 2: Mined block 1 Confirmed Block 4 - // Output 3: Imported so will have Unspent - // Output 4: Received in Block 5 - Unconfirmed - Change from spending Output 1 - // Output 5: Reorged out - // Output 6: Reorged out - let block5_header_reorg = BlockHeader::new(2); - block5_header.height = 5; - let mut block_headers = HashMap::new(); - block_headers.insert(1, block1_header.clone()); - block_headers.insert(4, block4_header.clone()); - block_headers.insert(5, block5_header_reorg.clone()); - oms.base_node_wallet_rpc_mock_state.set_blocks(block_headers.clone()); - - // Update UtxoResponses to not have the received output5 and coinbase output6 - let responses = vec![ - UtxoQueryResponse { - output: Some(output1_tx_output.clone().into()), - mmr_position: 1, - mined_height: 1, - mined_in_block: block1_header.hash(), - output_hash: output1_tx_output.hash(), - }, - UtxoQueryResponse { - output: Some(output2_tx_output.clone().into()), - mmr_position: 2, - mined_height: 1, - mined_in_block: block1_header.hash(), - output_hash: output2_tx_output.hash(), - }, - UtxoQueryResponse { - output: Some(output4_tx_output.clone().into()), - mmr_position: 4, - mined_height: 5, - mined_in_block: block5_header_reorg.hash(), - output_hash: output4_tx_output.hash(), - }, - ]; + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!( + balance.available_balance, + MicroTari::from(output2_value) + MicroTari::from(output3_value) + ); + assert_eq!(balance.pending_outgoing_balance, MicroTari::from(output1_value)); + assert_eq!( + balance.pending_incoming_balance, + MicroTari::from(output1_value) - MicroTari::from(901_260) + ); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); - let mut utxo_query_responses = UtxoQueryResponses { - best_block: block5_header_reorg.hash(), - height_of_longest_chain: 5, - responses, - }; + // Now we will update the mined_height in the responses so that the outputs on the reorged chain are confirmed + // Output 1: Spent in Block 5 - Confirmed + // Output 2: Mined block 1 Confirmed Block 4 + // Output 3: Imported so will have Unspent + // Output 4: Received in Block 5 - Confirmed - Change from spending Output 1 + // Output 5: Reorged out + // Output 6: Reorged out - oms.base_node_wallet_rpc_mock_state - .set_utxo_query_response(utxo_query_responses.clone()); - - // This response sets output1 as spent in the transaction that produced output4 - let mut query_deleted_response = QueryDeletedResponse { - best_block: block5_header_reorg.hash(), - height_of_longest_chain: 5, - deleted_positions: vec![1], - not_deleted_positions: vec![2, 4, 5, 6], - heights_deleted_at: vec![5], - blocks_deleted_in: vec![block5_header_reorg.hash()], - }; + utxo_query_responses.height_of_longest_chain = 8; + utxo_query_responses.best_block = [8u8; 16].to_vec(); + oms.base_node_wallet_rpc_mock_state + .set_utxo_query_response(utxo_query_responses); - oms.base_node_wallet_rpc_mock_state - .set_query_deleted_response(query_deleted_response.clone()); + query_deleted_response.height_of_longest_chain = 8; + query_deleted_response.best_block = [8u8; 16].to_vec(); + oms.base_node_wallet_rpc_mock_state + .set_query_deleted_response(query_deleted_response); - // Trigger validation through a base_node_service event - oms.node_event - .send(Arc::new(BaseNodeEvent::BaseNodeStateChanged(BaseNodeState::default()))) - .unwrap(); + let mut event_stream = oms.output_manager_handle.get_event_stream(); - let _result = oms - .base_node_wallet_rpc_mock_state - .wait_pop_get_header_by_height_calls(2, Duration::from_secs(60)) - .await - .unwrap(); + let validation_id = oms.output_manager_handle.validate_txos().await.unwrap(); - let _utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); + let _utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); - let _query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); + let _query_deleted_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); - // This is needed on a fast computer, otherwise the balance have not been updated correctly yet with the next step - let mut event_stream = oms.output_manager_handle.get_event_stream(); - let delay = sleep(Duration::from_secs(10)); - tokio::pin!(delay); - loop { - tokio::select! { - event = event_stream.recv() => { - if let OutputManagerEvent::TxoValidationSuccess(_) = &*event.unwrap(){ + let delay = sleep(Duration::from_secs(30)); + tokio::pin!(delay); + let mut validation_completed = false; + loop { + tokio::select! { + event = event_stream.recv() => { + if let OutputManagerEvent::TxoValidationSuccess(id) = &*event.unwrap(){ + if id == &validation_id { + validation_completed = true; + break; + } + } + }, + () = &mut delay => { break; - } - }, - () = &mut delay => { - break; - }, + }, + } } - } - - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!( - balance.available_balance, - MicroTari::from(output2_value) + MicroTari::from(output3_value) - ); - assert_eq!(balance.pending_outgoing_balance, MicroTari::from(output1_value)); - assert_eq!( - balance.pending_incoming_balance, - MicroTari::from(output1_value) - MicroTari::from(901_260) - ); - assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); - - // Now we will update the mined_height in the responses so that the outputs on the reorged chain are confirmed - // Output 1: Spent in Block 5 - Confirmed - // Output 2: Mined block 1 Confirmed Block 4 - // Output 3: Imported so will have Unspent - // Output 4: Received in Block 5 - Confirmed - Change from spending Output 1 - // Output 5: Reorged out - // Output 6: Reorged out - - utxo_query_responses.height_of_longest_chain = 8; - utxo_query_responses.best_block = [8u8; 16].to_vec(); - oms.base_node_wallet_rpc_mock_state - .set_utxo_query_response(utxo_query_responses); - - query_deleted_response.height_of_longest_chain = 8; - query_deleted_response.best_block = [8u8; 16].to_vec(); - oms.base_node_wallet_rpc_mock_state - .set_query_deleted_response(query_deleted_response); - - let mut event_stream = oms.output_manager_handle.get_event_stream(); - - let validation_id = oms.output_manager_handle.validate_txos().await.unwrap(); - - let _utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); - - let _query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); + assert!(validation_completed, "Validation protocol should complete"); - let delay = sleep(Duration::from_secs(30)); - tokio::pin!(delay); - let mut validation_completed = false; - loop { - tokio::select! { - event = event_stream.recv() => { - if let OutputManagerEvent::TxoValidationSuccess(id) = &*event.unwrap(){ - if id == &validation_id { - validation_completed = true; - break; - } - } - }, - () = &mut delay => { - break; - }, - } + let balance = oms.output_manager_handle.get_balance().await.unwrap(); + assert_eq!( + balance.available_balance, + MicroTari::from(output2_value) + MicroTari::from(output3_value) + MicroTari::from(output1_value) - + MicroTari::from(901_260) + ); + assert_eq!(balance.pending_outgoing_balance, MicroTari::from(1000000)); + assert_eq!(balance.pending_incoming_balance, MicroTari::from(0)); + assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); } - assert!(validation_completed, "Validation protocol should complete"); - - let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!( - balance.available_balance, - MicroTari::from(output2_value) + MicroTari::from(output3_value) + MicroTari::from(output1_value) - - MicroTari::from(901_260) - ); - assert_eq!(balance.pending_outgoing_balance, MicroTari::from(1000000)); - assert_eq!(balance.pending_incoming_balance, MicroTari::from(0)); - assert_eq!(MicroTari::from(0), balance.time_locked_balance.unwrap()); -} - -#[tokio::test] -#[allow(clippy::too_many_lines)] -async fn test_txo_revalidation() { - let factories = CryptoFactories::default(); - - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + #[tokio::test] + #[allow(clippy::too_many_lines)] + async fn test_txo_revalidation() { + let factories = CryptoFactories::default(); - oms.wallet_connectivity_mock.notify_base_node_set(oms.node_id.to_peer()); - // Now we add the connection - let mut connection = oms - .mock_rpc_service - .create_connection(oms.node_id.to_peer(), "t/bnwallet/1".into()) - .await; - oms.wallet_connectivity_mock - .set_base_node_wallet_rpc_client(connect_rpc_client(&mut connection).await); - - let output1_value = 1_000_000; - let output1 = create_unblinded_output( - script!(Nop), - OutputFeatures::default(), - &TestParamsHelpers::new(), - MicroTari::from(output1_value), - ); - let output1_tx_output = output1.as_transaction_output(&factories).unwrap(); - oms.output_manager_handle - .add_output_with_tx_id(TxId::from(1u64), output1.clone(), None) - .await - .unwrap(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let output2_value = 2_000_000; - let output2 = create_unblinded_output( - script!(Nop), - OutputFeatures::default(), - &TestParamsHelpers::new(), - MicroTari::from(output2_value), - ); - let output2_tx_output = output2.as_transaction_output(&factories).unwrap(); - - oms.output_manager_handle - .add_output_with_tx_id(TxId::from(2u64), output2.clone(), None) - .await - .unwrap(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - let mut block1_header = BlockHeader::new(1); - block1_header.height = 1; - let mut block4_header = BlockHeader::new(1); - block4_header.height = 4; - - let mut block_headers = HashMap::new(); - block_headers.insert(1, block1_header.clone()); - block_headers.insert(4, block4_header.clone()); - oms.base_node_wallet_rpc_mock_state.set_blocks(block_headers.clone()); - - // These responses will mark outputs 1 and 2 and mined confirmed - let responses = vec![ - UtxoQueryResponse { - output: Some(output1_tx_output.clone().into()), - mmr_position: 1, - mined_height: 1, - mined_in_block: block1_header.hash(), - output_hash: output1_tx_output.hash(), - }, - UtxoQueryResponse { - output: Some(output2_tx_output.clone().into()), - mmr_position: 2, - mined_height: 1, - mined_in_block: block1_header.hash(), - output_hash: output2_tx_output.hash(), - }, - ]; + oms.wallet_connectivity_mock.notify_base_node_set(oms.node_id.to_peer()); + // Now we add the connection + let mut connection = oms + .mock_rpc_service + .create_connection(oms.node_id.to_peer(), "t/bnwallet/1".into()) + .await; + oms.wallet_connectivity_mock + .set_base_node_wallet_rpc_client(connect_rpc_client(&mut connection).await); - let utxo_query_responses = UtxoQueryResponses { - best_block: block4_header.hash(), - height_of_longest_chain: 4, - responses, - }; + let output1_value = 1_000_000; + let output1 = create_unblinded_output( + script!(Nop), + OutputFeatures::default(), + &TestParamsHelpers::new(), + MicroTari::from(output1_value), + ); + let output1_tx_output = output1.as_transaction_output(&factories).unwrap(); + oms.output_manager_handle + .add_output_with_tx_id(TxId::from(1u64), output1.clone(), None) + .await + .unwrap(); - oms.base_node_wallet_rpc_mock_state - .set_utxo_query_response(utxo_query_responses.clone()); - - // This response sets output1 as spent - let query_deleted_response = QueryDeletedResponse { - best_block: block4_header.hash(), - height_of_longest_chain: 4, - deleted_positions: vec![], - not_deleted_positions: vec![1, 2], - heights_deleted_at: vec![], - blocks_deleted_in: vec![], - }; + let output2_value = 2_000_000; + let output2 = create_unblinded_output( + script!(Nop), + OutputFeatures::default(), + &TestParamsHelpers::new(), + MicroTari::from(output2_value), + ); + let output2_tx_output = output2.as_transaction_output(&factories).unwrap(); - oms.base_node_wallet_rpc_mock_state - .set_query_deleted_response(query_deleted_response.clone()); - oms.output_manager_handle.validate_txos().await.unwrap(); - let _utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); - let _query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); + oms.output_manager_handle + .add_output_with_tx_id(TxId::from(2u64), output2.clone(), None) + .await + .unwrap(); - let unspent_txos = oms.output_manager_handle.get_unspent_outputs().await.unwrap(); - assert_eq!(unspent_txos.len(), 2); - - // This response sets output1 as spent - let query_deleted_response = QueryDeletedResponse { - best_block: block4_header.hash(), - height_of_longest_chain: 4, - deleted_positions: vec![1], - not_deleted_positions: vec![2], - heights_deleted_at: vec![4], - blocks_deleted_in: vec![block4_header.hash()], - }; + let mut block1_header = BlockHeader::new(1); + block1_header.height = 1; + let mut block4_header = BlockHeader::new(1); + block4_header.height = 4; + + let mut block_headers = HashMap::new(); + block_headers.insert(1, block1_header.clone()); + block_headers.insert(4, block4_header.clone()); + oms.base_node_wallet_rpc_mock_state.set_blocks(block_headers.clone()); + + // These responses will mark outputs 1 and 2 and mined confirmed + let responses = vec![ + UtxoQueryResponse { + output: Some(output1_tx_output.clone().into()), + mmr_position: 1, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output1_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output2_tx_output.clone().into()), + mmr_position: 2, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output2_tx_output.hash(), + }, + ]; + + let utxo_query_responses = UtxoQueryResponses { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + responses, + }; + + oms.base_node_wallet_rpc_mock_state + .set_utxo_query_response(utxo_query_responses.clone()); + + // This response sets output1 as spent + let query_deleted_response = QueryDeletedResponse { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + deleted_positions: vec![], + not_deleted_positions: vec![1, 2], + heights_deleted_at: vec![], + blocks_deleted_in: vec![], + }; + + oms.base_node_wallet_rpc_mock_state + .set_query_deleted_response(query_deleted_response.clone()); + oms.output_manager_handle.validate_txos().await.unwrap(); + let _utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); + let _query_deleted_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); - oms.base_node_wallet_rpc_mock_state - .set_query_deleted_response(query_deleted_response.clone()); - oms.output_manager_handle.revalidate_all_outputs().await.unwrap(); - let _utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); - let _query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); + let unspent_txos = oms.output_manager_handle.get_unspent_outputs().await.unwrap(); + assert_eq!(unspent_txos.len(), 2); + + // This response sets output1 as spent + let query_deleted_response = QueryDeletedResponse { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + deleted_positions: vec![1], + not_deleted_positions: vec![2], + heights_deleted_at: vec![4], + blocks_deleted_in: vec![block4_header.hash()], + }; + + oms.base_node_wallet_rpc_mock_state + .set_query_deleted_response(query_deleted_response.clone()); + oms.output_manager_handle.revalidate_all_outputs().await.unwrap(); + let _utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); + let _query_deleted_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); - let unspent_txos = oms.output_manager_handle.get_unspent_outputs().await.unwrap(); - assert_eq!(unspent_txos.len(), 1); - - // This response sets output1 and 2 as spent - let query_deleted_response = QueryDeletedResponse { - best_block: block4_header.hash(), - height_of_longest_chain: 4, - deleted_positions: vec![1, 2], - not_deleted_positions: vec![], - heights_deleted_at: vec![4, 4], - blocks_deleted_in: vec![block4_header.hash(), block4_header.hash()], - }; + let unspent_txos = oms.output_manager_handle.get_unspent_outputs().await.unwrap(); + assert_eq!(unspent_txos.len(), 1); + + // This response sets output1 and 2 as spent + let query_deleted_response = QueryDeletedResponse { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + deleted_positions: vec![1, 2], + not_deleted_positions: vec![], + heights_deleted_at: vec![4, 4], + blocks_deleted_in: vec![block4_header.hash(), block4_header.hash()], + }; + + oms.base_node_wallet_rpc_mock_state + .set_query_deleted_response(query_deleted_response.clone()); + oms.output_manager_handle.revalidate_all_outputs().await.unwrap(); + let _utxo_query_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); + let _query_deleted_calls = oms + .base_node_wallet_rpc_mock_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); - oms.base_node_wallet_rpc_mock_state - .set_query_deleted_response(query_deleted_response.clone()); - oms.output_manager_handle.revalidate_all_outputs().await.unwrap(); - let _utxo_query_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) - .await - .unwrap(); - let _query_deleted_calls = oms - .base_node_wallet_rpc_mock_state - .wait_pop_query_deleted(1, Duration::from_secs(60)) - .await - .unwrap(); + let unspent_txos = oms.output_manager_handle.get_unspent_outputs().await.unwrap(); + assert_eq!(unspent_txos.len(), 0); + } - let unspent_txos = oms.output_manager_handle.get_unspent_outputs().await.unwrap(); - assert_eq!(unspent_txos.len(), 0); -} + #[tokio::test] + async fn test_get_status_by_tx_id() { + let factories = CryptoFactories::default(); -#[tokio::test] -async fn test_get_status_by_tx_id() { - let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let mut oms = setup_output_manager_service(backend, ks_backend, true).await; - let mut oms = setup_output_manager_service(backend, ks_backend, true).await; + let (_ti, uo1) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment, None).await; + oms.output_manager_handle + .add_unvalidated_output(TxId::from(1u64), uo1, None) + .await + .unwrap(); - let (_ti, uo1) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment, None).await; - oms.output_manager_handle - .add_unvalidated_output(TxId::from(1u64), uo1, None) - .await - .unwrap(); + let (_ti, uo2) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment, None).await; + oms.output_manager_handle + .add_unvalidated_output(TxId::from(2u64), uo2, None) + .await + .unwrap(); - let (_ti, uo2) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment, None).await; - oms.output_manager_handle - .add_unvalidated_output(TxId::from(2u64), uo2, None) - .await - .unwrap(); + let output_statuses_by_tx_id = oms + .output_manager_handle + .get_output_statuses_by_tx_id(TxId::from(1u64)) + .await + .unwrap(); - let output_statuses_by_tx_id = oms - .output_manager_handle - .get_output_statuses_by_tx_id(TxId::from(1u64)) - .await - .unwrap(); + assert_eq!(output_statuses_by_tx_id.statuses.len(), 1); + assert_eq!( + output_statuses_by_tx_id.statuses[0], + OutputStatus::EncumberedToBeReceived + ); + } - assert_eq!(output_statuses_by_tx_id.statuses.len(), 1); - assert_eq!( - output_statuses_by_tx_id.statuses[0], - OutputStatus::EncumberedToBeReceived - ); -} + #[tokio::test] + async fn scan_for_recovery_test() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let mut oms = setup_output_manager_service(backend.clone(), ks_backend, true).await; + + const NUM_REWINDABLE: usize = 5; + const NUM_NON_REWINDABLE: usize = 3; + + let mut rewindable_unblinded_outputs = Vec::new(); + + for i in 1..=NUM_REWINDABLE { + let spending_key_result = oms + .key_manager_handler + .get_next_key(OutputManagerKeyManagerBranch::Spend.get_branch_key()) + .await + .unwrap(); + let script_key = oms + .key_manager_handler + .get_key_at_index( + OutputManagerKeyManagerBranch::SpendScript.get_branch_key(), + spending_key_result.index, + ) + .await + .unwrap(); + let commitment = factories + .commitment + .commit_value(&spending_key_result.key, 1000 * i as u64); + let mut features = OutputFeatures::default(); + features.update_recovery_byte(&commitment, Some(&oms.rewind_data)); + let uo = UnblindedOutput::new_current_version( + MicroTari::from(1000 * i as u64), + spending_key_result.key, + features, + script!(Nop), + inputs!(PublicKey::from_secret_key(&script_key)), + script_key, + PublicKey::default(), + ComSignature::default(), + 0, + Covenant::new(), + ); + rewindable_unblinded_outputs.push(uo); + } -#[tokio::test] -async fn scan_for_recovery_test() { - let factories = CryptoFactories::default(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let mut oms = setup_output_manager_service(backend.clone(), ks_backend, true).await; + let mut non_rewindable_unblinded_outputs = Vec::new(); - const NUM_REWINDABLE: usize = 5; - const NUM_NON_REWINDABLE: usize = 3; + for i in 1..=NUM_NON_REWINDABLE { + let (_ti, uo) = make_input( + &mut OsRng, + MicroTari::from(1000 * i as u64), + &factories.commitment, + Some(oms.output_manager_handle.clone()), + ) + .await; + non_rewindable_unblinded_outputs.push(uo) + } - let mut rewindable_unblinded_outputs = Vec::new(); + let rewindable_outputs: Vec = rewindable_unblinded_outputs + .clone() + .into_iter() + .map(|uo| { + uo.as_rewindable_transaction_output(&factories, &oms.rewind_data, None) + .unwrap() + }) + .collect(); - for i in 1..=NUM_REWINDABLE { - let spending_key_result = oms + let recovery_byte_key = oms .key_manager_handler - .get_next_key(OutputManagerKeyManagerBranch::Spend.get_branch_key()) + .get_key_at_index(OutputManagerKeyManagerBranch::RecoveryByte.get_branch_key(), 0) .await .unwrap(); - let script_key = oms - .key_manager_handler - .get_key_at_index( - OutputManagerKeyManagerBranch::SpendScript.get_branch_key(), - spending_key_result.index, + let other_rewind_data = RewindData { + rewind_key: PrivateKey::random(&mut OsRng), + rewind_blinding_key: PrivateKey::random(&mut OsRng), + recovery_byte_key, + proof_message: [0u8; REWIND_USER_MESSAGE_LENGTH], + }; + + let non_rewindable_outputs: Vec = non_rewindable_unblinded_outputs + .clone() + .into_iter() + .map(|uo| { + uo.as_rewindable_transaction_output(&factories, &other_rewind_data, None) + .unwrap() + }) + .collect(); + + oms.output_manager_handle + .add_rewindable_output(rewindable_unblinded_outputs[0].clone(), None, None) + .await + .unwrap(); + + let recovered_outputs = oms + .output_manager_handle + .scan_for_recoverable_outputs( + rewindable_outputs + .clone() + .into_iter() + .chain(non_rewindable_outputs.clone().into_iter()) + .collect::>(), ) .await .unwrap(); - let commitment = factories - .commitment - .commit_value(&spending_key_result.key, 1000 * i as u64); - let mut features = OutputFeatures::default(); - features.update_recovery_byte(&commitment, Some(&oms.rewind_data)); - let uo = UnblindedOutput::new_current_version( - MicroTari::from(1000 * i as u64), - spending_key_result.key, - features, - script!(Nop), - inputs!(PublicKey::from_secret_key(&script_key)), - script_key, - PublicKey::default(), - ComSignature::default(), - 0, - Covenant::new(), - ); - rewindable_unblinded_outputs.push(uo); + + // Check that the non-rewindable outputs are not preset, also check that one rewindable output that was already + // contained in the OMS database is also not included in the returns outputs. + + assert_eq!(recovered_outputs.len(), NUM_REWINDABLE - 1); + for o in rewindable_unblinded_outputs.iter().skip(1) { + assert!(recovered_outputs + .iter() + .any(|ro| ro.output.spending_key == o.spending_key)); + } } - let mut non_rewindable_unblinded_outputs = Vec::new(); + #[tokio::test] + async fn recovered_output_key_not_in_keychain() { + let factories = CryptoFactories::default(); + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); + let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); + let mut oms = setup_output_manager_service(backend.clone(), ks_backend, true).await; - for i in 1..=NUM_NON_REWINDABLE { let (_ti, uo) = make_input( &mut OsRng, - MicroTari::from(1000 * i as u64), + MicroTari::from(1000u64), &factories.commitment, Some(oms.output_manager_handle.clone()), ) .await; - non_rewindable_unblinded_outputs.push(uo) - } - - let rewindable_outputs: Vec = rewindable_unblinded_outputs - .clone() - .into_iter() - .map(|uo| { - uo.as_rewindable_transaction_output(&factories, &oms.rewind_data, None) - .unwrap() - }) - .collect(); - - let recovery_byte_key = oms - .key_manager_handler - .get_key_at_index(OutputManagerKeyManagerBranch::RecoveryByte.get_branch_key(), 0) - .await - .unwrap(); - let other_rewind_data = RewindData { - rewind_key: PrivateKey::random(&mut OsRng), - rewind_blinding_key: PrivateKey::random(&mut OsRng), - recovery_byte_key, - proof_message: [0u8; REWIND_USER_MESSAGE_LENGTH], - }; - - let non_rewindable_outputs: Vec = non_rewindable_unblinded_outputs - .clone() - .into_iter() - .map(|uo| { - uo.as_rewindable_transaction_output(&factories, &other_rewind_data, None) - .unwrap() - }) - .collect(); - - oms.output_manager_handle - .add_rewindable_output(rewindable_unblinded_outputs[0].clone(), None, None) - .await - .unwrap(); - let recovered_outputs = oms - .output_manager_handle - .scan_for_recoverable_outputs( - rewindable_outputs - .clone() - .into_iter() - .chain(non_rewindable_outputs.clone().into_iter()) - .collect::>(), - ) - .await - .unwrap(); + let rewindable_output = uo + .as_rewindable_transaction_output(&factories, &oms.rewind_data, None) + .unwrap(); - // Check that the non-rewindable outputs are not preset, also check that one rewindable output that was already - // contained in the OMS database is also not included in the returns outputs. + let result = oms + .output_manager_handle + .scan_for_recoverable_outputs(vec![rewindable_output]) + .await; - assert_eq!(recovered_outputs.len(), NUM_REWINDABLE - 1); - for o in rewindable_unblinded_outputs.iter().skip(1) { - assert!(recovered_outputs - .iter() - .any(|ro| ro.output.spending_key == o.spending_key)); + assert!(matches!( + result, + Err(OutputManagerError::KeyManagerServiceError( + KeyManagerServiceError::KeyNotFoundInKeyChain + )) + )); } } - -#[tokio::test] -async fn recovered_output_key_not_in_keychain() { - let factories = CryptoFactories::default(); - let (connection, _tempdir) = get_temp_sqlite_database_connection(); - let backend = OutputManagerSqliteDatabase::new(connection.clone(), None); - let ks_backend = KeyManagerSqliteDatabase::new(connection, None).unwrap(); - let mut oms = setup_output_manager_service(backend.clone(), ks_backend, true).await; - - let (_ti, uo) = make_input( - &mut OsRng, - MicroTari::from(1000u64), - &factories.commitment, - Some(oms.output_manager_handle.clone()), - ) - .await; - - let rewindable_output = uo - .as_rewindable_transaction_output(&factories, &oms.rewind_data, None) - .unwrap(); - - let result = oms - .output_manager_handle - .scan_for_recoverable_outputs(vec![rewindable_output]) - .await; - - assert!(matches!( - result, - Err(OutputManagerError::KeyManagerServiceError( - KeyManagerServiceError::KeyNotFoundInKeyChain - )) - )); -} diff --git a/base_layer/wallet_ffi/Cargo.toml b/base_layer/wallet_ffi/Cargo.toml index aea8d20c59..7c6b38d852 100644 --- a/base_layer/wallet_ffi/Cargo.toml +++ b/base_layer/wallet_ffi/Cargo.toml @@ -31,6 +31,7 @@ thiserror = "1.0.26" tokio = "1.11" env_logger = "0.7.0" num-traits = "0.2.15" +itertools = "0.10.3" # # 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 3d24089b34..9759cc4855 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}, + fmt::{Display, Formatter}, mem::ManuallyDrop, num::NonZeroU16, path::PathBuf, @@ -68,7 +69,8 @@ use std::{ use chrono::{DateTime, Local}; use error::LibWalletError; -use libc::{c_char, c_int, c_uchar, c_uint, c_ulonglong, c_ushort}; +use itertools::Itertools; +use libc::{c_char, c_int, c_uchar, c_uint, c_ulonglong, c_ushort, c_void}; use log::{LevelFilter, *}; use log4rs::{ append::{ @@ -137,6 +139,7 @@ use tari_wallet::{ error::OutputManagerError, storage::{ database::{OutputBackendQuery, OutputManagerDatabase, SortDirection}, + models::DbUnblindedOutput, OutputStatus, }, }, @@ -227,11 +230,141 @@ pub struct TariWallet { shutdown: Shutdown, } +#[derive(Debug)] +#[repr(C)] +pub enum TariUtxoSort { + ValueAsc = 0, + ValueDesc = 1, + MinedHeightAsc = 2, + MinedHeightDesc = 3, +} + #[derive(Debug, Clone)] #[repr(C)] pub struct TariUtxo { pub commitment: *mut c_char, pub value: u64, + pub mined_height: u64, +} + +impl TryFrom for TariUtxo { + type Error = InterfaceError; + + fn try_from(x: DbUnblindedOutput) -> Result { + Ok(Self { + commitment: CString::new(x.commitment.to_hex()) + .map_err(|e| { + InterfaceError::InvalidArgument(format!("failed to obtain hex from a commitment: {:?}", e)) + })? + .into_raw(), + value: x.unblinded_output.value.as_u64(), + mined_height: x.mined_height.unwrap_or(0), + }) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[repr(C)] +pub enum TariTypeTag { + String = 0, + Utxo = 1, + Commitment = 2, +} + +impl Display for TariTypeTag { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TariTypeTag::String => write!(f, "String"), + TariTypeTag::Utxo => write!(f, "Utxo"), + TariTypeTag::Commitment => write!(f, "Commitment"), + } + } +} + +#[derive(Debug, Clone)] +#[repr(C)] +pub struct TariVector { + pub tag: TariTypeTag, + pub len: usize, + pub cap: usize, + pub ptr: *mut c_void, +} + +#[allow(dead_code)] +impl TariVector { + fn from_string_vec(v: Vec) -> Result { + let mut strings = ManuallyDrop::new( + v.into_iter() + .map(|x| CString::new(x.as_str()).unwrap().into_raw()) + .collect::>(), + ); + + Ok(Self { + tag: TariTypeTag::String, + len: strings.len(), + cap: strings.capacity(), + ptr: strings.as_mut_ptr() as *mut c_void, + }) + } + + fn from_commitment_vec(v: &mut Vec) -> Result { + Ok(Self { + tag: TariTypeTag::Commitment, + len: v.len(), + cap: v.capacity(), + ptr: v.as_mut_ptr() as *mut c_void, + }) + } + + fn to_string_vec(&self) -> Result, InterfaceError> { + if self.tag != TariTypeTag::String { + return Err(InterfaceError::InvalidArgument(format!( + "expecting String, got {}", + self.tag + ))); + } + + if self.ptr.is_null() { + return Err(InterfaceError::NullError(String::from( + "tari vector of strings has null pointer", + ))); + } + + Ok(unsafe { + Vec::from_raw_parts(self.ptr as *mut *mut c_char, self.len, self.cap) + .into_iter() + .map(|x| CString::from_raw(x).into_string().unwrap()) + .collect() + }) + } + + fn to_commitment_vec(&self) -> Result, InterfaceError> { + self.to_string_vec()? + .into_iter() + .map(|x| { + Commitment::from_hex(x.as_str()) + .map_err(|e| InterfaceError::PointerError(format!("failed to convert hex to commitment: {:?}", e))) + }) + .try_collect::, InterfaceError>() + } + + #[allow(dead_code)] + fn to_utxo_vec(&self) -> Result, InterfaceError> { + if self.tag != TariTypeTag::Utxo { + return Err(InterfaceError::InvalidArgument(format!( + "expecting Utxo, got {}", + self.tag + ))); + } + + if self.ptr.is_null() { + return Err(InterfaceError::NullError(String::from( + "tari vector of utxos has null pointer", + ))); + } + + Ok(unsafe { Vec::from_raw_parts(self.ptr as *mut TariUtxo, self.len, self.cap) }) + } } #[derive(Debug, Clone)] @@ -242,13 +375,37 @@ pub struct TariOutputs { pub ptr: *mut TariUtxo, } -#[derive(Debug)] -#[repr(C)] -pub enum TariUtxoSort { - ValueAsc, - ValueDesc, - MinedHeightAsc, - MinedHeightDesc, +// WARNING: must be destroyed properly after use +impl TryFrom> for TariOutputs { + type Error = InterfaceError; + + fn try_from(outputs: Vec) -> Result { + let mut outputs = ManuallyDrop::new( + outputs + .into_iter() + .map(|x| { + Ok(TariUtxo { + commitment: CString::new(x.commitment.to_hex()) + .map_err(|e| { + InterfaceError::InvalidArgument(format!( + "failed to obtain hex from a commitment: {:?}", + e + )) + })? + .into_raw(), + value: x.unblinded_output.value.as_u64(), + mined_height: x.mined_height.unwrap_or(0), + }) + }) + .try_collect::, InterfaceError>()?, + ); + + Ok(Self { + len: outputs.len(), + cap: outputs.capacity(), + ptr: outputs.as_mut_ptr(), + }) + } } /// -------------------------------- Strings ------------------------------------------------ /// @@ -4128,25 +4285,26 @@ pub unsafe extern "C" fn wallet_get_utxos( page_size: usize, sorting: TariUtxoSort, dust_threshold: u64, - error_out: *mut i32, + error_ptr: *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, + error_ptr, + LibWalletError::from(InterfaceError::NullError("wallet".to_string())).code, ); return ptr::null_mut(); } - 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 page = i64::from_usize(page).unwrap_or(i64::MAX); + let page_size = i64::from_usize(page_size).unwrap_or(i64::MAX); 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], + commitments: vec![], pagination: Some((page, page_size)), value_min: Some((dust_threshold, false)), value_max: None, @@ -4159,51 +4317,29 @@ pub unsafe extern "C" fn wallet_get_utxos( }; match (*wallet).wallet.output_db.fetch_outputs_by(q) { - Ok(unblinded_outputs) => { - let outputs: Vec = unblinded_outputs - .into_iter() - .filter_map(|out| { - 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) => { - error!( - target: LOG_TARGET, - "failed to obtain commitment from the transaction output: {:#?}", e - ); - return None; - }, - }, - }) - }) - .collect(); - - let mut outputs = ManuallyDrop::new(outputs); - Box::into_raw(Box::new(TariOutputs { - len: outputs.len(), - cap: outputs.capacity(), - ptr: outputs.as_mut_ptr(), - })) + Ok(outputs) => match TariOutputs::try_from(outputs) { + Ok(tari_outputs) => { + ptr::replace(error_ptr, 0); + Box::into_raw(Box::new(tari_outputs)) + }, + Err(e) => { + error!( + target: LOG_TARGET, + "failed to convert outputs to `TariOutputs`: {:#?}", e + ); + ptr::replace(error_ptr, LibWalletError::from(e).code); + ptr::null_mut() + }, }, Err(e) => { error!(target: LOG_TARGET, "failed to obtain outputs: {:#?}", e); ptr::replace( - error_out, + error_ptr, LibWalletError::from(WalletError::OutputManagerError( OutputManagerError::OutputManagerStorageError(e), )) - .code as c_int, + .code, ); ptr::null_mut() }, @@ -4228,6 +4364,169 @@ pub unsafe extern "C" fn destroy_tari_outputs(x: *mut TariOutputs) { } } +/// Frees memory for a `TariVector` +/// +/// ## Arguments +/// `x` - The pointer to `TariVector` +/// +/// ## Returns +/// `()` - Does not return a value, equivalent to void in C +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn destroy_tari_vector(x: *mut TariVector) { + if !x.is_null() { + let x = Box::from_raw(x); + _ = x.ptr; + } +} + +/// This function will tell the wallet to do a coin split. +/// +/// ## Arguments +/// * `wallet` - The TariWallet pointer +/// * `commitments` - A `TariVector` of "strings", tagged as `TariTypeTag::String`, containing commitment's hex values +/// (see `Commitment::to_hex()`) +/// * `amount_per_split` - The amount to split +/// * `number_of_splits` - The number of times to split the amount +/// * `fee_per_gram` - The transaction fee +/// * `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` - Returns the transaction id. +/// +/// # Safety +/// `TariVector` must be freed after use with `destroy_tari_vector()` +#[no_mangle] +pub unsafe extern "C" fn wallet_coin_split( + wallet: *mut TariWallet, + commitments: *mut TariVector, + amount_per_split: u64, + number_of_splits: usize, + fee_per_gram: u64, + error_ptr: *mut i32, +) -> u64 { + if wallet.is_null() { + error!(target: LOG_TARGET, "wallet pointer is null"); + ptr::replace( + error_ptr, + LibWalletError::from(InterfaceError::NullError("wallet".to_string())).code as c_int, + ); + return 0; + } + + let commitments = match commitments.as_ref() { + None => { + error!(target: LOG_TARGET, "failed to obtain commitments as reference"); + ptr::replace( + error_ptr, + LibWalletError::from(InterfaceError::NullError("commitments vector".to_string())).code as c_int, + ); + return 0; + }, + Some(cs) => match cs.to_commitment_vec() { + Ok(cs) => cs, + Err(e) => { + error!(target: LOG_TARGET, "failed to convert from tari vector: {:?}", e); + ptr::replace(error_ptr, LibWalletError::from(e).code as c_int); + return 0; + }, + }, + }; + + match (*wallet).runtime.block_on((*wallet).wallet.coin_split( + commitments, + MicroTari(amount_per_split), + number_of_splits, + MicroTari(fee_per_gram), + String::new(), + )) { + Ok(tx_id) => { + ptr::replace(error_ptr, 0); + tx_id.as_u64() + }, + Err(e) => { + error!(target: LOG_TARGET, "failed to join outputs: {:#?}", e); + ptr::replace(error_ptr, LibWalletError::from(e).code); + 0 + }, + } +} + +/// This function will tell the wallet to do a coin join, resulting in a new coin worth a sum of the joined coins minus +/// the fee. +/// +/// ## Arguments +/// * `wallet` - The TariWallet pointer +/// * `commitments` - A `TariVector` of "strings", tagged as `TariTypeTag::String`, containing commitment's hex values +/// (see `Commitment::to_hex()`) +/// * `fee_per_gram` - The transaction fee +/// * `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` - Returns the transaction id. +/// +/// # Safety +/// `TariVector` must be freed after use with `destroy_tari_vector()` +#[no_mangle] +pub unsafe extern "C" fn wallet_coin_join( + wallet: *mut TariWallet, + commitments: *mut TariVector, + fee_per_gram: u64, + error_ptr: *mut i32, +) -> u64 { + if wallet.is_null() { + error!(target: LOG_TARGET, "wallet pointer is null"); + ptr::replace( + error_ptr, + LibWalletError::from(InterfaceError::NullError("wallet".to_string())).code as c_int, + ); + return 0; + } + + let commitments = match commitments.as_ref() { + None => { + error!(target: LOG_TARGET, "failed to obtain commitments as reference"); + ptr::replace( + error_ptr, + LibWalletError::from(InterfaceError::NullError("commitments vector".to_string())).code as c_int, + ); + return 0; + }, + Some(cs) => match cs.to_commitment_vec() { + Ok(cs) => cs, + Err(e) => { + error!(target: LOG_TARGET, "failed to convert from tari vector: {:?}", e); + ptr::replace(error_ptr, LibWalletError::from(e).code as c_int); + return 0; + }, + }, + }; + + match (*wallet).runtime.block_on( + (*wallet) + .wallet + .output_manager_service + .create_coin_join(commitments, fee_per_gram.into()), + ) { + Ok((tx_id, _, _)) => { + ptr::replace(error_ptr, 0); + tx_id.as_u64() + }, + + Err(e) => { + error!(target: LOG_TARGET, "failed to join outputs: {:#?}", e); + ptr::replace(error_ptr, LibWalletError::from(WalletError::OutputManagerError(e)).code); + 0 + }, + } +} + /// Signs a message using the public key of the TariWallet /// /// ## Arguments @@ -5904,71 +6203,6 @@ pub unsafe extern "C" fn wallet_restart_transaction_broadcast(wallet: *mut TariW } } -/// This function will tell the wallet to do a coin split. -/// -/// ## Arguments -/// `wallet` - The TariWallet pointer -/// `amount` - The amount to split -/// `count` - The number of times to split the amount -/// `fee` - The transaction fee -/// `msg` - Message for split -/// `lock_height` - The number of bocks to lock the transaction for -/// `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` - Returns the transaction id. -/// -/// # Safety -/// None -#[no_mangle] -pub unsafe extern "C" fn wallet_coin_split( - wallet: *mut TariWallet, - amount: c_ulonglong, - count: c_ulonglong, - fee: c_ulonglong, - msg: *const c_char, - lock_height: c_ulonglong, - error_out: *mut c_int, -) -> c_ulonglong { - 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); - } - - let message; - - if msg.is_null() { - message = "Coin Split".to_string() - } else { - match CStr::from_ptr(msg).to_str() { - Ok(v) => { - message = v.to_owned(); - }, - _ => { - message = "Coin Split".to_string(); - }, - } - }; - - match (*wallet).runtime.block_on((*wallet).wallet.coin_split( - MicroTari(amount), - count as usize, - MicroTari(fee), - message, - Some(lock_height), - )) { - Ok(request_key) => request_key.as_u64(), - Err(e) => { - error = LibWalletError::from(e).code; - ptr::swap(error_out, &mut error as *mut c_int); - 0 - }, - } -} - /// Gets the seed words representing the seed private key of the provided `TariWallet`. /// /// ## Arguments @@ -8814,7 +9048,7 @@ mod test { }); // ascending order - let outputs = wallet_get_utxos(alice_wallet, 1, 20, TariUtxoSort::ValueAsc, 3000, error_ptr); + let outputs = wallet_get_utxos(alice_wallet, 0, 20, TariUtxoSort::ValueAsc, 3000, error_ptr); let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); assert_eq!(error, 0); assert_eq!((*outputs).len, 6); @@ -8829,7 +9063,7 @@ mod test { destroy_tari_outputs(outputs); // descending order - let outputs = wallet_get_utxos(alice_wallet, 1, 20, TariUtxoSort::ValueDesc, 3000, error_ptr); + let outputs = wallet_get_utxos(alice_wallet, 0, 20, TariUtxoSort::ValueDesc, 3000, error_ptr); let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); assert_eq!(error, 0); assert_eq!((*outputs).len, 6); @@ -8844,7 +9078,7 @@ mod test { destroy_tari_outputs(outputs); // result must be empty due to high dust threshold - let outputs = wallet_get_utxos(alice_wallet, 1, 20, TariUtxoSort::ValueAsc, 15000, error_ptr); + let outputs = wallet_get_utxos(alice_wallet, 0, 20, TariUtxoSort::ValueAsc, 15000, error_ptr); let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); assert_eq!(error, 0); assert_eq!((*outputs).len, 0); @@ -8861,4 +9095,284 @@ mod test { wallet_destroy(alice_wallet); } } + + #[test] + #[allow(clippy::too_many_lines)] + fn test_wallet_coin_join() { + unsafe { + let mut error = 0; + let error_ptr = &mut error as *mut c_int; + let mut recovery_in_progress = true; + let recovery_in_progress_ptr = &mut recovery_in_progress as *mut bool; + + let secret_key_alice = private_key_generate(); + let db_name_alice = CString::new(random::string(8).as_str()).unwrap(); + let db_name_alice_str: *const c_char = CString::into_raw(db_name_alice) as *const c_char; + let alice_temp_dir = tempdir().unwrap(); + let db_path_alice = CString::new(alice_temp_dir.path().to_str().unwrap()).unwrap(); + let db_path_alice_str: *const c_char = CString::into_raw(db_path_alice) as *const c_char; + let transport_config_alice = transport_memory_create(); + let address_alice = transport_memory_get_address(transport_config_alice, error_ptr); + let address_alice_str = CStr::from_ptr(address_alice).to_str().unwrap().to_owned(); + let address_alice_str: *const c_char = CString::new(address_alice_str).unwrap().into_raw() as *const c_char; + let network = CString::new(NETWORK_STRING).unwrap(); + let network_str: *const c_char = CString::into_raw(network) as *const c_char; + + let alice_config = comms_config_create( + address_alice_str, + transport_config_alice, + db_name_alice_str, + db_path_alice_str, + 20, + 10800, + error_ptr, + ); + + let alice_wallet = wallet_create( + alice_config, + ptr::null(), + 0, + 0, + ptr::null(), + ptr::null(), + network_str, + received_tx_callback, + received_tx_reply_callback, + received_tx_finalized_callback, + broadcast_callback, + mined_callback, + mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, + transaction_send_result_callback, + tx_cancellation_callback, + txo_validation_complete_callback, + contacts_liveness_data_updated_callback, + balance_updated_callback, + transaction_validation_complete_callback, + saf_messages_received_callback, + connectivity_status_callback, + recovery_in_progress_ptr, + error_ptr, + ); + + (1..=5).for_each(|i| { + (*alice_wallet) + .runtime + .block_on((*alice_wallet).wallet.output_manager_service.add_output( + create_test_input((15000 * i).into(), 0, &PedersenCommitmentFactory::default()).1, + None, + )) + .unwrap(); + }); + + let outputs = wallet_get_utxos(alice_wallet, 0, 100, TariUtxoSort::ValueAsc, 0, error_ptr); + let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); + assert_eq!(error, 0); + + let payload = utxos[0..3] + .iter() + .map(|x| CString::from_raw(x.commitment).into_string().unwrap()) + .collect::>(); + + let commitments = Box::into_raw(Box::new(TariVector::from_string_vec(payload).unwrap())) as *mut TariVector; + + let result = wallet_coin_join(alice_wallet, commitments, 5, error_ptr); + assert_eq!(error, 0); + assert!(result > 0); + + let unspent_outputs = (*alice_wallet) + .wallet + .output_db + .fetch_outputs_by(OutputBackendQuery { + status: vec![OutputStatus::Unspent], + ..Default::default() + }) + .unwrap() + .into_iter() + .map(|x| x.unblinded_output.value) + .collect::>(); + + let new_pending_outputs = (*alice_wallet) + .wallet + .output_db + .fetch_outputs_by(OutputBackendQuery { + status: vec![OutputStatus::EncumberedToBeReceived], + ..Default::default() + }) + .unwrap() + .into_iter() + .map(|x| x.unblinded_output.value) + .collect::>(); + + let outputs = wallet_get_utxos(alice_wallet, 0, 20, TariUtxoSort::ValueAsc, 0, error_ptr); + let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); + assert_eq!(error, 0); + assert_eq!(utxos.len(), 2); + assert_eq!(unspent_outputs.len(), 2); + assert_eq!(new_pending_outputs.len(), 1); + + destroy_tari_outputs(outputs); + destroy_tari_vector(commitments); + + string_destroy(network_str as *mut c_char); + string_destroy(db_name_alice_str as *mut c_char); + string_destroy(db_path_alice_str as *mut c_char); + 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); + } + } + + #[test] + #[allow(clippy::too_many_lines)] + fn test_wallet_coin_split() { + unsafe { + let mut error = 0; + let error_ptr = &mut error as *mut c_int; + let mut recovery_in_progress = true; + let recovery_in_progress_ptr = &mut recovery_in_progress as *mut bool; + + let secret_key_alice = private_key_generate(); + let db_name_alice = CString::new(random::string(8).as_str()).unwrap(); + let db_name_alice_str: *const c_char = CString::into_raw(db_name_alice) as *const c_char; + let alice_temp_dir = tempdir().unwrap(); + let db_path_alice = CString::new(alice_temp_dir.path().to_str().unwrap()).unwrap(); + let db_path_alice_str: *const c_char = CString::into_raw(db_path_alice) as *const c_char; + let transport_config_alice = transport_memory_create(); + let address_alice = transport_memory_get_address(transport_config_alice, error_ptr); + let address_alice_str = CStr::from_ptr(address_alice).to_str().unwrap().to_owned(); + let address_alice_str: *const c_char = CString::new(address_alice_str).unwrap().into_raw() as *const c_char; + let network = CString::new(NETWORK_STRING).unwrap(); + let network_str: *const c_char = CString::into_raw(network) as *const c_char; + + let alice_config = comms_config_create( + address_alice_str, + transport_config_alice, + db_name_alice_str, + db_path_alice_str, + 20, + 10800, + error_ptr, + ); + + let alice_wallet = wallet_create( + alice_config, + ptr::null(), + 0, + 0, + ptr::null(), + ptr::null(), + network_str, + received_tx_callback, + received_tx_reply_callback, + received_tx_finalized_callback, + broadcast_callback, + mined_callback, + mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, + transaction_send_result_callback, + tx_cancellation_callback, + txo_validation_complete_callback, + contacts_liveness_data_updated_callback, + balance_updated_callback, + transaction_validation_complete_callback, + saf_messages_received_callback, + connectivity_status_callback, + recovery_in_progress_ptr, + error_ptr, + ); + + (1..=5).for_each(|i| { + (*alice_wallet) + .runtime + .block_on((*alice_wallet).wallet.output_manager_service.add_output( + create_test_input((15000 * i).into(), 0, &PedersenCommitmentFactory::default()).1, + None, + )) + .unwrap(); + }); + + let outputs = wallet_get_utxos(alice_wallet, 0, 100, TariUtxoSort::ValueAsc, 0, error_ptr); + let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); + assert_eq!(error, 0); + + let payload = utxos[0..3] + .iter() + .map(|x| CString::from_raw(x.commitment).into_string().unwrap()) + .collect::>(); + + let commitments = Box::into_raw(Box::new(TariVector::from_string_vec(payload).unwrap())) as *mut TariVector; + + let result = wallet_coin_split(alice_wallet, commitments, 20500, 3, 5, error_ptr); + assert_eq!(error, 0); + assert!(result > 0); + + let unspent_outputs = (*alice_wallet) + .wallet + .output_db + .fetch_outputs_by(OutputBackendQuery { + status: vec![OutputStatus::Unspent], + ..Default::default() + }) + .unwrap() + .into_iter() + .map(|x| x.unblinded_output.value) + .collect::>(); + + let new_pending_outputs = (*alice_wallet) + .wallet + .output_db + .fetch_outputs_by(OutputBackendQuery { + status: vec![OutputStatus::EncumberedToBeReceived], + ..Default::default() + }) + .unwrap() + .into_iter() + .map(|x| x.unblinded_output.value) + .collect::>(); + + let outputs = wallet_get_utxos(alice_wallet, 0, 20, TariUtxoSort::ValueAsc, 0, error_ptr); + let utxos: &[TariUtxo] = slice::from_raw_parts_mut((*outputs).ptr, (*outputs).len); + assert_eq!(error, 0); + assert_eq!(utxos.len(), 2); + assert_eq!(unspent_outputs.len(), 2); + assert_eq!(new_pending_outputs.len(), 4); + assert!(new_pending_outputs[0..3].iter().all(|x| *x == 20500.into())); + + destroy_tari_outputs(outputs); + destroy_tari_vector(commitments); + + string_destroy(network_str as *mut c_char); + string_destroy(db_name_alice_str as *mut c_char); + string_destroy(db_path_alice_str as *mut c_char); + 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); + } + } + + #[test] + fn test_tari_vector() { + let mut strings = ManuallyDrop::new(vec![ + CString::new("string0").unwrap().into_raw(), + CString::new("string1").unwrap().into_raw(), + CString::new("string2").unwrap().into_raw(), + ]); + + let v = TariVector { + tag: TariTypeTag::String, + len: strings.len(), + cap: strings.capacity(), + ptr: strings.as_mut_ptr() as *mut c_void, + }; + + eprintln!("v = {:#?}", v); + eprintln!("result = {:#?}", v.to_string_vec()); + } } diff --git a/base_layer/wallet_ffi/tari_wallet_ffi.h b/base_layer/wallet_ffi/tari_wallet_ffi.h index 31ff6774b5..09f19acf2f 100644 --- a/base_layer/wallet_ffi/tari_wallet_ffi.h +++ b/base_layer/wallet_ffi/tari_wallet_ffi.h @@ -11,11 +11,17 @@ */ #define OutputFields_NUM_FIELDS 10 +enum TariTypeTag { + String = 0, + Utxo = 1, + Commitment = 2, +}; + enum TariUtxoSort { - ValueAsc, - ValueDesc, - MinedHeightAsc, - MinedHeightDesc, + ValueAsc = 0, + ValueDesc = 1, + MinedHeightAsc = 2, + MinedHeightDesc = 3, }; /** @@ -279,6 +285,7 @@ typedef struct Balance TariBalance; struct TariUtxo { char *commitment; uint64_t value; + uint64_t mined_height; }; struct TariOutputs { @@ -287,6 +294,13 @@ struct TariOutputs { struct TariUtxo *ptr; }; +struct TariVector { + enum TariTypeTag tag; + uintptr_t len; + uintptr_t cap; + void *ptr; +}; + typedef struct FeePerGramStatsResponse TariFeePerGramStats; typedef struct FeePerGramStat TariFeePerGramStat; @@ -2190,7 +2204,7 @@ struct TariOutputs *wallet_get_utxos(struct TariWallet *wallet, uintptr_t page_size, enum TariUtxoSort sorting, uint64_t dust_threshold, - int32_t *error_out); + int32_t *error_ptr); /** * Frees memory for a `TariOutputs` @@ -2206,6 +2220,71 @@ struct TariOutputs *wallet_get_utxos(struct TariWallet *wallet, */ void destroy_tari_outputs(struct TariOutputs *x); +/** + * Frees memory for a `TariVector` + * + * ## Arguments + * `x` - The pointer to `TariVector` + * + * ## Returns + * `()` - Does not return a value, equivalent to void in C + * + * # Safety + * None + */ +void destroy_tari_vector(struct TariVector *x); + +/** + * This function will tell the wallet to do a coin split. + * + * ## Arguments + * * `wallet` - The TariWallet pointer + * * `commitments` - A `TariVector` of "strings", tagged as `TariTypeTag::String`, containing commitment's hex values + * (see `Commitment::to_hex()`) + * * `amount_per_split` - The amount to split + * * `number_of_splits` - The number of times to split the amount + * * `fee_per_gram` - The transaction fee + * * `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` - Returns the transaction id. + * + * # Safety + * `TariVector` must be freed after use with `destroy_tari_vector()` + */ +uint64_t wallet_coin_split(struct TariWallet *wallet, + struct TariVector *commitments, + uint64_t amount_per_split, + uintptr_t number_of_splits, + uint64_t fee_per_gram, + int32_t *error_ptr); + +/** + * This function will tell the wallet to do a coin join, resulting in a new coin worth a sum of the joined coins minus + * the fee. + * + * ## Arguments + * * `wallet` - The TariWallet pointer + * * `commitments` - A `TariVector` of "strings", tagged as `TariTypeTag::String`, containing commitment's hex values + * (see `Commitment::to_hex()`) + * * `fee_per_gram` - The transaction fee + * * `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` - Returns the transaction id. + * + * # Safety + * `TariVector` must be freed after use with `destroy_tari_vector()` + */ +uint64_t wallet_coin_join(struct TariWallet *wallet, + struct TariVector *commitments, + uint64_t fee_per_gram, + int32_t *error_ptr); + /** * Signs a message using the public key of the TariWallet * @@ -2790,33 +2869,6 @@ unsigned long long wallet_start_transaction_validation(struct TariWallet *wallet bool wallet_restart_transaction_broadcast(struct TariWallet *wallet, int *error_out); -/** - * This function will tell the wallet to do a coin split. - * - * ## Arguments - * `wallet` - The TariWallet pointer - * `amount` - The amount to split - * `count` - The number of times to split the amount - * `fee` - The transaction fee - * `msg` - Message for split - * `lock_height` - The number of bocks to lock the transaction for - * `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` - Returns the transaction id. - * - * # Safety - * None - */ -unsigned long long wallet_coin_split(struct TariWallet *wallet, - unsigned long long amount, - unsigned long long count, - unsigned long long fee, - const char *msg, - unsigned long long lock_height, - int *error_out); - /** * Gets the seed words representing the seed private key of the provided `TariWallet`. *