From 702e92fc1704a6918687f1b18c9306094eb79c8c Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Fri, 28 Jan 2022 15:30:43 +0200 Subject: [PATCH 1/3] Add faux transaction handling - Added separate statuses for faux transactions to enable unique events and validation for scanned one-sided transactions. - Added validation for one-sided payments updating the associated faux transaction mined height and confirmations. - Added callbacks for faux transactions (unconfirmed and confirmed). - Updated the wallet FFI with the callbacks. --- applications/ffi_client/index.js | 14 ++ applications/ffi_client/lib/index.js | 3 + applications/ffi_client/recovery.js | 12 ++ applications/tari_app_grpc/proto/wallet.proto | 4 + .../src/conversions/transaction.rs | 2 + .../src/grpc/wallet_grpc_server.rs | 8 +- .../src/ui/state/wallet_event_monitor.rs | 9 +- base_layer/common_types/src/transaction.rs | 54 ++++++- .../src/output_manager_service/handle.rs | 27 ++-- .../recovery/standard_outputs_recoverer.rs | 4 +- .../src/output_manager_service/service.rs | 29 +++- .../wallet/src/transaction_service/handle.rs | 59 +++++-- .../protocols/transaction_receive_protocol.rs | 1 + .../protocols/transaction_send_protocol.rs | 1 + .../transaction_validation_protocol.rs | 20 ++- .../wallet/src/transaction_service/service.rs | 75 ++++++--- .../transaction_service/storage/database.rs | 47 +++++- .../src/transaction_service/storage/models.rs | 4 +- .../transaction_service/storage/sqlite_db.rs | 65 +++++++- ...us.rs => check_faux_transaction_status.rs} | 55 ++++++- .../src/transaction_service/tasks/mod.rs | 2 +- .../utxo_scanner_service/utxo_scanner_task.rs | 48 ++++-- base_layer/wallet/src/wallet.rs | 26 ++- .../support/output_manager_service_mock.rs | 4 +- .../tests/support/transaction_service_mock.rs | 2 +- .../transaction_service_tests/service.rs | 127 +++++++++++---- .../transaction_service_tests/storage.rs | 79 +++++++++- .../transaction_protocols.rs | 1 + base_layer/wallet/tests/wallet.rs | 96 ++++++++--- base_layer/wallet_ffi/src/callback_handler.rs | 60 ++++++- .../wallet_ffi/src/callback_handler_tests.rs | 149 ++++++++++++++++-- base_layer/wallet_ffi/src/lib.rs | 79 +++++++++- base_layer/wallet_ffi/wallet.h | 63 +++++--- integration_tests/features/WalletFFI.feature | 11 +- .../features/support/ffi_steps.js | 49 +++++- .../features/support/wallet_steps.js | 4 +- integration_tests/features/support/world.js | 4 +- integration_tests/helpers/ffi/ffiInterface.js | 14 ++ integration_tests/helpers/ffi/wallet.js | 97 ++++++++---- integration_tests/package-lock.json | 72 +++------ 40 files changed, 1192 insertions(+), 288 deletions(-) rename base_layer/wallet/src/transaction_service/tasks/{check_imported_transaction_status.rs => check_faux_transaction_status.rs} (55%) diff --git a/applications/ffi_client/index.js b/applications/ffi_client/index.js index 20aeaf6c39..44ecf21dad 100644 --- a/applications/ffi_client/index.js +++ b/applications/ffi_client/index.js @@ -69,6 +69,18 @@ try { console.log("txMinedUnconfirmed: ", ptr, confirmations); } ); + // callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), + const txScanned = ffi.Callback("void", ["pointer"], function (ptr) { + console.log("txScanned: ", ptr); + }); + // callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), + const txScannedUnconfirmed = ffi.Callback( + "void", + ["pointer"], + function (ptr, confirmations) { + console.log("txScannedUnconfirmed: ", ptr, confirmations); + } + ); // callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), const directSendResult = ffi.Callback("void", [u64, bool], function (i, j) { console.log("directSendResult: ", i, j); @@ -112,6 +124,8 @@ try { txBroadcast, txMined, txMinedUnconfirmed, + txScanned, + txScannedUnconfirmed, directSendResult, safResult, txCancelled, diff --git a/applications/ffi_client/lib/index.js b/applications/ffi_client/lib/index.js index db922de1f0..b9e8b34da7 100644 --- a/applications/ffi_client/lib/index.js +++ b/applications/ffi_client/lib/index.js @@ -66,6 +66,9 @@ const libWallet = ffi.Library("./libtari_wallet_ffi.dylib", { fn, fn, fn, + fn, + fn, + bool, errPtr, ], ], diff --git a/applications/ffi_client/recovery.js b/applications/ffi_client/recovery.js index 0c51d3daa3..46c3d64265 100644 --- a/applications/ffi_client/recovery.js +++ b/applications/ffi_client/recovery.js @@ -80,6 +80,18 @@ try { console.log("txMinedUnconfirmed: ", ptr, confirmations); } ); + // callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), + const txScanned = ffi.Callback("void", ["pointer"], function (ptr) { + console.log("txScanned: ", ptr); + }); + // callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), + const txScannedUnconfirmed = ffi.Callback( + "void", + ["pointer"], + function (ptr, confirmations) { + console.log("txScannedUnconfirmed: ", ptr, confirmations); + } + ); // callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), const directSendResult = ffi.Callback("void", [u64, bool], function (i, j) { console.log("directSendResult: ", i, j); diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index 3ba81bbf0f..edb4a6957d 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -192,6 +192,10 @@ enum TransactionStatus { TRANSACTION_STATUS_NOT_FOUND = 7; // The transaction was rejected by the mempool TRANSACTION_STATUS_REJECTED = 8; + // This is faux transaction mainly for one-sided transaction outputs or wallet recovery outputs have been found + TRANSACTION_STATUS_FAUX_UNCONFIRMED = 9; + // All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed + TRANSACTION_STATUS_FAUX_CONFIRMED = 10; } message GetCompletedTransactionsRequest { } diff --git a/applications/tari_app_grpc/src/conversions/transaction.rs b/applications/tari_app_grpc/src/conversions/transaction.rs index fffb851ef7..a6f673d8cd 100644 --- a/applications/tari_app_grpc/src/conversions/transaction.rs +++ b/applications/tari_app_grpc/src/conversions/transaction.rs @@ -98,6 +98,8 @@ impl From for grpc::TransactionStatus { Pending => grpc::TransactionStatus::Pending, Coinbase => grpc::TransactionStatus::Coinbase, Rejected => grpc::TransactionStatus::Rejected, + FauxUnconfirmed => grpc::TransactionStatus::ScannedUnconfirmed, + FauxConfirmed => grpc::TransactionStatus::ScannedConfirmed, } } } 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 d394bcb338..297164fb7c 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -73,6 +73,7 @@ use tari_app_grpc::{ }, }; use tari_common_types::{ + transaction::ImportStatus, array::copy_into_fixed_array, types::{BlockHash, PublicKey, Signature}, }; @@ -591,7 +592,12 @@ impl wallet_server::Wallet for WalletGrpcServer { for o in unblinded_outputs.iter() { tx_ids.push( wallet - .import_unblinded_utxo(o.clone(), &CommsPublicKey::default(), "Imported via gRPC".to_string()) + .import_unblinded_utxo( + o.clone(), + &CommsPublicKey::default(), + "Imported via gRPC".to_string(), + ImportStatus::Imported, + ) .await .map_err(|e| Status::internal(format!("{:?}", e)))? .into(), diff --git a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs index 8e341008c6..d01aadd520 100644 --- a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs +++ b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs @@ -94,13 +94,15 @@ impl WalletEventMonitor { self.trigger_balance_refresh(); notifier.transaction_received(tx_id); }, - TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations, is_valid: _} => { + TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations, is_valid: _} | + TransactionEvent::FauxTransactionUnconfirmed{tx_id, num_confirmations, is_valid: _}=> { self.trigger_confirmations_refresh(tx_id, num_confirmations).await; self.trigger_tx_state_refresh(tx_id).await; self.trigger_balance_refresh(); notifier.transaction_mined_unconfirmed(tx_id, num_confirmations); }, - TransactionEvent::TransactionMined{tx_id, is_valid: _} => { + TransactionEvent::TransactionMined{tx_id, is_valid: _} | + TransactionEvent::FauxTransactionConfirmed{tx_id, is_valid: _}=> { self.trigger_confirmations_cleanup(tx_id).await; self.trigger_tx_state_refresh(tx_id).await; self.trigger_balance_refresh(); @@ -114,7 +116,8 @@ impl WalletEventMonitor { TransactionEvent::ReceivedTransaction(tx_id) | TransactionEvent::ReceivedTransactionReply(tx_id) | TransactionEvent::TransactionBroadcast(tx_id) | - TransactionEvent::TransactionMinedRequestTimedOut(tx_id) | TransactionEvent::TransactionImported(tx_id) => { + TransactionEvent::TransactionMinedRequestTimedOut(tx_id) | + TransactionEvent::TransactionImported(tx_id) => { self.trigger_tx_state_refresh(tx_id).await; self.trigger_balance_refresh(); }, diff --git a/base_layer/common_types/src/transaction.rs b/base_layer/common_types/src/transaction.rs index 0e33ca237e..cabc8b60bd 100644 --- a/base_layer/common_types/src/transaction.rs +++ b/base_layer/common_types/src/transaction.rs @@ -17,7 +17,7 @@ pub enum TransactionStatus { Broadcast, /// This transaction has been mined and included in a block. MinedUnconfirmed, - /// This transaction was generated as part of importing a spendable UTXO + /// This transaction was generated as part of importing a spendable unblinded UTXO Imported, /// This transaction is still being negotiated by the parties Pending, @@ -27,6 +27,10 @@ pub enum TransactionStatus { MinedConfirmed, /// This transaction was Rejected by the mempool Rejected, + /// This is faux transaction mainly for one-sided transaction outputs or wallet recovery outputs have been found + FauxUnconfirmed, + /// All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed + FauxConfirmed, } #[derive(Debug, Error)] @@ -48,6 +52,8 @@ impl TryFrom for TransactionStatus { 5 => Ok(TransactionStatus::Coinbase), 6 => Ok(TransactionStatus::MinedConfirmed), 7 => Ok(TransactionStatus::Rejected), + 8 => Ok(TransactionStatus::FauxUnconfirmed), + 9 => Ok(TransactionStatus::FauxConfirmed), code => Err(TransactionConversionError { code }), } } @@ -71,10 +77,56 @@ impl Display for TransactionStatus { TransactionStatus::Pending => write!(f, "Pending"), TransactionStatus::Coinbase => write!(f, "Coinbase"), TransactionStatus::Rejected => write!(f, "Rejected"), + TransactionStatus::FauxUnconfirmed => write!(f, "ScannedUnconfirmed"), + TransactionStatus::FauxConfirmed => write!(f, "ScannedConfirmed"), } } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ImportStatus { + /// This is an invalid transaction import status that will result in an error + Invalid, + /// This transaction import status is used when importing a spendable UTXO + Imported, + /// This transaction import status is used when a one-sided transaction has been scanned but is unconfirmed + ScannedUnconfirmed, + /// This transaction import status is used when a one-sided transaction has been scanned and confirmed + ScannedConfirmed, +} + +impl TryFrom for TransactionStatus { + type Error = TransactionConversionError; + + fn try_from(value: ImportStatus) -> Result { + match value { + ImportStatus::Imported => Ok(TransactionStatus::Imported), + ImportStatus::ScannedUnconfirmed => Ok(TransactionStatus::FauxUnconfirmed), + ImportStatus::ScannedConfirmed => Ok(TransactionStatus::FauxConfirmed), + _ => Err(TransactionConversionError { code: i32::MAX }), + } + } +} + +impl TryFrom for ImportStatus { + type Error = TransactionConversionError; + + fn try_from(value: TransactionStatus) -> Result { + match value { + TransactionStatus::Imported => Ok(ImportStatus::Imported), + TransactionStatus::FauxUnconfirmed => Ok(ImportStatus::ScannedUnconfirmed), + TransactionStatus::FauxConfirmed => Ok(ImportStatus::ScannedConfirmed), + _ => Err(TransactionConversionError { code: i32::MAX }), + } + } +} + +impl Default for ImportStatus { + fn default() -> Self { + ImportStatus::Invalid + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum TransactionDirection { Inbound, diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 315c931a6a..1744ed866d 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -41,6 +41,7 @@ use tari_crypto::{script::TariScript, tari_utilities::hex::Hex}; use tari_service_framework::reply_channel::SenderService; use tokio::sync::broadcast; use tower::Service; +use tari_common_types::types::BlockHash; use crate::output_manager_service::{ error::OutputManagerError, @@ -106,8 +107,14 @@ pub enum OutputManagerRequest { num_kernels: usize, num_outputs: usize, }, - ScanForRecoverableOutputs(Vec), - ScanOutputs(Vec), + ScanForRecoverableOutputs { + outputs: Vec, + tx_id: TxId + }, + ScanOutputs { + outputs: Vec, + tx_id: TxId + }, AddKnownOneSidedPaymentScript(KnownOneSidedPaymentScript), CreateOutputWithFeatures { value: MicroTari, @@ -165,8 +172,8 @@ impl fmt::Display for OutputManagerRequest { "FeeEstimate(amount: {}, fee_per_gram: {}, num_kernels: {}, num_outputs: {})", amount, fee_per_gram, num_kernels, num_outputs ), - ScanForRecoverableOutputs(_) => write!(f, "ScanForRecoverableOutputs"), - ScanOutputs(_) => write!(f, "ScanOutputs"), + ScanForRecoverableOutputs {.. } => write!(f, "ScanForRecoverableOutputs"), + ScanOutputs {.. } => write!(f, "ScanOutputs"), AddKnownOneSidedPaymentScript(_) => write!(f, "AddKnownOneSidedPaymentScript"), CreateOutputWithFeatures { value, features } => { write!(f, "CreateOutputWithFeatures({}, {})", value, features,) @@ -225,7 +232,7 @@ pub enum OutputManagerResponse { ReinstatedCancelledInboundTx, CoinbaseAbandonedSet, ClaimHtlcTransaction((TxId, MicroTari, MicroTari, Transaction)), - OutputStatusesByTxId(Vec), + OutputStatusesByTxId {statuses: Vec, mined_height: Option, block_hash: Option}, } pub type OutputManagerEventSender = broadcast::Sender>; @@ -642,10 +649,11 @@ impl OutputManagerHandle { pub async fn scan_for_recoverable_outputs( &mut self, outputs: Vec, + tx_id: TxId, ) -> Result, OutputManagerError> { match self .handle - .call(OutputManagerRequest::ScanForRecoverableOutputs(outputs)) + .call(OutputManagerRequest::ScanForRecoverableOutputs {outputs, tx_id}) .await?? { OutputManagerResponse::RewoundOutputs(outputs) => Ok(outputs), @@ -656,8 +664,9 @@ impl OutputManagerHandle { pub async fn scan_outputs_for_one_sided_payments( &mut self, outputs: Vec, + tx_id: TxId, ) -> Result, OutputManagerError> { - match self.handle.call(OutputManagerRequest::ScanOutputs(outputs)).await?? { + match self.handle.call(OutputManagerRequest::ScanOutputs {outputs, tx_id}).await?? { OutputManagerResponse::ScanOutputs(outputs) => Ok(outputs), _ => Err(OutputManagerError::UnexpectedApiResponse), } @@ -749,13 +758,13 @@ impl OutputManagerHandle { } } - pub async fn get_output_statuses_by_tx_id(&mut self, tx_id: TxId) -> Result, OutputManagerError> { + pub async fn get_output_statuses_by_tx_id(&mut self, tx_id: TxId) -> Result<(Vec, Option, Option), OutputManagerError> { match self .handle .call(OutputManagerRequest::GetOutputStatusesByTxId(tx_id)) .await?? { - OutputManagerResponse::OutputStatusesByTxId(s) => Ok(s), + OutputManagerResponse::OutputStatusesByTxId {statuses, mined_height, block_hash} => Ok((statuses, mined_height, block_hash)), _ => Err(OutputManagerError::UnexpectedApiResponse), } } diff --git a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs index d6f4200e0c..a52a42847e 100644 --- a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs +++ b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs @@ -34,6 +34,7 @@ use tari_crypto::{ keys::{PublicKey as PublicKeyTrait, SecretKey}, tari_utilities::hex::Hex, }; +use tari_common_types::transaction::TxId; use crate::output_manager_service::{ error::{OutputManagerError, OutputManagerStorageError}, @@ -73,6 +74,7 @@ where TBackend: OutputManagerBackend + 'static pub async fn scan_and_recover_outputs( &mut self, outputs: Vec, + tx_id: TxId, ) -> Result, OutputManagerError> { let start = Instant::now(); let outputs_length = outputs.len(); @@ -133,7 +135,7 @@ where TBackend: OutputManagerBackend + 'static Some(proof), )?; let output_hex = db_output.commitment.to_hex(); - if let Err(e) = self.db.add_unspent_output(db_output).await { + if let Err(e) = self.db.add_unspent_output_with_tx_id(tx_id, db_output).await { match e { OutputManagerStorageError::DuplicateOutput => { info!( diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 3dfb07ecc1..1385666443 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -61,6 +61,7 @@ use tari_crypto::{ script::TariScript, tari_utilities::{hex::Hex, ByteArray}, }; +use tari_common_types::types::BlockHash; use tari_key_manager::cipher_seed::CipherSeed; use tari_service_framework::reply_channel; use tari_shutdown::ShutdownSignal; @@ -358,16 +359,16 @@ where OutputManagerRequest::GetPublicRewindKeys => Ok(OutputManagerResponse::PublicRewindKeys(Box::new( self.resources.master_key_manager.get_rewind_public_keys(), ))), - OutputManagerRequest::ScanForRecoverableOutputs(outputs) => StandardUtxoRecoverer::new( + OutputManagerRequest::ScanForRecoverableOutputs {outputs, tx_id} => StandardUtxoRecoverer::new( self.resources.master_key_manager.clone(), self.resources.factories.clone(), self.resources.db.clone(), ) - .scan_and_recover_outputs(outputs) + .scan_and_recover_outputs(outputs, tx_id) .await .map(OutputManagerResponse::RewoundOutputs), - OutputManagerRequest::ScanOutputs(outputs) => self - .scan_outputs_for_one_sided_payments(outputs) + OutputManagerRequest::ScanOutputs {outputs, tx_id} => self + .scan_outputs_for_one_sided_payments(outputs, tx_id) .await .map(OutputManagerResponse::ScanOutputs), OutputManagerRequest::AddKnownOneSidedPaymentScript(known_script) => self @@ -422,9 +423,22 @@ where } } - async fn get_output_status_by_tx_id(&self, tx_id: TxId) -> Result, OutputManagerError> { + async fn get_output_status_by_tx_id(&self, tx_id: TxId) -> Result<(Vec, Option, Option), OutputManagerError> { let outputs = self.resources.db.fetch_outputs_by_tx_id(tx_id).await?; - Ok(outputs.into_iter().map(|uo| uo.status).collect()) + let statuses = outputs.into_iter().map(|uo| uo.status).collect(); + let mined_heights = outputs.into_iter().map(|uo| uo.mined_height).collect(); + let mined_height: Option = if let Some(height) = mined_heights.iter().min() { + Some(*height) + } else { + None + }; + let block_hashes = outputs.into_iter().map(|uo| uo.mined_in_block).collect(); + let block_hash: Option = if let Some(hash) = block_hashes.iter().min() { + Some(*hash) + } else { + None + }; + Ok((statuses, mined_height, block_hash)) } async fn claim_sha_atomic_swap_with_hash( @@ -1893,6 +1907,7 @@ where async fn scan_outputs_for_one_sided_payments( &mut self, outputs: Vec, + tx_id: TxId, ) -> Result, OutputManagerError> { let known_one_sided_payment_scripts: Vec = self.resources.db.get_all_known_one_sided_payment_scripts().await?; @@ -1945,7 +1960,7 @@ where )?; let output_hex = output.commitment.to_hex(); - match self.resources.db.add_unspent_output(db_output).await { + match self.resources.db.add_unspent_output_with_tx_id(tx_id, db_output).await { Ok(_) => { rewound_outputs.push(rewound_output); }, diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 8a9fde45dd..35590eb022 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -23,7 +23,10 @@ use std::{collections::HashMap, fmt, fmt::Formatter, sync::Arc}; use aes_gcm::Aes256Gcm; -use tari_common_types::{transaction::TxId, types::PublicKey}; +use tari_common_types::{ + transaction::{ImportStatus, TxId}, + types::PublicKey, +}; use tari_comms::types::CommsPublicKey; use tari_core::transactions::{ tari_amount::MicroTari, @@ -72,7 +75,9 @@ pub enum TransactionServiceRequest { }, SendShaAtomicSwapTransaction(CommsPublicKey, MicroTari, MicroTari, String), CancelTransaction(TxId), - ImportUtxo(MicroTari, CommsPublicKey, String, Option), + ImportUtxoWithStatus { + amount: MicroTari, source_public_key: CommsPublicKey, message: String, maturity: Option, import_status: ImportStatus, tx_id: Option, current_height: Option + }, SubmitTransactionToSelf(TxId, Transaction, MicroTari, MicroTari, String), SetLowPowerMode, SetNormalPowerMode, @@ -123,12 +128,15 @@ impl fmt::Display for TransactionServiceRequest { f.write_str(&format!("SendShaAtomicSwapTransaction (to {}, {}, {})", k, v, msg)) }, Self::CancelTransaction(t) => f.write_str(&format!("CancelTransaction ({})", t)), - Self::ImportUtxo(v, k, msg, maturity) => f.write_str(&format!( - "ImportUtxo (from {}, {}, {} with maturity: {})", - k, - v, - msg, - maturity.unwrap_or(0) + Self::ImportUtxoWithStatus {amount, source_public_key, message, maturity, import_status, tx_id, current_height } => f.write_str(&format!( + "ImportUtxo (from {}, {}, {} with maturity {} and {:?} and {:?} and {:?})", + source_public_key, + amount, + message, + maturity.unwrap_or(0), + import_status, + tx_id, + current_height, )), Self::SubmitTransactionToSelf(tx_id, _, _, _, _) => f.write_str(&format!("SubmitTransaction ({})", tx_id)), Self::SetLowPowerMode => f.write_str("SetLowPowerMode "), @@ -189,6 +197,15 @@ pub enum TransactionEvent { TransactionCancelled(TxId, TxRejection), TransactionBroadcast(TxId), TransactionImported(TxId), + FauxTransactionUnconfirmed { + tx_id: TxId, + num_confirmations: u64, + is_valid: bool, + }, + FauxTransactionConfirmed { + tx_id: TxId, + is_valid: bool, + }, TransactionMined { tx_id: TxId, is_valid: bool, @@ -242,6 +259,20 @@ impl fmt::Display for TransactionEvent { TransactionEvent::TransactionImported(tx) => { write!(f, "TransactionImported for {}", tx) }, + TransactionEvent::FauxTransactionUnconfirmed { + tx_id, + num_confirmations, + is_valid, + } => { + write!( + f, + "TransactionScannedUnconfirmed for {} with num confirmations: {}. is_valid: {}", + tx_id, num_confirmations, is_valid + ) + }, + TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid } => { + write!(f, "TransactionScanned for {}. is_valid: {}", tx_id, is_valid) + }, TransactionEvent::TransactionMined { tx_id, is_valid } => { write!(f, "TransactionMined for {}. is_valid: {}", tx_id, is_valid) }, @@ -517,21 +548,27 @@ impl TransactionServiceHandle { } } - pub async fn import_utxo( + pub async fn import_utxo_with_status( &mut self, amount: MicroTari, source_public_key: CommsPublicKey, message: String, maturity: Option, + import_status: ImportStatus, + tx_id: Option, + current_height: Option ) -> Result { match self .handle - .call(TransactionServiceRequest::ImportUtxo( + .call(TransactionServiceRequest::ImportUtxoWithStatus { amount, source_public_key, message, maturity, - )) + import_status, + tx_id, + current_height, + }) .await?? { TransactionServiceResponse::UtxoImported(tx_id) => Ok(tx_id), diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs index d32efc4900..3d5f8189b5 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs @@ -451,6 +451,7 @@ where inbound_tx.timestamp, TransactionDirection::Inbound, None, + None, ); self.resources diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs index 4007eeaa77..c9e92680ac 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs @@ -511,6 +511,7 @@ where Utc::now().naive_utc(), TransactionDirection::Outbound, None, + None, ); self.resources diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs index b348c29758..afc59bbcb7 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs @@ -388,6 +388,8 @@ where mined_height: u64, num_confirmations: u64, ) -> Result<(), TransactionServiceProtocolError> { + let is_faux = + *status == TransactionStatus::FauxUnconfirmed || *status == TransactionStatus::FauxConfirmed; self.db .set_transaction_mined_height( tx_id, @@ -396,12 +398,23 @@ where mined_in_block.clone(), num_confirmations, num_confirmations >= self.config.num_confirmations_required, + is_faux, ) .await .for_protocol(self.operation_id.as_u64())?; if num_confirmations >= self.config.num_confirmations_required { - self.publish_event(TransactionEvent::TransactionMined { tx_id, is_valid: true }) + if is_faux { + self.publish_event(TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid: true }) + } else { + self.publish_event(TransactionEvent::TransactionMined { tx_id, is_valid: true }) + } + } else if is_faux { + self.publish_event(TransactionEvent::FauxTransactionUnconfirmed { + tx_id, + num_confirmations, + is_valid: true, + }) } else { self.publish_event(TransactionEvent::TransactionMinedUnconfirmed { tx_id, @@ -441,6 +454,7 @@ where mined_in_block.clone(), num_confirmations, num_confirmations >= self.config.num_confirmations_required, + false, ) .await .for_protocol(self.operation_id.as_u64())?; @@ -465,8 +479,10 @@ where tx_id: TxId, status: &TransactionStatus, ) -> Result<(), TransactionServiceProtocolError> { + let is_faux = + *status == TransactionStatus::FauxUnconfirmed || *status == TransactionStatus::FauxConfirmed; self.db - .set_transaction_as_unmined(tx_id) + .set_transaction_as_unmined(tx_id, is_faux) .await .for_protocol(self.operation_id.as_u64())?; diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 7ce9c6a77b..757a881b11 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -34,7 +34,7 @@ use log::*; use rand::rngs::OsRng; use sha2::Sha256; use tari_common_types::{ - transaction::{TransactionDirection, TransactionStatus, TxId}, + transaction::{ImportStatus, TransactionConversionError, TransactionDirection, TransactionStatus, TxId}, types::{PrivateKey, PublicKey}, }; use tari_comms::{peer_manager::NodeIdentity, types::CommsPublicKey}; @@ -68,6 +68,7 @@ use tokio::{ sync::{mpsc, mpsc::Sender, oneshot}, task::JoinHandle, }; +use tari_common_types::chain_metadata::ChainMetadata; use crate::{ base_node_service::handle::{BaseNodeEvent, BaseNodeServiceHandle}, @@ -93,7 +94,7 @@ use crate::{ models::CompletedTransaction, }, tasks::{ - check_imported_transaction_status::check_imported_transactions, + check_faux_transaction_status::check_faux_transactions, send_finalized_transaction::send_finalized_transaction_message, send_transaction_cancelled::send_transaction_cancelled_message, send_transaction_reply::send_transaction_reply, @@ -644,8 +645,16 @@ where TransactionServiceRequest::GetAnyTransaction(tx_id) => Ok(TransactionServiceResponse::AnyTransaction( Box::new(self.db.get_any_transaction(tx_id).await?), )), - TransactionServiceRequest::ImportUtxo(value, source_public_key, message, maturity) => self - .add_utxo_import_transaction(value, source_public_key, message, maturity) + TransactionServiceRequest::ImportUtxoWithStatus { + amount, + source_public_key, + message, + maturity, + import_status, + tx_id, + current_height, + } => self + .add_utxo_import_transaction_with_status(amount, source_public_key, message, maturity, import_status, tx_id, current_height) .await .map(TransactionServiceResponse::UtxoImported), TransactionServiceRequest::SubmitTransactionToSelf(tx_id, tx, fee, amount, message) => self @@ -748,7 +757,12 @@ where if let OutputManagerEvent::TxoValidationSuccess(_) = (*event).clone() { let db = self.db.clone(); let output_manager_handle = self.output_manager_service.clone(); - tokio::spawn(check_imported_transactions(output_manager_handle, db)); + let metadata = self.wallet_db.get_chain_metadata().await?; + let tip_height = match metadata { + None => 0u64, + Some(v) => v.height_of_longest_chain(), + }; + tokio::spawn(check_faux_transactions(output_manager_handle, db, tip_height)); } } @@ -812,6 +826,7 @@ where Utc::now().naive_utc(), TransactionDirection::Inbound, None, + None, ), ) .await?; @@ -1016,6 +1031,7 @@ where Utc::now().naive_utc(), TransactionDirection::Outbound, None, + None, ), ) .await?; @@ -1159,6 +1175,7 @@ where Utc::now().naive_utc(), TransactionDirection::Outbound, None, + None, ), ) .await?; @@ -2023,35 +2040,55 @@ where } /// Add a completed transaction to the Transaction Manager to record directly importing a spendable UTXO. - pub async fn add_utxo_import_transaction( + pub async fn add_utxo_import_transaction_with_status( &mut self, value: MicroTari, source_public_key: CommsPublicKey, message: String, maturity: Option, + import_status: ImportStatus, + tx_id: Option, + current_height: Option, ) -> Result { - let tx_id = TxId::new_random(); + let tx_id = if let Some(id) = tx_id { + id + } else { + TxId::new_random(); + }; self.db - .add_utxo_import_transaction( + .add_utxo_import_transaction_with_status( tx_id, value, source_public_key, self.node_identity.public_key().clone(), message, maturity, + import_status.clone(), + current_height, ) .await?; - let _ = self - .event_publisher - .send(Arc::new(TransactionEvent::TransactionImported(tx_id))) - .map_err(|e| { - trace!( - target: LOG_TARGET, - "Error sending event, usually because there are no subscribers: {:?}", - e - ); + let transaction_event = match import_status { + ImportStatus::Invalid => { + return Err(TransactionServiceError::ConversionError(TransactionConversionError { + code: i32::MAX, + })) + }, + ImportStatus::Imported => TransactionEvent::TransactionImported(tx_id), + ImportStatus::ScannedUnconfirmed => TransactionEvent::FauxTransactionUnconfirmed { + tx_id, + num_confirmations: 0, + is_valid: true, + }, + ImportStatus::ScannedConfirmed => TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid: true }, + }; + let _ = self.event_publisher.send(Arc::new(transaction_event)).map_err(|e| { + trace!( + target: LOG_TARGET, + "Error sending event, usually because there are no subscribers: {:?}", e - }); + ); + e + }); Ok(tx_id) } @@ -2105,6 +2142,7 @@ where Utc::now().naive_utc(), TransactionDirection::Inbound, None, + None, ), ) .await?; @@ -2165,6 +2203,7 @@ where Utc::now().naive_utc(), TransactionDirection::Inbound, Some(block_height), + None, ), ) .await?; diff --git a/base_layer/wallet/src/transaction_service/storage/database.rs b/base_layer/wallet/src/transaction_service/storage/database.rs index aede6aad88..5708153e9a 100644 --- a/base_layer/wallet/src/transaction_service/storage/database.rs +++ b/base_layer/wallet/src/transaction_service/storage/database.rs @@ -22,6 +22,7 @@ use std::{ collections::HashMap, + convert::TryFrom, fmt, fmt::{Display, Error, Formatter}, sync::Arc, @@ -31,7 +32,7 @@ use aes_gcm::Aes256Gcm; use chrono::Utc; use log::*; use tari_common_types::{ - transaction::{TransactionDirection, TransactionStatus, TxId}, + transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, types::{BlindingFactor, BlockHash}, }; use tari_comms::types::CommsPublicKey; @@ -132,9 +133,10 @@ pub trait TransactionBackend: Send + Sync + Clone { mined_in_block: BlockHash, num_confirmations: u64, is_confirmed: bool, + is_faux: bool, ) -> Result<(), TransactionStorageError>; /// Clears the mined block and height of a transaction - fn set_transaction_as_unmined(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; + fn set_transaction_as_unmined(&self, tx_id: TxId, is_faux: bool) -> Result<(), TransactionStorageError>; /// Reset optional 'mined height' and 'mined in block' fields to nothing fn mark_all_transactions_as_unvalidated(&self) -> Result<(), TransactionStorageError>; /// Light weight method to retrieve pertinent transaction sender info for all pending inbound transactions @@ -142,6 +144,8 @@ pub trait TransactionBackend: Send + Sync + Clone { &self, ) -> Result, TransactionStorageError>; fn fetch_imported_transactions(&self) -> Result, TransactionStorageError>; + fn fetch_unconfirmed_faux_transactions(&self) -> Result, TransactionStorageError>; + fn fetch_confirmed_faux_transactions_from_height(&self, height: u64) -> Result, TransactionStorageError>; } #[derive(Clone, PartialEq)] @@ -442,6 +446,27 @@ where T: TransactionBackend + 'static Ok(t) } + pub async fn get_unconfirmed_faux_transactions( + &self, + ) -> Result, TransactionStorageError> { + let db_clone = self.db.clone(); + let t = tokio::task::spawn_blocking(move || db_clone.fetch_unconfirmed_faux_transactions()) + .await + .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(t) + } + + pub async fn get_confirmed_faux_transactions_from_height( + &self, + height: u64, + ) -> Result, TransactionStorageError> { + let db_clone = self.db.clone(); + let t = tokio::task::spawn_blocking(move || db_clone.fetch_confirmed_faux_transactions_from_height(height)) + .await + .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(t) + } + pub async fn fetch_last_mined_transaction(&self) -> Result, TransactionStorageError> { self.db.fetch_last_mined_transaction() } @@ -702,7 +727,8 @@ where T: TransactionBackend + 'static .and_then(|inner_result| inner_result) } - pub async fn add_utxo_import_transaction( + /// Faux transaction added to the database with imported status + pub async fn add_utxo_import_transaction_with_status( &self, tx_id: TxId, amount: MicroTari, @@ -710,6 +736,8 @@ where T: TransactionBackend + 'static comms_public_key: CommsPublicKey, message: String, maturity: Option, + import_status: ImportStatus, + current_height: Option, ) -> Result<(), TransactionStorageError> { let transaction = CompletedTransaction::new( tx_id, @@ -724,11 +752,12 @@ where T: TransactionBackend + 'static BlindingFactor::default(), BlindingFactor::default(), ), - TransactionStatus::Imported, + TransactionStatus::try_from(import_status)?, message, Utc::now().naive_utc(), TransactionDirection::Inbound, maturity, + current_height, ); let db_clone = self.db.clone(); @@ -792,9 +821,13 @@ where T: TransactionBackend + 'static Ok(()) } - pub async fn set_transaction_as_unmined(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { + pub async fn set_transaction_as_unmined( + &self, + tx_id: TxId, + is_faux: bool, + ) -> Result<(), TransactionStorageError> { let db_clone = self.db.clone(); - tokio::task::spawn_blocking(move || db_clone.set_transaction_as_unmined(tx_id)) + tokio::task::spawn_blocking(move || db_clone.set_transaction_as_unmined(tx_id, is_faux)) .await .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(()) @@ -816,6 +849,7 @@ where T: TransactionBackend + 'static mined_in_block: BlockHash, num_confirmations: u64, is_confirmed: bool, + is_faux: bool, ) -> Result<(), TransactionStorageError> { let db_clone = self.db.clone(); tokio::task::spawn_blocking(move || { @@ -826,6 +860,7 @@ where T: TransactionBackend + 'static mined_in_block, num_confirmations, is_confirmed, + is_faux, ) }) .await diff --git a/base_layer/wallet/src/transaction_service/storage/models.rs b/base_layer/wallet/src/transaction_service/storage/models.rs index d0c0702571..e6377f450a 100644 --- a/base_layer/wallet/src/transaction_service/storage/models.rs +++ b/base_layer/wallet/src/transaction_service/storage/models.rs @@ -159,6 +159,8 @@ impl CompletedTransaction { timestamp: NaiveDateTime, direction: TransactionDirection, coinbase_block_height: Option, + mined_height: Option, + ) -> Self { let transaction_signature = if let Some(excess_sig) = transaction.first_kernel_excess_sig() { excess_sig.clone() @@ -183,7 +185,7 @@ impl CompletedTransaction { valid: true, transaction_signature, confirmations: None, - mined_height: None, + mined_height, mined_in_block: None, } } diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index 8dc318d5e8..8b70f35f11 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -1009,6 +1009,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { mined_in_block: BlockHash, num_confirmations: u64, is_confirmed: bool, + is_faux: bool, ) -> Result<(), TransactionStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; @@ -1022,6 +1023,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { num_confirmations, is_confirmed, &conn, + is_faux, )?; }, Err(TransactionStorageError::DieselError(DieselError::NotFound)) => { @@ -1048,6 +1050,8 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); let tx = completed_transactions::table + // Note: Check 'mined_in_block' as well as 'mined_height' is populated for faux transactions before it is confirmed + .filter(completed_transactions::mined_in_block.is_not_null()) .filter(completed_transactions::mined_height.is_not_null()) .filter(completed_transactions::mined_height.gt(0)) .order_by(completed_transactions::mined_height.desc()) @@ -1072,6 +1076,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Ok(result) } + // This method returns completed but unconfirmed transactions that were not imported fn fetch_unconfirmed_transactions_info(&self) -> Result, TransactionStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; @@ -1160,13 +1165,13 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Ok(()) } - fn set_transaction_as_unmined(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { + fn set_transaction_as_unmined(&self, tx_id: TxId, is_faux: bool) -> Result<(), TransactionStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); match CompletedTransactionSql::find(tx_id, &conn) { Ok(v) => { - v.set_as_unmined(&conn)?; + v.set_as_unmined(&conn, is_faux)?; }, Err(TransactionStorageError::DieselError(DieselError::NotFound)) => { return Err(TransactionStorageError::ValueNotFound(DbKey::CompletedTransaction( @@ -1226,6 +1231,32 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { }) .collect::, TransactionStorageError>>() } + + fn fetch_unconfirmed_faux_transactions(&self) -> Result, TransactionStorageError> { + let conn = self.database_connection.get_pooled_connection()?; + CompletedTransactionSql::index_by_status_and_cancelled(TransactionStatus::FauxUnconfirmed, false, &conn)? + .into_iter() + .map(|mut ct: CompletedTransactionSql| { + if let Err(e) = self.decrypt_if_necessary(&mut ct) { + return Err(e); + } + CompletedTransaction::try_from(ct).map_err(TransactionStorageError::from) + }) + .collect::, TransactionStorageError>>() + } + + fn fetch_confirmed_faux_transactions_from_height(&self, height: u64) -> Result, TransactionStorageError> { + let conn = self.database_connection.get_pooled_connection()?; + CompletedTransactionSql::index_by_status_and_cancelled_from_block_height(TransactionStatus::FauxConfirmed, false, height as i64, &conn)? + .into_iter() + .map(|mut ct: CompletedTransactionSql| { + if let Err(e) = self.decrypt_if_necessary(&mut ct) { + return Err(e); + } + CompletedTransaction::try_from(ct).map_err(TransactionStorageError::from) + }) + .collect::, TransactionStorageError>>() + } } #[derive(Debug, PartialEq)] @@ -1674,6 +1705,19 @@ impl CompletedTransactionSql { .load::(conn)?) } + pub fn index_by_status_and_cancelled_from_block_height( + status: TransactionStatus, + cancelled: bool, + block_height: i64, + conn: &SqliteConnection, + ) -> Result, TransactionStorageError> { + Ok(completed_transactions::table + .filter(completed_transactions::cancelled.eq(cancelled as i32)) + .filter(completed_transactions::status.eq(status as i32)) + .filter(completed_transactions::coinbase_block_height.ge(block_height)) + .load::(conn)?) + } + pub fn index_coinbase_at_block_height( block_height: i64, conn: &SqliteConnection, @@ -1750,9 +1794,11 @@ impl CompletedTransactionSql { Ok(()) } - pub fn set_as_unmined(&self, conn: &SqliteConnection) -> Result<(), TransactionStorageError> { + pub fn set_as_unmined(&self, conn: &SqliteConnection, is_faux: bool) -> Result<(), TransactionStorageError> { let status = if self.coinbase_block_height.is_some() { Some(TransactionStatus::Coinbase as i32) + } else if is_faux { + Some(TransactionStatus::FauxUnconfirmed as i32) } else if self.status == TransactionStatus::Broadcast as i32 { Some(TransactionStatus::Broadcast as i32) } else { @@ -1798,11 +1844,18 @@ impl CompletedTransactionSql { num_confirmations: u64, is_confirmed: bool, conn: &SqliteConnection, + is_faux: bool, ) -> Result<(), TransactionStorageError> { let status = if self.coinbase_block_height.is_some() && !is_valid { TransactionStatus::Coinbase as i32 } else if is_confirmed { - TransactionStatus::MinedConfirmed as i32 + if is_faux { + TransactionStatus::FauxConfirmed as i32 + } else { + TransactionStatus::MinedConfirmed as i32 + } + } else if is_faux { + TransactionStatus::FauxUnconfirmed as i32 } else { TransactionStatus::MinedUnconfirmed as i32 }; @@ -1983,7 +2036,7 @@ pub struct UnconfirmedTransactionInfoSql { } impl UnconfirmedTransactionInfoSql { - /// This method returns completed but unconfirmed transactions + /// This method returns completed but unconfirmed transactions that were not imported or scanned pub fn fetch_unconfirmed_transactions_info( conn: &SqliteConnection, ) -> Result, TransactionStorageError> { @@ -1999,6 +2052,8 @@ impl UnconfirmedTransactionInfoSql { .filter( completed_transactions::status .ne(TransactionStatus::Imported as i32) + .ne(TransactionStatus::FauxUnconfirmed as i32) + .ne(TransactionStatus::FauxConfirmed as i32) .and( completed_transactions::mined_height .is_null() diff --git a/base_layer/wallet/src/transaction_service/tasks/check_imported_transaction_status.rs b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs similarity index 55% rename from base_layer/wallet/src/transaction_service/tasks/check_imported_transaction_status.rs rename to base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs index fe1ea992b2..50e0d1ec8f 100644 --- a/base_layer/wallet/src/transaction_service/tasks/check_imported_transaction_status.rs +++ b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs @@ -29,23 +29,51 @@ use crate::{ storage::database::{TransactionBackend, TransactionDatabase}, }, }; +use crate::transaction_service::storage::models::CompletedTransaction; const LOG_TARGET: &str = "wallet::transaction_service::service"; -pub async fn check_imported_transactions( +pub async fn check_faux_transactions( mut output_manager: OutputManagerHandle, db: TransactionDatabase, + tip_height: u64, ) { - let imported_transactions = match db.get_imported_transactions().await { + let mut all_faux_transactions: Vec; + let mut imported_transactions = match db.get_imported_transactions().await { Ok(txs) => txs, Err(e) => { error!(target: LOG_TARGET, "Problem retrieving imported transactions: {}", e); return; }, }; + all_faux_transactions.append(&mut imported_transactions); + let mut unconfirmed_faux = match db.get_unconfirmed_faux_transactions().await { + Ok(txs) => txs, + Err(e) => { + error!(target: LOG_TARGET, "Problem retrieving unconfirmed faux transactions: {}", e); + return; + }, + }; + all_faux_transactions.append(&mut unconfirmed_faux); + // Reorged faux transactions cannot be detected by excess signature, thus use last known confirmed transaction + // height or current tip height with safety margin to determine if these should be returned + let height_with_margin = tip_height.checked_sub(100).unwrap_or(0); + let check_height = if let Some(tx) = db.fetch_last_mined_transaction().await? { + tx.mined_height.unwrap_or(height_with_margin) + } else { + height_with_margin + }; + let mut confirmed_faux = match db.get_confirmed_faux_transactions_from_height(check_height).await { + Ok(txs) => txs, + Err(e) => { + error!(target: LOG_TARGET, "Problem retrieving confirmed faux transactions: {}", e); + return; + }, + }; + all_faux_transactions.append(&mut confirmed_faux); - for tx in imported_transactions.into_iter() { - let status = match output_manager.get_output_statuses_by_tx_id(tx.tx_id).await { + for tx in all_faux_transactions.into_iter() { + let (status, mined_height, block_hash) = match output_manager.get_output_statuses_by_tx_id(tx.tx_id).await { Ok(s) => s, Err(e) => { error!(target: LOG_TARGET, "Problem retrieving output statuses: {}", e); @@ -57,13 +85,26 @@ pub async fn check_imported_transactions target: LOG_TARGET, "Faux Transaction (TxId: {}) updated to confirmed", tx.tx_id ); + let mined_height= if let Some(height) = mined_height { + *height + } else { + 0 + }; + let mined_in_block= if let Some(hash) = block_hash { + *gash + } else { + vec![0u8; 32] + }; + let confirmations = tip_height.checked_sub(mined_height).unwrap_or(0); + let is_confirmed = confirmations >= TransactionServiceConfig::default().num_confirmations_required; if let Err(e) = db .set_transaction_mined_height( tx.tx_id, true, - 0, - vec![0u8; 32], - TransactionServiceConfig::default().num_confirmations_required, + mined_height, + mined_in_block, + tip_height - mined_heigh, + is_confirmed, true, ) .await diff --git a/base_layer/wallet/src/transaction_service/tasks/mod.rs b/base_layer/wallet/src/transaction_service/tasks/mod.rs index dce38a144c..292932156e 100644 --- a/base_layer/wallet/src/transaction_service/tasks/mod.rs +++ b/base_layer/wallet/src/transaction_service/tasks/mod.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod check_imported_transaction_status; +pub mod check_faux_transaction_status; pub mod send_finalized_transaction; pub mod send_transaction_cancelled; pub mod send_transaction_reply; diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index a56bb36e4d..0625cfb91d 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -28,7 +28,10 @@ use std::{ use chrono::Utc; use futures::StreamExt; use log::*; -use tari_common_types::{transaction::TxId, types::HashOutput}; +use tari_common_types::{ + transaction::{ImportStatus, TxId}, + types::HashOutput, +}; use tari_comms::{peer_manager::NodeId, types::CommsPublicKey, PeerConnection}; use tari_core::{ base_node::rpc::BaseNodeWalletRpcClient, @@ -427,10 +430,10 @@ where TBackend: WalletBackend + 'static total_scanned += outputs.len(); let start = Instant::now(); - let found_outputs = self.scan_for_outputs(outputs).await?; + let (tx_id, found_outputs) = self.scan_for_outputs(outputs).await?; scan_for_outputs_profiling.push(start.elapsed()); - let (count, amount) = self.import_utxos_to_transaction_service(found_outputs).await?; + let (count, amount) = self.import_utxos_to_transaction_service(found_outputs, tx_id, current_height).await?; self.resources .db @@ -481,17 +484,24 @@ where TBackend: WalletBackend + 'static async fn scan_for_outputs( &mut self, outputs: Vec, - ) -> Result, UtxoScannerError> { - let mut found_outputs: Vec<(UnblindedOutput, String)> = Vec::new(); + ) -> Result<(TxId, Vec<(UnblindedOutput, String, ImportStatus)>), UtxoScannerError> { + let mut found_outputs: Vec<(UnblindedOutput, String, ImportStatus)> = Vec::new(); + let tx_id = TxId::new_random(); if self.mode == UtxoScannerMode::Recovery { found_outputs.append( &mut self .resources .output_manager_service - .scan_for_recoverable_outputs(outputs.clone()) + .scan_for_recoverable_outputs(outputs.clone(), tx_id) .await? .into_iter() - .map(|v| (v, format!("Recovered on {}.", Utc::now().naive_utc()))) + .map(|v| { + ( + v, + format!("Recovered on {}.", Utc::now().naive_utc()), + ImportStatus::Imported, + ) + }) .collect(), ); }; @@ -499,23 +509,26 @@ where TBackend: WalletBackend + 'static &mut self .resources .output_manager_service - .scan_outputs_for_one_sided_payments(outputs.clone()) + .scan_outputs_for_one_sided_payments(outputs.clone(), tx_id) .await? .into_iter() .map(|v| { ( v, format!("Detected one-sided transaction on {}.", Utc::now().naive_utc()), + ImportStatus::ScannedUnconfirmed, ) }) .collect(), ); - Ok(found_outputs) + Ok((tx_id, found_outputs)) } async fn import_utxos_to_transaction_service( &mut self, - utxos: Vec<(UnblindedOutput, String)>, + utxos: Vec<(UnblindedOutput, String, ImportStatus)>, + tx_id: TxId, + current_height: u64, ) -> Result<(u64, MicroTari), UtxoScannerError> { let mut num_recovered = 0u64; let mut total_amount = MicroTari::from(0); @@ -523,7 +536,7 @@ where TBackend: WalletBackend + 'static for uo in utxos { match self - .import_unblinded_utxo_to_transaction_service(uo.0.clone(), &source_public_key, uo.1) + .import_unblinded_utxo_to_transaction_service(uo.0.clone(), &source_public_key, uo.1, uo.2, tx_id, current_height) .await { Ok(_) => { @@ -572,26 +585,33 @@ where TBackend: WalletBackend + 'static unblinded_output: UnblindedOutput, source_public_key: &CommsPublicKey, message: String, + import_status: ImportStatus, + tx_id: TxId, + current_height: u64, ) -> Result { let tx_id = self .resources .transaction_service - .import_utxo( + .import_utxo_with_status( unblinded_output.value, source_public_key.clone(), message, Some(unblinded_output.features.maturity), + import_status.clone(), + Some(tx_id), + Some(current_height) ) .await?; info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet", + "UTXO (Commitment: {}) imported into wallet as '{:?}'", unblinded_output .as_transaction_input(&self.resources.factories.commitment)? .commitment() .map_err(WalletError::TransactionError)? - .to_hex() + .to_hex(), + import_status, ); Ok(tx_id) diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index 46801d53ab..fbb0cf6197 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -26,7 +26,7 @@ use digest::Digest; use log::*; use tari_common::configuration::bootstrap::ApplicationType; use tari_common_types::{ - transaction::TxId, + transaction::{ImportStatus, TxId}, types::{ComSignature, PrivateKey, PublicKey}, }; use tari_comms::{ @@ -385,6 +385,7 @@ where sender_offset_public_key: &PublicKey, script_lock_height: u64, covenant: Covenant, + import_status: ImportStatus, ) -> Result { let unblinded_output = UnblindedOutput::new_current_version( amount, @@ -401,7 +402,15 @@ where let tx_id = self .transaction_service - .import_utxo(amount, source_public_key.clone(), message, Some(features.maturity)) + .import_utxo_with_status( + amount, + source_public_key.clone(), + message, + Some(features.maturity), + import_status.clone(), + None, + None, + ) .await?; let commitment_hex = unblinded_output @@ -416,7 +425,7 @@ where info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet", commitment_hex + "UTXO (Commitment: {}) imported into wallet as '{:?}'", commitment_hex, import_status ); Ok(tx_id) @@ -430,14 +439,18 @@ where unblinded_output: UnblindedOutput, source_public_key: &CommsPublicKey, message: String, + import_status: ImportStatus, ) -> Result { let tx_id = self .transaction_service - .import_utxo( + .import_utxo_with_status( unblinded_output.value, source_public_key.clone(), message, Some(unblinded_output.features.maturity), + import_status.clone(), + None, + None, ) .await?; @@ -447,12 +460,13 @@ where info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet", + "UTXO (Commitment: {}) imported into wallet as '{:?}'", unblinded_output .as_transaction_input(&self.factories.commitment)? .commitment() .map_err(WalletError::TransactionError)? - .to_hex() + .to_hex(), + import_status ); Ok(tx_id) diff --git a/base_layer/wallet/tests/support/output_manager_service_mock.rs b/base_layer/wallet/tests/support/output_manager_service_mock.rs index a409dd2c4c..3ae2ebb103 100644 --- a/base_layer/wallet/tests/support/output_manager_service_mock.rs +++ b/base_layer/wallet/tests/support/output_manager_service_mock.rs @@ -96,7 +96,7 @@ impl OutputManagerServiceMock { ) { info!(target: LOG_TARGET, "Handling Request: {}", request); match request { - OutputManagerRequest::ScanForRecoverableOutputs(requested_outputs) => { + OutputManagerRequest::ScanForRecoverableOutputs {outputs: _to, tx_id: _tx_id} => { let lock = acquire_lock!(self.state.recoverable_outputs); let outputs = (*lock) .clone() @@ -117,7 +117,7 @@ impl OutputManagerServiceMock { e }); }, - OutputManagerRequest::ScanOutputs(_to) => { + OutputManagerRequest::ScanOutputs {outputs: _to, tx_id: _tx_id} => { let lock = acquire_lock!(self.state.one_sided_payments); let outputs = (*lock).clone(); let _ = reply_tx diff --git a/base_layer/wallet/tests/support/transaction_service_mock.rs b/base_layer/wallet/tests/support/transaction_service_mock.rs index cce11b5707..40949b8cf8 100644 --- a/base_layer/wallet/tests/support/transaction_service_mock.rs +++ b/base_layer/wallet/tests/support/transaction_service_mock.rs @@ -95,7 +95,7 @@ impl TransactionServiceMock { info!(target: LOG_TARGET, "Handling Request: {}", request); match request { - TransactionServiceRequest::ImportUtxo(_, _, _, _) => { + TransactionServiceRequest::ImportUtxoWithStatus(_, _, _, _, _, , _) => { let _ = reply_tx .send(Ok(TransactionServiceResponse::UtxoImported(TxId::from(42)))) .map_err(|e| { diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index b145f8ef8c..c988bfa6fd 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -38,7 +38,7 @@ use prost::Message; use rand::rngs::OsRng; use tari_common_types::{ chain_metadata::ChainMetadata, - transaction::{TransactionDirection, TransactionStatus, TxId}, + transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, types::{PrivateKey, PublicKey, Signature}, }; use tari_comms::{ @@ -908,7 +908,7 @@ fn recover_one_sided_transaction() { let outputs = completed_tx.transaction.body.outputs().clone(); let unblinded = bob_oms - .scan_outputs_for_one_sided_payments(outputs.clone()) + .scan_outputs_for_one_sided_payments(outputs.clone(), TxId::new_random()) .await .unwrap(); // Bob should be able to claim 1 output. @@ -916,7 +916,7 @@ fn recover_one_sided_transaction() { assert_eq!(value, unblinded[0].value); // Should ignore already existing outputs - let unblinded = bob_oms.scan_outputs_for_one_sided_payments(outputs).await.unwrap(); + let unblinded = bob_oms.scan_outputs_for_one_sided_payments(outputs, TxId::new_random()).await.unwrap(); assert!(unblinded.is_empty()); }); } @@ -5394,52 +5394,115 @@ fn test_update_faux_tx_on_oms_validation() { let mut alice_ts_interface = setup_transaction_service_no_comms(&mut runtime, factories.clone(), connection, None); - let tx_id = runtime - .block_on(alice_ts_interface.transaction_service_handle.import_utxo( + let tx_id_1 = runtime + .block_on(alice_ts_interface.transaction_service_handle.import_utxo_with_status( MicroTari::from(10000), alice_ts_interface.base_node_identity.public_key().clone(), "blah".to_string(), None, + ImportStatus::Imported, + None, + None, )) .unwrap(); - - let (_ti, uo) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment); - runtime - .block_on( - alice_ts_interface - .output_manager_service_handle - .add_output_with_tx_id(tx_id, uo, None), - ) + let tx_id_2 = runtime + .block_on(alice_ts_interface.transaction_service_handle.import_utxo_with_status( + MicroTari::from(20000), + alice_ts_interface.base_node_identity.public_key().clone(), + "one-sided 1".to_string(), + None, + ImportStatus::ScannedUnconfirmed, + None, + None, + )) .unwrap(); - - let transaction = runtime - .block_on(alice_ts_interface.transaction_service_handle.get_any_transaction(tx_id)) - .unwrap() + let tx_id_3 = runtime + .block_on(alice_ts_interface.transaction_service_handle.import_utxo_with_status( + MicroTari::from(30000), + alice_ts_interface.base_node_identity.public_key().clone(), + "one-sided 2".to_string(), + None, + ImportStatus::ScannedConfirmed, + None, + None, + )) .unwrap(); - if let WalletTransaction::Completed(tx) = transaction { - assert_eq!(tx.status, TransactionStatus::Imported); - } else { - panic!("Should find a complete transaction"); + + let (_ti, uo_1) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment); + let (_ti, uo_2) = make_input(&mut OsRng.clone(), MicroTari::from(20000), &factories.commitment); + let (_ti, uo_3) = make_input(&mut OsRng.clone(), MicroTari::from(30000), &factories.commitment); + for (tx_id, uo) in [(tx_id_1, uo_1), (tx_id_2, uo_2), (tx_id_3, uo_3)] { + runtime + .block_on( + alice_ts_interface + .output_manager_service_handle + .add_output_with_tx_id(tx_id, uo, None), + ) + .unwrap(); } + for tx_id in [tx_id_1, tx_id_2, tx_id_3] { + let transaction = runtime + .block_on(alice_ts_interface.transaction_service_handle.get_any_transaction(tx_id)) + .unwrap() + .unwrap(); + if tx_id == tx_id_1 { + if let WalletTransaction::Completed(tx) = &transaction { + assert_eq!(tx.status, TransactionStatus::Imported); + } else { + panic!("Should find a complete Imported transaction"); + } + } + if tx_id == tx_id_2 { + if let WalletTransaction::Completed(tx) = &transaction { + assert_eq!(tx.status, TransactionStatus::FauxUnconfirmed); + } else { + panic!("Should find a complete ScannedUnconfirmed transaction"); + } + } + if tx_id == tx_id_3 { + if let WalletTransaction::Completed(tx) = &transaction { + assert_eq!(tx.status, TransactionStatus::FauxConfirmed); + } else { + panic!("Should find a complete ScannedConfirmed transaction"); + } + } + } + + // This will only change the status of the imported transaction alice_ts_interface .output_manager_service_event_publisher - .send(Arc::new(OutputManagerEvent::TxoValidationSuccess(1))) + .send(Arc::new(OutputManagerEvent::TxoValidationSuccess(1u64))) .unwrap(); - let mut found = false; + let mut found_imported = false; + let mut found_scanned_unconfirmed = false; + let mut found_scanned_confirmed = false; for _ in 0..20 { runtime.block_on(async { sleep(Duration::from_secs(1)).await }); - let transaction = runtime - .block_on(alice_ts_interface.transaction_service_handle.get_any_transaction(tx_id)) - .unwrap() - .unwrap(); - if let WalletTransaction::Completed(tx) = transaction { - if tx.status == TransactionStatus::MinedConfirmed { - found = true; - break; + for tx_id in [tx_id_1, tx_id_2, tx_id_3] { + let transaction = runtime + .block_on(alice_ts_interface.transaction_service_handle.get_any_transaction(tx_id)) + .unwrap() + .unwrap(); + if let WalletTransaction::Completed(tx) = transaction { + if tx_id == tx_id_1 && tx.status == TransactionStatus::MinedConfirmed { + found_imported = true; + } + if tx_id == tx_id_2 && tx.status == TransactionStatus::FauxUnconfirmed { + found_scanned_unconfirmed = true; + } + if tx_id == tx_id_3 && tx.status == TransactionStatus::FauxConfirmed { + found_scanned_confirmed = true; + } } } + if found_imported && found_scanned_unconfirmed && found_scanned_confirmed { + break; + } } - assert!(found, "Should have found the updated status"); + assert!( + found_imported && found_scanned_unconfirmed && found_scanned_confirmed, + "Should have found the updated statuses" + ); } diff --git a/base_layer/wallet/tests/transaction_service_tests/storage.rs b/base_layer/wallet/tests/transaction_service_tests/storage.rs index 72b2cf2052..1bca65ec66 100644 --- a/base_layer/wallet/tests/transaction_service_tests/storage.rs +++ b/base_layer/wallet/tests/transaction_service_tests/storage.rs @@ -323,7 +323,7 @@ pub fn test_db_backend(backend: T) { assert!(runtime.block_on(db.fetch_last_mined_transaction()).unwrap().is_none()); runtime - .block_on(db.set_transaction_mined_height(completed_txs[0].tx_id, true, 10, [0u8; 16].to_vec(), 5, true)) + .block_on(db.set_transaction_mined_height(completed_txs[0].tx_id, true, 10, [0u8; 16].to_vec(), 5, true, false)) .unwrap(); assert_eq!( @@ -551,7 +551,7 @@ pub fn test_db_backend(backend: T) { assert_eq!(unmined_txs.len(), 4); runtime - .block_on(db.set_transaction_as_unmined(completed_txs[0].tx_id)) + .block_on(db.set_transaction_as_unmined(completed_txs[0].tx_id, false)) .unwrap(); let unmined_txs = runtime.block_on(db.fetch_unconfirmed_transactions_info()).unwrap(); @@ -605,14 +605,15 @@ async fn import_tx_and_read_it_from_db() { Vec::new(), Vec::new(), Vec::new(), - PrivateKey::default(), - PrivateKey::default(), + PrivateKey::random(&mut OsRng), + PrivateKey::random(&mut OsRng), ), TransactionStatus::Imported, "message".to_string(), Utc::now().naive_utc(), TransactionDirection::Inbound, Some(0), + Some(5), ); sqlite_db @@ -622,8 +623,76 @@ async fn import_tx_and_read_it_from_db() { ))) .unwrap(); - let db_tx = sqlite_db.fetch_imported_transactions().unwrap(); + let transaction = CompletedTransaction::new( + TxId::from(2), + PublicKey::default(), + PublicKey::default(), + MicroTari::from(100000), + MicroTari::from(0), + Transaction::new( + Vec::new(), + Vec::new(), + Vec::new(), + PrivateKey::random(&mut OsRng), + PrivateKey::random(&mut OsRng), + ), + TransactionStatus::FauxUnconfirmed, + "message".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + Some(0), + Some(6), + ); + + sqlite_db + .write(WriteOperation::Insert(DbKeyValuePair::CompletedTransaction( + TxId::from(2), + Box::new(transaction), + ))) + .unwrap(); + let transaction = CompletedTransaction::new( + TxId::from(3), + PublicKey::default(), + PublicKey::default(), + MicroTari::from(100000), + MicroTari::from(0), + Transaction::new( + Vec::new(), + Vec::new(), + Vec::new(), + PrivateKey::random(&mut OsRng), + PrivateKey::random(&mut OsRng), + ), + TransactionStatus::FauxConfirmed, + "message".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + Some(0), + Some(7), + ); + + sqlite_db + .write(WriteOperation::Insert(DbKeyValuePair::CompletedTransaction( + TxId::from(3), + Box::new(transaction), + ))) + .unwrap(); + + let db_tx = sqlite_db.fetch_imported_transactions().unwrap(); assert_eq!(db_tx.len(), 1); assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(1)); + assert_eq!(db_tx.first().unwrap().mined_height, 5); + + let db_tx = sqlite_db.fetch_unconfirmed_faux_transactions().unwrap(); + assert_eq!(db_tx.len(), 1); + assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(2)); + assert_eq!(db_tx.first().unwrap().mined_height, 6); + + let db_tx = sqlite_db.fetch_confirmed_faux_transactions_from_height(10).unwrap(); + assert_eq!(db_tx.len(), 0); + let db_tx = sqlite_db.fetch_confirmed_faux_transactions_from_height(4).unwrap(); + assert_eq!(db_tx.len(), 1); + assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(3)); + assert_eq!(db_tx.first().unwrap().mined_height, 7); } diff --git a/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs b/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs index 97f55d1c1e..c84666355f 100644 --- a/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs +++ b/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs @@ -197,6 +197,7 @@ pub async fn add_transaction_to_database( Utc::now().naive_local(), TransactionDirection::Outbound, coinbase_block_height, + None, ); completed_tx1.valid = valid; db.insert_completed_transaction(tx_id, completed_tx1).await.unwrap(); diff --git a/base_layer/wallet/tests/wallet.rs b/base_layer/wallet/tests/wallet.rs index f122812223..5b5c4b08bc 100644 --- a/base_layer/wallet/tests/wallet.rs +++ b/base_layer/wallet/tests/wallet.rs @@ -45,8 +45,10 @@ use std::{panic, path::Path, sync::Arc, time::Duration}; use rand::rngs::OsRng; +use support::{comms_and_services::get_next_memory_address, utils::make_input}; use tari_common_types::{ chain_metadata::ChainMetadata, + transaction::{ImportStatus, TransactionStatus}, types::{PrivateKey, PublicKey}, }; use tari_comms::{ @@ -55,11 +57,14 @@ use tari_comms::{ types::CommsPublicKey, }; use tari_comms_dht::{store_forward::SafConfig, DhtConfig}; -use tari_core::transactions::{ - tari_amount::{uT, MicroTari}, - test_helpers::{create_unblinded_output, TestParams}, - transaction::OutputFeatures, - CryptoFactories, +use tari_core::{ + covenants::Covenant, + transactions::{ + tari_amount::{uT, MicroTari}, + test_helpers::{create_unblinded_output, TestParams}, + transaction::OutputFeatures, + CryptoFactories, + }, }; use tari_crypto::{ inputs, @@ -97,11 +102,7 @@ use tari_wallet::{ }; use tempfile::tempdir; use tokio::{runtime::Runtime, time::sleep}; - pub mod support; -use support::{comms_and_services::get_next_memory_address, utils::make_input}; -use tari_common_types::transaction::TransactionStatus; -use tari_core::covenants::Covenant; use tari_wallet::output_manager_service::storage::database::OutputManagerDatabase; fn create_peer(public_key: CommsPublicKey, net_address: Multiaddr) -> Peer { @@ -755,42 +756,99 @@ async fn test_import_utxo() { let features = OutputFeatures::create_coinbase(50); let p = TestParams::new(); - let utxo = create_unblinded_output(script.clone(), features.clone(), p.clone(), 20000 * uT); + let utxo_1 = create_unblinded_output(script.clone(), features.clone(), p.clone(), 20000 * uT); + let utxo_2 = create_unblinded_output(script.clone(), features.clone(), p.clone(), 30000 * uT); + let utxo_3 = create_unblinded_output(script.clone(), features.clone(), p.clone(), 40000 * uT); let output = utxo.as_transaction_output(&factories).unwrap(); let expected_output_hash = output.hash(); - let tx_id = alice_wallet + let tx_id_1 = alice_wallet + .import_utxo( + utxo_1.value, + &utxo_1.spending_key, + script.clone(), + input.clone(), + base_node_identity.public_key(), + features.clone(), + "Testing".to_string(), + utxo_1.metadata_signature.clone(), + &p.script_private_key, + &p.sender_offset_public_key, + 0, + Covenant::default(), + ImportStatus::Imported, + ) + .await + .unwrap(); + let tx_id_2 = alice_wallet + .import_utxo( + utxo_2.value, + &utxo_2.spending_key, + script.clone(), + input.clone(), + base_node_identity.public_key(), + features.clone(), + "Testing".to_string(), + utxo_2.metadata_signature.clone(), + &p.script_private_key, + &p.sender_offset_public_key, + 0, + Covenant::default(), + ImportStatus::ScannedUnconfirmed, + ) + .await + .unwrap(); + let tx_id_3 = alice_wallet .import_utxo( - utxo.value, - &utxo.spending_key, + utxo_3.value, + &utxo_3.spending_key, script, input, base_node_identity.public_key(), features, "Testing".to_string(), - utxo.metadata_signature.clone(), + utxo_3.metadata_signature.clone(), &p.script_private_key, &p.sender_offset_public_key, 0, Covenant::default(), + ImportStatus::ScannedConfirmed, ) .await .unwrap(); let balance = alice_wallet.output_manager_service.get_balance().await.unwrap(); - assert_eq!(balance.pending_incoming_balance, 20000 * uT); + assert_eq!(balance.pending_incoming_balance, 90000 * uT); - let completed_tx = alice_wallet + let completed_tx_1 = alice_wallet + .transaction_service + .get_completed_transactions() + .await + .unwrap() + .remove(&tx_id_1) + .expect("Tx should be in collection"); + let completed_tx_2 = alice_wallet + .transaction_service + .get_completed_transactions() + .await + .unwrap() + .remove(&tx_id_2) + .expect("Tx should be in collection"); + let completed_tx_3 = alice_wallet .transaction_service .get_completed_transactions() .await .unwrap() - .remove(&tx_id) + .remove(&tx_id_3) .expect("Tx should be in collection"); - assert_eq!(completed_tx.amount, 20000 * uT); - assert_eq!(completed_tx.status, TransactionStatus::Imported); + assert_eq!(completed_tx_1.amount, 20000 * uT); + assert_eq!(completed_tx_1.status, TransactionStatus::Imported); + assert_eq!(completed_tx_2.amount, 30000 * uT); + assert_eq!(completed_tx_2.status, TransactionStatus::FauxUnconfirmed); + assert_eq!(completed_tx_3.amount, 40000 * uT); + assert_eq!(completed_tx_3.status, TransactionStatus::FauxConfirmed); let db = OutputManagerDatabase::new(OutputManagerSqliteDatabase::new(connection, None)); let outputs = db.fetch_outputs_by_tx_id(tx_id).await.unwrap(); assert!(outputs.iter().any(|o| { o.hash == expected_output_hash })); diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index f3a4c7d3b5..f25c9097e1 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -40,6 +40,8 @@ //! `callback_transaction_mined` - This will be called when a Broadcast transaction is detected as mined via a base //! node request //! +//! `callback_faux_transaction_confirmed` - This will be called when a one-sided transaction is detected as mined +//! //! `callback_discovery_process_complete` - This will be called when a `send_transacion(..)` call is made to a peer //! whose address is not known and a discovery process must be conducted. The outcome of the discovery process is //! relayed via this callback @@ -80,6 +82,8 @@ where TBackend: TransactionBackend + 'static callback_transaction_broadcast: unsafe extern "C" fn(*mut CompletedTransaction), callback_transaction_mined: unsafe extern "C" fn(*mut CompletedTransaction), callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), + callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut CompletedTransaction), + callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(u64, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(u64, bool), callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction, u64), @@ -118,6 +122,8 @@ where TBackend: TransactionBackend + 'static callback_transaction_broadcast: unsafe extern "C" fn(*mut CompletedTransaction), callback_transaction_mined: unsafe extern "C" fn(*mut CompletedTransaction), callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), + callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut CompletedTransaction), + callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(u64, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(u64, bool), callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction, u64), @@ -151,6 +157,14 @@ where TBackend: TransactionBackend + 'static target: LOG_TARGET, "TransactionMinedUnconfirmedCallback -> Assigning Fn: {:?}", callback_transaction_mined_unconfirmed ); + info!( + target: LOG_TARGET, + "FauxTransactionConfirmedCallback -> Assigning Fn: {:?}", callback_faux_transaction_confirmed + ); + info!( + target: LOG_TARGET, + "FauxTransactionUnconfirmedCallback -> Assigning Fn: {:?}", callback_faux_transaction_unconfirmed + ); info!( target: LOG_TARGET, "DirectSendResultCallback -> Assigning Fn: {:?}", callback_direct_send_result @@ -191,6 +205,8 @@ where TBackend: TransactionBackend + 'static callback_transaction_broadcast, callback_transaction_mined, callback_transaction_mined_unconfirmed, + callback_faux_transaction_confirmed, + callback_faux_transaction_unconfirmed, callback_direct_send_result, callback_store_and_forward_send_result, callback_transaction_cancellation, @@ -262,6 +278,14 @@ where TBackend: TransactionBackend + 'static self.receive_transaction_mined_unconfirmed_event(tx_id, num_confirmations).await; self.trigger_balance_refresh().await; }, + TransactionEvent::FauxTransactionConfirmed{tx_id, is_valid: _} => { + self.receive_faux_transaction_confirmed_event(tx_id).await; + self.trigger_balance_refresh().await; + }, + TransactionEvent::FauxTransactionUnconfirmed{tx_id, num_confirmations, is_valid: _} => { + self.receive_faux_transaction_unconfirmed_event(tx_id, num_confirmations).await; + self.trigger_balance_refresh().await; + }, TransactionEvent::TransactionValidationStateChanged(_request_key) => { self.trigger_balance_refresh().await; }, @@ -272,7 +296,7 @@ where TBackend: TransactionBackend + 'static self.transaction_validation_complete_event(request_key.as_u64(), false); }, TransactionEvent::TransactionMinedRequestTimedOut(_tx_id) | - TransactionEvent::TransactionImported(_tx_id) | + TransactionEvent::TransactionImported(_tx_id)| TransactionEvent::TransactionCompletedImmediately(_tx_id) => { self.trigger_balance_refresh().await; @@ -493,7 +517,7 @@ where TBackend: TransactionBackend + 'static Ok(tx) => { debug!( target: LOG_TARGET, - "Calling Received Transaction Mined callback function for TxId: {}", tx_id + "Calling Received Transaction Mined Unconfirmed callback function for TxId: {}", tx_id ); let boxing = Box::into_raw(Box::new(tx)); unsafe { @@ -504,6 +528,38 @@ where TBackend: TransactionBackend + 'static } } + async fn receive_faux_transaction_confirmed_event(&mut self, tx_id: TxId) { + match self.db.get_completed_transaction(tx_id).await { + Ok(tx) => { + debug!( + target: LOG_TARGET, + "Calling Received Faux Transaction Confirmed callback function for TxId: {}", tx_id + ); + let boxing = Box::into_raw(Box::new(tx)); + unsafe { + (self.callback_faux_transaction_confirmed)(boxing); + } + }, + Err(e) => error!(target: LOG_TARGET, "Error retrieving Completed Transaction: {:?}", e), + } + } + + async fn receive_faux_transaction_unconfirmed_event(&mut self, tx_id: TxId, confirmations: u64) { + match self.db.get_completed_transaction(tx_id).await { + Ok(tx) => { + debug!( + target: LOG_TARGET, + "Calling Received Faux Transaction Unconfirmed callback function for TxId: {}", tx_id + ); + let boxing = Box::into_raw(Box::new(tx)); + unsafe { + (self.callback_faux_transaction_unconfirmed)(boxing, confirmations); + } + }, + Err(e) => error!(target: LOG_TARGET, "Error retrieving Completed Transaction: {:?}", e), + } + } + fn transaction_validation_complete_event(&mut self, request_key: u64, success: bool) { debug!( target: LOG_TARGET, diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index 64732fc9b1..6d38f44146 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -77,6 +77,8 @@ mod test { pub broadcast_tx_callback_called: bool, pub mined_tx_callback_called: bool, pub mined_tx_unconfirmed_callback_called: u64, + pub faux_tx_confirmed_callback_called: bool, + pub faux_tx_unconfirmed_callback_called: u64, pub direct_send_callback_called: bool, pub store_and_forward_send_callback_called: bool, pub tx_cancellation_callback_called_completed: bool, @@ -98,6 +100,8 @@ mod test { broadcast_tx_callback_called: false, mined_tx_callback_called: false, mined_tx_unconfirmed_callback_called: 0, + faux_tx_confirmed_callback_called: false, + faux_tx_unconfirmed_callback_called: 0, direct_send_callback_called: false, store_and_forward_send_callback_called: false, callback_txo_validation_complete: 0, @@ -158,6 +162,20 @@ mod test { Box::from_raw(tx); } + unsafe extern "C" fn faux_confirmed_callback(tx: *mut CompletedTransaction) { + let mut lock = CALLBACK_STATE.lock().unwrap(); + lock.faux_tx_confirmed_callback_called = true; + drop(lock); + Box::from_raw(tx); + } + + unsafe extern "C" fn faux_unconfirmed_callback(tx: *mut CompletedTransaction, confirmations: u64) { + let mut lock = CALLBACK_STATE.lock().unwrap(); + lock.faux_tx_unconfirmed_callback_called = confirmations; + drop(lock); + Box::from_raw(tx); + } + unsafe extern "C" fn direct_send_callback(_tx_id: u64, _result: bool) { let mut lock = CALLBACK_STATE.lock().unwrap(); lock.direct_send_callback_called = true; @@ -230,6 +248,10 @@ mod test { "1".to_string(), Utc::now().naive_utc(), ); + runtime + .block_on(db.add_pending_inbound_transaction(1u64.into(), inbound_tx.clone())) + .unwrap(); + let completed_tx = CompletedTransaction::new( 2u64.into(), PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), @@ -248,7 +270,12 @@ mod test { Utc::now().naive_utc(), TransactionDirection::Inbound, None, + None, ); + runtime + .block_on(db.insert_completed_transaction(2u64.into(), completed_tx.clone())) + .unwrap(); + let stp = SenderTransactionProtocol::new_placeholder(); let outbound_tx = OutboundTransaction::new( 3u64.into(), @@ -261,33 +288,76 @@ mod test { Utc::now().naive_utc(), false, ); + runtime + .block_on(db.add_pending_outbound_transaction(3u64.into(), outbound_tx.clone())) + .unwrap(); + runtime.block_on(db.cancel_pending_transaction(3u64.into())).unwrap(); + let inbound_tx_cancelled = InboundTransaction { tx_id: 4u64.into(), ..inbound_tx.clone() }; - let completed_tx_cancelled = CompletedTransaction { - tx_id: 5u64.into(), - ..completed_tx.clone() - }; - - runtime - .block_on(db.add_pending_inbound_transaction(1u64.into(), inbound_tx.clone())) - .unwrap(); - runtime - .block_on(db.insert_completed_transaction(2u64.into(), completed_tx.clone())) - .unwrap(); runtime .block_on(db.add_pending_inbound_transaction(4u64.into(), inbound_tx_cancelled)) .unwrap(); runtime.block_on(db.cancel_pending_transaction(4u64.into())).unwrap(); + + let completed_tx_cancelled = CompletedTransaction { + tx_id: 5u64.into(), + ..completed_tx.clone() + }; runtime .block_on(db.insert_completed_transaction(5u64.into(), completed_tx_cancelled.clone())) .unwrap(); runtime.block_on(db.reject_completed_transaction(5u64.into())).unwrap(); + + let faux_unconfirmed_tx = CompletedTransaction::new( + 6u64.into(), + PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + MicroTari::from(100), + MicroTari::from(2000), + Transaction::new( + Vec::new(), + Vec::new(), + Vec::new(), + BlindingFactor::default(), + BlindingFactor::default(), + ), + TransactionStatus::FauxUnconfirmed, + "6".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + None, + Some(2), + ); runtime - .block_on(db.add_pending_outbound_transaction(3u64.into(), outbound_tx.clone())) + .block_on(db.insert_completed_transaction(6u64.into(), faux_unconfirmed_tx.clone())) + .unwrap(); + + let faux_confirmed_tx = CompletedTransaction::new( + 7u64.into(), + PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + MicroTari::from(100), + MicroTari::from(2000), + Transaction::new( + Vec::new(), + Vec::new(), + Vec::new(), + BlindingFactor::default(), + BlindingFactor::default(), + ), + TransactionStatus::FauxConfirmed, + "7".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + None, + Some(5), + ); + runtime + .block_on(db.insert_completed_transaction(7u64.into(), faux_confirmed_tx.clone())) .unwrap(); - runtime.block_on(db.cancel_pending_transaction(3u64.into())).unwrap(); let (transaction_event_sender, transaction_event_receiver) = broadcast::channel(20); let (oms_event_sender, oms_event_receiver) = broadcast::channel(20); @@ -330,6 +400,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + faux_confirmed_callback, + faux_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -478,7 +550,7 @@ mod test { .unwrap(); balance.available_balance -= completed_tx_cancelled.amount; - mock_output_manager_service_state.set_balance(balance); + mock_output_manager_service_state.set_balance(balance.clone()); // Balance updated should be detected with following event, total = 5 times oms_event_sender .send(Arc::new(OutputManagerEvent::TxoValidationSuccess(1u64))) @@ -520,6 +592,51 @@ mod test { .send(Arc::new(TransactionEvent::TransactionValidationCompleted(4u64.into()))) .unwrap(); + balance.pending_incoming_balance += faux_unconfirmed_tx.amount; + mock_output_manager_service_state.set_balance(balance.clone()); + // Balance updated should be detected with following event, total = 6 times + transaction_event_sender + .send(Arc::new(TransactionEvent::FauxTransactionUnconfirmed { + tx_id: 6u64.into(), + num_confirmations: 2, + is_valid: true, + })) + .unwrap(); + let start = Instant::now(); + while start.elapsed().as_secs() < 10 { + { + let lock = CALLBACK_STATE.lock().unwrap(); + if lock.callback_balance_updated == 6 { + callback_balance_updated = 6; + break; + } + } + thread::sleep(Duration::from_millis(100)); + } + assert_eq!(callback_balance_updated, 6); + + balance.available_balance += faux_confirmed_tx.amount; + mock_output_manager_service_state.set_balance(balance.clone()); + // Balance updated should be detected with following event, total = 7 times + transaction_event_sender + .send(Arc::new(TransactionEvent::FauxTransactionConfirmed { + tx_id: 7u64.into(), + is_valid: true, + })) + .unwrap(); + let start = Instant::now(); + while start.elapsed().as_secs() < 10 { + { + let lock = CALLBACK_STATE.lock().unwrap(); + if lock.callback_balance_updated == 7 { + callback_balance_updated = 7; + break; + } + } + thread::sleep(Duration::from_millis(100)); + } + assert_eq!(callback_balance_updated, 7); + dht_event_sender .send(Arc::new(DhtEvent::StoreAndForwardMessagesReceived)) .unwrap(); @@ -541,6 +658,8 @@ mod test { assert!(lock.broadcast_tx_callback_called); assert!(lock.mined_tx_callback_called); assert_eq!(lock.mined_tx_unconfirmed_callback_called, 22u64); + assert!(lock.faux_tx_confirmed_callback_called); + assert_eq!(lock.faux_tx_unconfirmed_callback_called, 2u64); assert!(lock.direct_send_callback_called); assert!(lock.store_and_forward_send_callback_called); assert!(lock.tx_cancellation_callback_called_inbound); @@ -548,7 +667,7 @@ mod test { assert!(lock.tx_cancellation_callback_called_outbound); assert!(lock.saf_messages_received); assert_eq!(lock.callback_txo_validation_complete, 3); - assert_eq!(lock.callback_balance_updated, 5); + assert_eq!(lock.callback_balance_updated, 7); assert_eq!(lock.callback_transaction_validation_complete, 7); assert_eq!(lock.connectivity_status_callback_called, 7); diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 5041dd5084..45804e5d01 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -108,7 +108,7 @@ use log4rs::{ use rand::rngs::OsRng; use tari_common_types::{ emoji::{emoji_set, EmojiId, EmojiIdError}, - transaction::{TransactionDirection, TransactionStatus, TxId}, + transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, types::{Commitment, PublicKey}, }; use tari_comms::{ @@ -3231,7 +3231,11 @@ unsafe fn init_logging( /// `callback_transaction_mined` - The callback function pointer matching the function signature. This will be called /// when a Broadcast transaction is detected as mined AND confirmed. /// `callback_transaction_mined_unconfirmed` - The callback function pointer matching the function signature. This will -/// be called when a Broadcast transaction is detected as mined but not yet confirmed. +/// be called when a Broadcast transaction is detected as mined but not yet confirmed. +/// `callback_faux_transaction_confirmed` - The callback function pointer matching the function signature. This will be called +/// when a one-sided transaction is detected as mined AND confirmed. +/// `callback_faux_transaction_unconfirmed` - The callback function pointer matching the function signature. This +/// will be called when a one-sided transaction is detected as mined but not yet confirmed. /// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called /// when a direct send is completed. The first parameter is the transaction id and the second is whether if was /// successful or not. @@ -3293,6 +3297,8 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_broadcast: unsafe extern "C" fn(*mut TariCompletedTransaction), callback_transaction_mined: unsafe extern "C" fn(*mut TariCompletedTransaction), callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), + callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), + callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(c_ulonglong, bool), callback_transaction_cancellation: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), @@ -3499,6 +3505,8 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_broadcast, callback_transaction_mined, callback_transaction_mined_unconfirmed, + callback_faux_transaction_confirmed, + callback_faux_transaction_unconfirmed, callback_direct_send_result, callback_store_and_forward_send_result, callback_transaction_cancellation, @@ -5027,6 +5035,7 @@ pub unsafe extern "C" fn wallet_import_utxo( &(*sender_offset_public_key).clone(), 0, covenant, + ImportStatus::Imported, )) { Ok(tx_id) => { if let Err(e) = (*wallet) @@ -6053,6 +6062,8 @@ mod test { pub broadcast_tx_callback_called: bool, pub mined_tx_callback_called: bool, pub mined_tx_unconfirmed_callback_called: bool, + pub scanned_tx_callback_called: bool, + pub scanned_tx_unconfirmed_callback_called: bool, pub direct_send_callback_called: bool, pub store_and_forward_send_callback_called: bool, pub tx_cancellation_callback_called: bool, @@ -6070,6 +6081,8 @@ mod test { broadcast_tx_callback_called: false, mined_tx_callback_called: false, mined_tx_unconfirmed_callback_called: false, + scanned_tx_callback_called: false, + scanned_tx_unconfirmed_callback_called: false, direct_send_callback_called: false, store_and_forward_send_callback_called: false, tx_cancellation_callback_called: false, @@ -6177,6 +6190,48 @@ mod test { completed_transaction_destroy(tx); } + unsafe extern "C" fn scanned_callback(tx: *mut TariCompletedTransaction) { + assert!(!tx.is_null()); + assert_eq!( + type_of((*tx).clone()), + std::any::type_name::() + ); + assert_eq!((*tx).status, TransactionStatus::FauxConfirmed); + let mut lock = CALLBACK_STATE_FFI.lock().unwrap(); + lock.scanned_tx_callback_called = true; + drop(lock); + completed_transaction_destroy(tx); + } + + unsafe extern "C" fn scanned_unconfirmed_callback(tx: *mut TariCompletedTransaction, _confirmations: u64) { + assert!(!tx.is_null()); + assert_eq!( + type_of((*tx).clone()), + std::any::type_name::() + ); + assert_eq!((*tx).status, TransactionStatus::FauxUnconfirmed); + let mut lock = CALLBACK_STATE_FFI.lock().unwrap(); + lock.scanned_tx_unconfirmed_callback_called = true; + let mut error = 0; + let error_ptr = &mut error as *mut c_int; + let kernel = completed_transaction_get_transaction_kernel(tx, error_ptr); + let excess_hex_ptr = transaction_kernel_get_excess_hex(kernel, error_ptr); + let excess_hex = CString::from_raw(excess_hex_ptr).to_str().unwrap().to_owned(); + assert!(!excess_hex.is_empty()); + let nonce_hex_ptr = transaction_kernel_get_excess_public_nonce_hex(kernel, error_ptr); + let nonce_hex = CString::from_raw(nonce_hex_ptr).to_str().unwrap().to_owned(); + assert!(!nonce_hex.is_empty()); + let sig_hex_ptr = transaction_kernel_get_excess_signature_hex(kernel, error_ptr); + let sig_hex = CString::from_raw(sig_hex_ptr).to_str().unwrap().to_owned(); + assert!(!sig_hex.is_empty()); + string_destroy(excess_hex_ptr as *mut c_char); + string_destroy(sig_hex_ptr as *mut c_char); + string_destroy(nonce_hex_ptr); + transaction_kernel_destroy(kernel); + drop(lock); + completed_transaction_destroy(tx); + } + unsafe extern "C" fn direct_send_callback(_tx_id: c_ulonglong, _result: bool) { // assert!(true); //optimized out by compiler } @@ -6579,6 +6634,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6616,6 +6673,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6719,6 +6778,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6767,6 +6828,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6798,6 +6861,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6824,6 +6889,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6871,6 +6938,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6947,6 +7016,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -7154,6 +7225,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -7209,6 +7282,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index 889aaea741..a3bc010ddc 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -263,14 +263,19 @@ unsigned long long completed_transaction_get_fee(struct TariCompletedTransaction const char *completed_transaction_get_message(struct TariCompletedTransaction *transaction, int *error_out); // Gets the status of a TariCompletedTransaction -// | Value | Interpretation | +// | Value | Interpretation | // |---|---| -// | -1 | TxNullError | -// | 0 | Completed | -// | 1 | Broadcast | -// | 2 | Mined | -// | 3 | Imported | -// | 4 | Pending | +// | -1 | TxNullError | +// | 0 | Completed | +// | 1 | Broadcast | +// | 2 | MinedUnconfirmed | +// | 3 | Imported | +// | 4 | Pending | +// | 5 | Coinbase | +// | 6 | MinedConfirmed | +// | 7 | Rejected | +// | 8 | ScannedUnconfirmed | +// | 9 | ScannedConfirmed | int completed_transaction_get_status(struct TariCompletedTransaction *transaction, int *error_out); // Gets the TransactionID of a TariCompletedTransaction @@ -341,14 +346,19 @@ const char *pending_outbound_transaction_get_message(struct TariPendingOutboundT unsigned long long pending_outbound_transaction_get_timestamp(struct TariPendingOutboundTransaction *transaction, int *error_out); // Gets the status of a TariPendingOutboundTransaction -// | Value | Interpretation | +// | Value | Interpretation | // |---|---| -// | -1 | TxNullError | -// | 0 | Completed | -// | 1 | Broadcast | -// | 2 | Mined | -// | 3 | Imported | -// | 4 | Pending | +// | -1 | TxNullError | +// | 0 | Completed | +// | 1 | Broadcast | +// | 2 | MinedUnconfirmed | +// | 3 | Imported | +// | 4 | Pending | +// | 5 | Coinbase | +// | 6 | MinedConfirmed | +// | 7 | Rejected | +// | 8 | ScannedUnconfirmed | +// | 9 | ScannedConfirmed | int pending_outbound_transaction_get_status(struct TariPendingOutboundTransaction *transaction, int *error_out); // Frees memory for a TariPendingOutboundTactions @@ -383,14 +393,19 @@ unsigned long long pending_inbound_transaction_get_amount(struct TariPendingInbo unsigned long long pending_inbound_transaction_get_timestamp(struct TariPendingInboundTransaction *transaction, int *error_out); // Gets the status of a TariPendingInboundTransaction -// | Value | Interpretation | +// | Value | Interpretation | // |---|---| -// | -1 | TxNullError | -// | 0 | Completed | -// | 1 | Broadcast | -// | 2 | Mined | -// | 3 | Imported | -// | 4 | Pending | +// | -1 | TxNullError | +// | 0 | Completed | +// | 1 | Broadcast | +// | 2 | MinedUnconfirmed | +// | 3 | Imported | +// | 4 | Pending | +// | 5 | Coinbase | +// | 6 | MinedConfirmed | +// | 7 | Rejected | +// | 8 | ScannedUnconfirmed | +// | 9 | ScannedConfirmed | int pending_inbound_transaction_get_status(struct TariPendingInboundTransaction *transaction, int *error_out); // Frees memory for a TariPendingInboundTransaction @@ -451,6 +466,10 @@ struct TariPublicKeys *comms_list_connected_public_keys(struct TariWallet *walle /// when a Broadcast transaction is detected as mined AND confirmed. /// `callback_transaction_mined_unconfirmed` - The callback function pointer matching the function signature. This will /// be called when a Broadcast transaction is detected as mined but not yet confirmed. +/// `callback_faux_transaction_confirmed` - The callback function pointer matching the function signature. This will be called +/// when a one-sided transaction is detected as mined AND confirmed. +/// `callback_faux_transaction_unconfirmed` - The callback function pointer matching the function signature. This will +/// be called when a one-sided transaction is detected as mined but not yet confirmed. /// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called /// when a direct send is completed. The first parameter is the transaction id and the second is whether if was successful or not. /// `callback_store_and_forward_send_result` - The callback function pointer matching the function signature. This is called @@ -515,6 +534,8 @@ struct TariWallet *wallet_create(struct TariCommsConfig *config, void (*callback_transaction_broadcast)(struct TariCompletedTransaction *), void (*callback_transaction_mined)(struct TariCompletedTransaction *), void (*callback_transaction_mined_unconfirmed)(struct TariCompletedTransaction *, unsigned long long), + void (*callback_faux_transaction_confirmed)(struct TariCompletedTransaction *), + void (*callback_faux_transaction_unconfirmed)(struct TariCompletedTransaction *, unsigned long long), void (*callback_direct_send_result)(unsigned long long, bool), void (*callback_store_and_forward_send_result)(unsigned long long, bool), void (*callback_transaction_cancellation)(struct TariCompletedTransaction *, unsigned long long), diff --git a/integration_tests/features/WalletFFI.feature b/integration_tests/features/WalletFFI.feature index 9e96c20e7d..846081c2ad 100644 --- a/integration_tests/features/WalletFFI.feature +++ b/integration_tests/features/WalletFFI.feature @@ -145,15 +145,18 @@ Feature: Wallet FFI And mining node MINER mines 10 blocks Then I wait for wallet SENDER to have at least 1000000 uT And I send 2000000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be Broadcast + Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 10 blocks Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT And I send 1000000 uT from ffi wallet FFI_WALLET to wallet RECEIVER at fee 20 via one-sided transactions + Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 2 blocks Then all nodes are at height 22 - And mining node MINER mines 2 blocks - Then all nodes are at height 24 - And mining node MINER mines 6 blocks + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_UNCONFIRMED and valid/ + And mining node MINER mines 5 blocks + Then all nodes are at height 27 + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_CONFIRMED and valid/ +# And mining node MINER mines 6 blocks Then I wait for wallet RECEIVER to have at least 1000000 uT Then I wait for ffi wallet FFI_WALLET to receive 2 mined And I stop ffi wallet FFI_WALLET diff --git a/integration_tests/features/support/ffi_steps.js b/integration_tests/features/support/ffi_steps.js index 1853b77eda..1a9a1fb9d7 100644 --- a/integration_tests/features/support/ffi_steps.js +++ b/integration_tests/features/support/ffi_steps.js @@ -479,13 +479,17 @@ Then( ); Then( - "ffi wallet {word} detects {word} {int} ffi transactions to be Broadcast", + "ffi wallet {word} detects {word} {int} ffi transactions to be {word}", { timeout: 125 * 1000 }, - async function (walletName, comparison, amount) { + async function (walletName, comparison, amount, status) { // Pending -> Completed -> Broadcast -> Mined Unconfirmed -> Mined Confirmed const atLeast = "AT_LEAST"; const exactly = "EXACTLY"; expect(comparison === atLeast || comparison === exactly).to.equal(true); + const broadcast = "TRANSACTION_STATUS_BROADCAST"; + const scannedUnconfirmed = "TRANSACTION_STATUS_FAUX_UNCONFIRMED"; + const scannedConfirmed = "TRANSACTION_STATUS_FAUX_CONFIRMED"; + expect(status === broadcast || status === scannedUnconfirmed || status === scannedConfirmed).to.equal(true); const wallet = this.getWallet(walletName); console.log("\n"); @@ -496,27 +500,56 @@ Then( comparison + " " + amount + - " broadcast transaction(s)" + " " + + status + + " transaction(s)" ); - await waitForIterate( + await waitForIterate( () => { - return wallet.getCounters().broadcast >= amount; + switch (status) { + case broadcast: + return wallet.getCounters().broadcast >= amount; + break; + case scannedUnconfirmed: + return wallet.getCounters().scannedUnconfirmed >= amount; + break; + case scannedConfirmed: + return wallet.getCounters().scanned >= amount; + break; + default: + expect(expr).to.equal("please add this<< TransactionStatus");; + } }, true, 1000, 120 ); - if (!(wallet.getCounters().broadcast >= amount)) { + let amountOfCallbacks; + switch (status) { + case broadcast: + amountOfCallbacks = wallet.getCounters().broadcast; + break; + case scannedUnconfirmed: + amountOfCallbacks = wallet.getCounters().scannedUnconfirmed; + break; + case scannedConfirmed: + amountOfCallbacks = wallet.getCounters().scanned; + break; + default: + expect(expr).to.equal("please add this<< TransactionStatus");; + } + + if (!(amountOfCallbacks >= amount)) { console.log("Counter not adequate!"); } else { console.log(wallet.getCounters()); } if (comparison === atLeast) { - expect(wallet.getCounters().broadcast >= amount).to.equal(true); + expect(amountOfCallbacks >= amount).to.equal(true); } else { - expect(wallet.getCounters().broadcast === amount).to.equal(true); + expect(amountOfCallbacks === amount).to.equal(true); } } ); diff --git a/integration_tests/features/support/wallet_steps.js b/integration_tests/features/support/wallet_steps.js index 1828e32fc4..16e80af104 100644 --- a/integration_tests/features/support/wallet_steps.js +++ b/integration_tests/features/support/wallet_steps.js @@ -2034,7 +2034,9 @@ Then( 1 + " has " + transactions[i]["status"] + - " and is valid(" + + " (need " + + transactionStatus + + ") and is valid(" + transactions[i]["valid"] + ")" ); diff --git a/integration_tests/features/support/world.js b/integration_tests/features/support/world.js index 412e288f0f..e6e96b65cd 100644 --- a/integration_tests/features/support/world.js +++ b/integration_tests/features/support/world.js @@ -638,7 +638,7 @@ BeforeAll({ timeout: 2400000 }, async function () { const wallet = new WalletProcess("compile"); console.log("Compiling wallet..."); await wallet.init(); - await wallet.compile(); + // await wallet.compile(); const mmProxy = new MergeMiningProxyProcess( "compile", @@ -675,7 +675,7 @@ BeforeAll({ timeout: 2400000 }, async function () { await miningNode.compile(); console.log("Compiling wallet FFI..."); - await InterfaceFFI.compile(); + // await InterfaceFFI.compile(); console.log("Finished compilation."); console.log("Loading FFI interface.."); await InterfaceFFI.init(); diff --git a/integration_tests/helpers/ffi/ffiInterface.js b/integration_tests/helpers/ffi/ffiInterface.js index 2f2f455fa2..cb98d17bf4 100644 --- a/integration_tests/helpers/ffi/ffiInterface.js +++ b/integration_tests/helpers/ffi/ffiInterface.js @@ -292,6 +292,8 @@ class InterfaceFFI { this.ptr, this.ptr, this.ptr, + this.ptr, + this.ptr, this.boolPtr, this.intPtr, ], @@ -1136,6 +1138,14 @@ class InterfaceFFI { return ffi.Callback(this.void, [this.ptr, this.ulonglong], fn); } + static createCallbackFauxTransactionConfirmed(fn) { + return ffi.Callback(this.void, [this.ptr], fn); + } + + static createCallbackFauxTransactionUnconfirmed(fn) { + return ffi.Callback(this.void, [this.ptr, this.ulonglong], fn); + } + static createCallbackDirectSendResult(fn) { return ffi.Callback(this.void, [this.ulonglong, this.bool], fn); } @@ -1184,6 +1194,8 @@ class InterfaceFFI { callback_transaction_broadcast, callback_transaction_mined, callback_transaction_mined_unconfirmed, + callback_faux_transaction_confirmed, + callback_faux_transaction_unconfirmed, callback_direct_send_result, callback_store_and_forward_send_result, callback_transaction_cancellation, @@ -1209,6 +1221,8 @@ class InterfaceFFI { callback_transaction_broadcast, callback_transaction_mined, callback_transaction_mined_unconfirmed, + callback_faux_transaction_confirmed, + callback_faux_transaction_unconfirmed, callback_direct_send_result, callback_store_and_forward_send_result, callback_transaction_cancellation, diff --git a/integration_tests/helpers/ffi/wallet.js b/integration_tests/helpers/ffi/wallet.js index d307660dd2..c79105ce45 100644 --- a/integration_tests/helpers/ffi/wallet.js +++ b/integration_tests/helpers/ffi/wallet.js @@ -22,11 +22,16 @@ class Wallet { ptr; balance = new WalletBalance(); log_path = ""; - receivedTransaction = 0; - receivedTransactionReply = 0; + transactionReceived = 0; + transactionReplyReceived = 0; transactionBroadcast = 0; transactionMined = 0; - saf_messages = 0; + transactionMinedUnconfirmed = 0; + transactionFauxConfirmed = 0; + transactionFauxUnconfirmed = 0; + transactionSafMessageReceived = 0; + transactionCancelled = 0; + transactionFinalized = 0; txo_validation_complete = false; txo_validation_result = 0; tx_validation_complete = false; @@ -37,6 +42,8 @@ class Wallet { callback_transaction_broadcast; callback_transaction_mined; callback_transaction_mined_unconfirmed; + callback_faux_transaction_confirmed; + callback_faux_transaction_unconfirmed; callback_direct_send_result; callback_store_and_forward_send_result; callback_transaction_cancellation; @@ -61,27 +68,31 @@ class Wallet { } clearCallbackCounters() { - this.receivedTransaction = - this.receivedTransactionReply = + this.transactionReceived = + this.transactionReplyReceived = this.transactionBroadcast = this.transactionMined = - this.saf_messages = - this.cancelled = - this.minedunconfirmed = - this.finalized = + this.transactionFauxConfirmed = + this.transactionSafMessageReceived = + this.transactionCancelled = + this.transactionMinedUnconfirmed = + this.transactionFauxUnconfirmed = + this.transactionFinalized = 0; } getCounters() { return { - received: this.receivedTransaction, - replyreceived: this.receivedTransactionReply, + received: this.transactionReceived, + replyReceived: this.transactionReplyReceived, broadcast: this.transactionBroadcast, - finalized: this.finalized, - minedunconfirmed: this.minedunconfirmed, - cancelled: this.cancelled, + finalized: this.transactionFinalized, + minedUnconfirmed: this.transactionMinedUnconfirmed, + scannedUnconfirmed: this.transactionFauxUnconfirmed, + cancelled: this.transactionCancelled, mined: this.transactionMined, - saf: this.saf_messages, + scanned: this.transactionFauxConfirmed, + saf: this.transactionSafMessageReceived, }; } @@ -116,6 +127,12 @@ class Wallet { InterfaceFFI.createCallbackTransactionMinedUnconfirmed( this.onTransactionMinedUnconfirmed ); + this.callback_faux_transaction_confirmed = + InterfaceFFI.createCallbackFauxTransactionConfirmed(this.onFauxTransactionConfirmed); + this.callback_faux_transaction_unconfirmed = + InterfaceFFI.createCallbackFauxTransactionUnconfirmed( + this.onFauxTransactionUnconfirmed + ); this.callback_direct_send_result = InterfaceFFI.createCallbackDirectSendResult(this.onDirectSendResult); this.callback_store_and_forward_send_result = @@ -148,14 +165,16 @@ class Wallet { ); //endregion - this.receivedTransaction = 0; - this.receivedTransactionReply = 0; + this.transactionReceived = 0; + this.transactionReplyReceived = 0; this.transactionBroadcast = 0; this.transactionMined = 0; - this.saf_messages = 0; - this.cancelled = 0; - this.minedunconfirmed = 0; - this.finalized = 0; + this.transactionFauxConfirmed = 0; + this.transactionSafMessageReceived = 0; + this.transactionCancelled = 0; + this.transactionMinedUnconfirmed = 0; + this.transactionFauxUnconfirmed = 0; + this.transactionFinalized = 0; this.recoveryFinished = true; let sanitize = null; let words = null; @@ -179,6 +198,8 @@ class Wallet { this.callback_transaction_broadcast, this.callback_transaction_mined, this.callback_transaction_mined_unconfirmed, + this.callback_faux_transaction_confirmed, + this.callback_faux_transaction_unconfirmed, this.callback_direct_send_result, this.callback_store_and_forward_send_result, this.callback_transaction_cancellation, @@ -198,7 +219,7 @@ class Wallet { `${new Date().toISOString()} received Transaction with txID ${tx.getTransactionID()}` ); tx.destroy(); - this.receivedTransaction += 1; + this.transactionReceived += 1; }; onReceivedTransactionReply = (ptr) => { @@ -208,7 +229,7 @@ class Wallet { `${new Date().toISOString()} received reply for Transaction with txID ${tx.getTransactionID()}.` ); tx.destroy(); - this.receivedTransactionReply += 1; + this.transactionReplyReceived += 1; }; onReceivedFinalizedTransaction = (ptr) => { @@ -218,7 +239,7 @@ class Wallet { `${new Date().toISOString()} received finalization for Transaction with txID ${tx.getTransactionID()}.` ); tx.destroy(); - this.finalized += 1; + this.transactionFinalized += 1; }; onTransactionBroadcast = (ptr) => { @@ -248,7 +269,27 @@ class Wallet { `${new Date().toISOString()} Transaction with txID ${tx.getTransactionID()} is mined unconfirmed with ${confirmations} confirmations.` ); tx.destroy(); - this.minedunconfirmed += 1; + this.transactionMinedUnconfirmed += 1; + }; + + onFauxTransactionConfirmed = (ptr) => { + let tx = new CompletedTransaction(); + tx.pointerAssign(ptr); + console.log( + `${new Date().toISOString()} Faux transaction with txID ${tx.getTransactionID()} was confirmed.` + ); + tx.destroy(); + this.transactionFauxConfirmed += 1; + }; + + onFauxTransactionUnconfirmed = (ptr, confirmations) => { + let tx = new CompletedTransaction(); + tx.pointerAssign(ptr); + console.log( + `${new Date().toISOString()} Faux transaction with txID ${tx.getTransactionID()} is unconfirmed with ${confirmations} confirmations.` + ); + tx.destroy(); + this.transactionFauxUnconfirmed += 1; }; onTransactionCancellation = (ptr, reason) => { @@ -258,7 +299,7 @@ class Wallet { `${new Date().toISOString()} Transaction with txID ${tx.getTransactionID()} was cancelled with reason code ${reason}.` ); tx.destroy(); - this.cancelled += 1; + this.transactionCancelled += 1; }; onDirectSendResult = (id, success) => { @@ -308,7 +349,7 @@ class Wallet { onSafMessageReceived = () => { console.log(`${new Date().toISOString()} callbackSafMessageReceived()`); - this.saf_messages += 1; + this.transactionSafMessageReceived += 1; }; onRecoveryProgress = (a, b, c) => { @@ -465,6 +506,8 @@ class Wallet { this.callback_transaction_broadcast = this.callback_transaction_mined = this.callback_transaction_mined_unconfirmed = + this.callback_faux_transaction_confirmed = + this.callback_faux_transaction_unconfirmed = this.callback_direct_send_result = this.callback_store_and_forward_send_result = this.callback_transaction_cancellation = diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index 868ccd2cda..b0a9b0ba99 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -1946,7 +1946,7 @@ }, "globals": { "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "resolved": false, "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, @@ -2252,7 +2252,7 @@ }, "jsesc": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "resolved": false, "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, @@ -3038,7 +3038,7 @@ }, "source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "resolved": false, "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, @@ -3290,7 +3290,7 @@ }, "to-fast-properties": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "resolved": false, "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, @@ -3440,104 +3440,68 @@ "dependencies": { "@grpc/grpc-js": { "version": "1.3.6", - "resolved": false, - "integrity": "sha512-v7+LQFbqZKmd/Tvf5/j1Xlbq6jXL/4d+gUtm2TNX4QiEC3ELWADmGr2dGlUyLl6aKTuYfsN72vAsO5zmavYkEg==", "requires": { "@types/node": ">=12.12.47" } }, "@grpc/proto-loader": { "version": "0.5.6", - "resolved": false, - "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", "requires": { "lodash.camelcase": "^4.3.0", "protobufjs": "^6.8.6" } }, "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "version": "1.1.2" }, "@protobufjs/base64": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "version": "1.1.2" }, "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": false, - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "version": "2.0.4" }, "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "version": "1.1.0" }, "@protobufjs/fetch": { "version": "1.1.0", - "resolved": false, - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "@protobufjs/float": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "version": "1.0.2" }, "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "version": "1.1.0" }, "@protobufjs/path": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "version": "1.1.2" }, "@protobufjs/pool": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "version": "1.1.0" }, "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "version": "1.1.0" }, "@types/long": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + "version": "4.0.1" }, "@types/node": { - "version": "16.3.2", - "resolved": false, - "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==" + "version": "16.3.2" }, "grpc-promise": { - "version": "1.4.0", - "resolved": false, - "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==" + "version": "1.4.0" }, "lodash.camelcase": { - "version": "4.3.0", - "resolved": false, - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + "version": "4.3.0" }, "long": { - "version": "4.0.0", - "resolved": false, - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "4.0.0" }, "protobufjs": { "version": "6.11.2", - "resolved": false, - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", From db8357527b2cbf34bc88de8000b52f9d2d2c3c62 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Mon, 7 Feb 2022 14:37:56 +0200 Subject: [PATCH 2/3] finalizing & review comments --- applications/ffi_client/index.js | 12 ++-- applications/ffi_client/recovery.js | 8 +-- .../src/conversions/transaction.rs | 4 +- .../src/grpc/wallet_grpc_server.rs | 8 +-- base_layer/common_types/src/transaction.rs | 42 ++++++----- .../src/output_manager_service/handle.rs | 45 ++++++++---- .../recovery/standard_outputs_recoverer.rs | 6 +- .../src/output_manager_service/service.rs | 52 +++++++------- .../wallet/src/transaction_service/handle.rs | 24 +++++-- .../transaction_validation_protocol.rs | 12 ++-- .../wallet/src/transaction_service/service.rs | 35 +++++----- .../transaction_service/storage/database.rs | 15 ++-- .../src/transaction_service/storage/models.rs | 1 - .../transaction_service/storage/sqlite_db.rs | 42 ++++++----- .../tasks/check_faux_transaction_status.rs | 62 +++++++++++------ .../utxo_scanner_service/utxo_scanner_task.rs | 49 +++++++------ base_layer/wallet/src/wallet.rs | 12 ++-- .../output_manager_service_tests/service.rs | 6 +- .../support/output_manager_service_mock.rs | 10 ++- .../tests/support/transaction_service_mock.rs | 4 +- .../transaction_service_tests/service.rs | 35 +++++----- .../transaction_service_tests/storage.rs | 20 +++--- base_layer/wallet/tests/wallet.rs | 61 +--------------- base_layer/wallet_ffi/src/callback_handler.rs | 6 +- base_layer/wallet_ffi/src/lib.rs | 7 +- base_layer/wallet_ffi/wallet.h | 12 ++-- integration_tests/features/WalletFFI.feature | 36 +++++++--- .../features/support/ffi_steps.js | 69 ++++++++----------- integration_tests/features/support/world.js | 4 +- integration_tests/helpers/ffi/wallet.js | 8 ++- 30 files changed, 368 insertions(+), 339 deletions(-) diff --git a/applications/ffi_client/index.js b/applications/ffi_client/index.js index 44ecf21dad..c7a7c81f39 100644 --- a/applications/ffi_client/index.js +++ b/applications/ffi_client/index.js @@ -70,15 +70,15 @@ try { } ); // callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), - const txScanned = ffi.Callback("void", ["pointer"], function (ptr) { - console.log("txScanned: ", ptr); + const txFauxConfirmed = ffi.Callback("void", ["pointer"], function (ptr) { + console.log("txFauxConfirmed: ", ptr); }); // callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), - const txScannedUnconfirmed = ffi.Callback( + const txFauxUnconfirmed = ffi.Callback( "void", ["pointer"], function (ptr, confirmations) { - console.log("txScannedUnconfirmed: ", ptr, confirmations); + console.log("txFauxUnconfirmed: ", ptr, confirmations); } ); // callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), @@ -124,8 +124,8 @@ try { txBroadcast, txMined, txMinedUnconfirmed, - txScanned, - txScannedUnconfirmed, + txFauxConfirmed, + txFauxUnconfirmed, directSendResult, safResult, txCancelled, diff --git a/applications/ffi_client/recovery.js b/applications/ffi_client/recovery.js index 46c3d64265..bf82225ca2 100644 --- a/applications/ffi_client/recovery.js +++ b/applications/ffi_client/recovery.js @@ -81,15 +81,15 @@ try { } ); // callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), - const txScanned = ffi.Callback("void", ["pointer"], function (ptr) { - console.log("txScanned: ", ptr); + const txFauxConfirmed = ffi.Callback("void", ["pointer"], function (ptr) { + console.log("txFauxConfirmed: ", ptr); }); // callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), - const txScannedUnconfirmed = ffi.Callback( + const txFauxUnconfirmed = ffi.Callback( "void", ["pointer"], function (ptr, confirmations) { - console.log("txScannedUnconfirmed: ", ptr, confirmations); + console.log("txFauxUnconfirmed: ", ptr, confirmations); } ); // callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), diff --git a/applications/tari_app_grpc/src/conversions/transaction.rs b/applications/tari_app_grpc/src/conversions/transaction.rs index a6f673d8cd..f74dc1e66f 100644 --- a/applications/tari_app_grpc/src/conversions/transaction.rs +++ b/applications/tari_app_grpc/src/conversions/transaction.rs @@ -98,8 +98,8 @@ impl From for grpc::TransactionStatus { Pending => grpc::TransactionStatus::Pending, Coinbase => grpc::TransactionStatus::Coinbase, Rejected => grpc::TransactionStatus::Rejected, - FauxUnconfirmed => grpc::TransactionStatus::ScannedUnconfirmed, - FauxConfirmed => grpc::TransactionStatus::ScannedConfirmed, + FauxUnconfirmed => grpc::TransactionStatus::FauxUnconfirmed, + FauxConfirmed => grpc::TransactionStatus::FauxConfirmed, } } } 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 297164fb7c..d394bcb338 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -73,7 +73,6 @@ use tari_app_grpc::{ }, }; use tari_common_types::{ - transaction::ImportStatus, array::copy_into_fixed_array, types::{BlockHash, PublicKey, Signature}, }; @@ -592,12 +591,7 @@ impl wallet_server::Wallet for WalletGrpcServer { for o in unblinded_outputs.iter() { tx_ids.push( wallet - .import_unblinded_utxo( - o.clone(), - &CommsPublicKey::default(), - "Imported via gRPC".to_string(), - ImportStatus::Imported, - ) + .import_unblinded_utxo(o.clone(), &CommsPublicKey::default(), "Imported via gRPC".to_string()) .await .map_err(|e| Status::internal(format!("{:?}", e)))? .into(), diff --git a/base_layer/common_types/src/transaction.rs b/base_layer/common_types/src/transaction.rs index cabc8b60bd..45908a7c89 100644 --- a/base_layer/common_types/src/transaction.rs +++ b/base_layer/common_types/src/transaction.rs @@ -33,6 +33,23 @@ pub enum TransactionStatus { FauxConfirmed, } +impl TransactionStatus { + pub fn is_faux(&self) -> bool { + match self { + TransactionStatus::Completed => false, + TransactionStatus::Broadcast => false, + TransactionStatus::MinedUnconfirmed => false, + TransactionStatus::Imported => true, + TransactionStatus::Pending => false, + TransactionStatus::Coinbase => false, + TransactionStatus::MinedConfirmed => false, + TransactionStatus::Rejected => false, + TransactionStatus::FauxUnconfirmed => true, + TransactionStatus::FauxConfirmed => true, + } + } +} + #[derive(Debug, Error)] #[error("Invalid TransactionStatus: {code}")] pub struct TransactionConversionError { @@ -77,22 +94,20 @@ impl Display for TransactionStatus { TransactionStatus::Pending => write!(f, "Pending"), TransactionStatus::Coinbase => write!(f, "Coinbase"), TransactionStatus::Rejected => write!(f, "Rejected"), - TransactionStatus::FauxUnconfirmed => write!(f, "ScannedUnconfirmed"), - TransactionStatus::FauxConfirmed => write!(f, "ScannedConfirmed"), + TransactionStatus::FauxUnconfirmed => write!(f, "FauxUnconfirmed"), + TransactionStatus::FauxConfirmed => write!(f, "FauxConfirmed"), } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ImportStatus { - /// This is an invalid transaction import status that will result in an error - Invalid, /// This transaction import status is used when importing a spendable UTXO Imported, /// This transaction import status is used when a one-sided transaction has been scanned but is unconfirmed - ScannedUnconfirmed, + FauxUnconfirmed, /// This transaction import status is used when a one-sided transaction has been scanned and confirmed - ScannedConfirmed, + FauxConfirmed, } impl TryFrom for TransactionStatus { @@ -101,9 +116,8 @@ impl TryFrom for TransactionStatus { fn try_from(value: ImportStatus) -> Result { match value { ImportStatus::Imported => Ok(TransactionStatus::Imported), - ImportStatus::ScannedUnconfirmed => Ok(TransactionStatus::FauxUnconfirmed), - ImportStatus::ScannedConfirmed => Ok(TransactionStatus::FauxConfirmed), - _ => Err(TransactionConversionError { code: i32::MAX }), + ImportStatus::FauxUnconfirmed => Ok(TransactionStatus::FauxUnconfirmed), + ImportStatus::FauxConfirmed => Ok(TransactionStatus::FauxConfirmed), } } } @@ -114,19 +128,13 @@ impl TryFrom for ImportStatus { fn try_from(value: TransactionStatus) -> Result { match value { TransactionStatus::Imported => Ok(ImportStatus::Imported), - TransactionStatus::FauxUnconfirmed => Ok(ImportStatus::ScannedUnconfirmed), - TransactionStatus::FauxConfirmed => Ok(ImportStatus::ScannedConfirmed), + TransactionStatus::FauxUnconfirmed => Ok(ImportStatus::FauxUnconfirmed), + TransactionStatus::FauxConfirmed => Ok(ImportStatus::FauxConfirmed), _ => Err(TransactionConversionError { code: i32::MAX }), } } } -impl Default for ImportStatus { - fn default() -> Self { - ImportStatus::Invalid - } -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum TransactionDirection { Inbound, diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 1744ed866d..9bfd99a350 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, PublicKey}, + types::{BlockHash, HashOutput, PublicKey}, }; use tari_core::{ covenants::Covenant, @@ -41,7 +41,6 @@ use tari_crypto::{script::TariScript, tari_utilities::hex::Hex}; use tari_service_framework::reply_channel::SenderService; use tokio::sync::broadcast; use tower::Service; -use tari_common_types::types::BlockHash; use crate::output_manager_service::{ error::OutputManagerError, @@ -109,11 +108,11 @@ pub enum OutputManagerRequest { }, ScanForRecoverableOutputs { outputs: Vec, - tx_id: TxId + tx_id: TxId, }, ScanOutputs { outputs: Vec, - tx_id: TxId + tx_id: TxId, }, AddKnownOneSidedPaymentScript(KnownOneSidedPaymentScript), CreateOutputWithFeatures { @@ -172,8 +171,8 @@ impl fmt::Display for OutputManagerRequest { "FeeEstimate(amount: {}, fee_per_gram: {}, num_kernels: {}, num_outputs: {})", amount, fee_per_gram, num_kernels, num_outputs ), - ScanForRecoverableOutputs {.. } => write!(f, "ScanForRecoverableOutputs"), - ScanOutputs {.. } => write!(f, "ScanOutputs"), + ScanForRecoverableOutputs { .. } => write!(f, "ScanForRecoverableOutputs"), + ScanOutputs { .. } => write!(f, "ScanOutputs"), AddKnownOneSidedPaymentScript(_) => write!(f, "AddKnownOneSidedPaymentScript"), CreateOutputWithFeatures { value, features } => { write!(f, "CreateOutputWithFeatures({}, {})", value, features,) @@ -227,12 +226,21 @@ pub enum OutputManagerResponse { RewoundOutputs(Vec), ScanOutputs(Vec), AddKnownOneSidedPaymentScript, - CreateOutputWithFeatures { output: Box }, - CreatePayToSelfWithOutputs { transaction: Box, tx_id: TxId }, + CreateOutputWithFeatures { + output: Box, + }, + CreatePayToSelfWithOutputs { + transaction: Box, + tx_id: TxId, + }, ReinstatedCancelledInboundTx, CoinbaseAbandonedSet, ClaimHtlcTransaction((TxId, MicroTari, MicroTari, Transaction)), - OutputStatusesByTxId {statuses: Vec, mined_height: Option, block_hash: Option}, + OutputStatusesByTxId { + statuses: Vec, + mined_height: Option, + block_hash: Option, + }, } pub type OutputManagerEventSender = broadcast::Sender>; @@ -653,7 +661,7 @@ impl OutputManagerHandle { ) -> Result, OutputManagerError> { match self .handle - .call(OutputManagerRequest::ScanForRecoverableOutputs {outputs, tx_id}) + .call(OutputManagerRequest::ScanForRecoverableOutputs { outputs, tx_id }) .await?? { OutputManagerResponse::RewoundOutputs(outputs) => Ok(outputs), @@ -666,7 +674,11 @@ impl OutputManagerHandle { outputs: Vec, tx_id: TxId, ) -> Result, OutputManagerError> { - match self.handle.call(OutputManagerRequest::ScanOutputs {outputs, tx_id}).await?? { + match self + .handle + .call(OutputManagerRequest::ScanOutputs { outputs, tx_id }) + .await?? + { OutputManagerResponse::ScanOutputs(outputs) => Ok(outputs), _ => Err(OutputManagerError::UnexpectedApiResponse), } @@ -758,13 +770,20 @@ impl OutputManagerHandle { } } - pub async fn get_output_statuses_by_tx_id(&mut self, tx_id: TxId) -> Result<(Vec, Option, Option), OutputManagerError> { + pub async fn get_output_statuses_by_tx_id( + &mut self, + tx_id: TxId, + ) -> Result<(Vec, Option, Option), OutputManagerError> { match self .handle .call(OutputManagerRequest::GetOutputStatusesByTxId(tx_id)) .await?? { - OutputManagerResponse::OutputStatusesByTxId {statuses, mined_height, block_hash} => Ok((statuses, mined_height, block_hash)), + OutputManagerResponse::OutputStatusesByTxId { + statuses, + mined_height, + block_hash, + } => Ok((statuses, mined_height, block_hash)), _ => Err(OutputManagerError::UnexpectedApiResponse), } } diff --git a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs index a52a42847e..afa80139f6 100644 --- a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs +++ b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs @@ -24,7 +24,10 @@ use std::{sync::Arc, time::Instant}; use log::*; use rand::rngs::OsRng; -use tari_common_types::types::{PrivateKey, PublicKey, RangeProof}; +use tari_common_types::{ + transaction::TxId, + types::{PrivateKey, PublicKey, RangeProof}, +}; use tari_core::transactions::{ transaction::{TransactionOutput, UnblindedOutput}, CryptoFactories, @@ -34,7 +37,6 @@ use tari_crypto::{ keys::{PublicKey as PublicKeyTrait, SecretKey}, tari_utilities::hex::Hex, }; -use tari_common_types::transaction::TxId; use crate::output_manager_service::{ error::{OutputManagerError, OutputManagerStorageError}, diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 1385666443..908ca3427b 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -29,7 +29,7 @@ use log::*; use rand::{rngs::OsRng, RngCore}; use tari_common_types::{ transaction::TxId, - types::{HashOutput, PrivateKey, PublicKey}, + types::{BlockHash, HashOutput, PrivateKey, PublicKey}, }; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_core::{ @@ -61,7 +61,6 @@ use tari_crypto::{ script::TariScript, tari_utilities::{hex::Hex, ByteArray}, }; -use tari_common_types::types::BlockHash; use tari_key_manager::cipher_seed::CipherSeed; use tari_service_framework::reply_channel; use tari_shutdown::ShutdownSignal; @@ -359,7 +358,7 @@ where OutputManagerRequest::GetPublicRewindKeys => Ok(OutputManagerResponse::PublicRewindKeys(Box::new( self.resources.master_key_manager.get_rewind_public_keys(), ))), - OutputManagerRequest::ScanForRecoverableOutputs {outputs, tx_id} => StandardUtxoRecoverer::new( + OutputManagerRequest::ScanForRecoverableOutputs { outputs, tx_id } => StandardUtxoRecoverer::new( self.resources.master_key_manager.clone(), self.resources.factories.clone(), self.resources.db.clone(), @@ -367,7 +366,7 @@ where .scan_and_recover_outputs(outputs, tx_id) .await .map(OutputManagerResponse::RewoundOutputs), - OutputManagerRequest::ScanOutputs {outputs, tx_id} => self + OutputManagerRequest::ScanOutputs { outputs, tx_id } => self .scan_outputs_for_one_sided_payments(outputs, tx_id) .await .map(OutputManagerResponse::ScanOutputs), @@ -416,29 +415,36 @@ where .create_htlc_refund_transaction(output, fee_per_gram) .await .map(OutputManagerResponse::ClaimHtlcTransaction), - OutputManagerRequest::GetOutputStatusesByTxId(tx_id) => self - .get_output_status_by_tx_id(tx_id) - .await - .map(OutputManagerResponse::OutputStatusesByTxId), + OutputManagerRequest::GetOutputStatusesByTxId(tx_id) => { + let (statuses, mined_height, block_hash) = self.get_output_status_by_tx_id(tx_id).await?; + Ok(OutputManagerResponse::OutputStatusesByTxId { + statuses, + mined_height, + block_hash, + }) + }, } } - async fn get_output_status_by_tx_id(&self, tx_id: TxId) -> Result<(Vec, Option, Option), OutputManagerError> { + async fn get_output_status_by_tx_id( + &self, + tx_id: TxId, + ) -> Result<(Vec, Option, Option), OutputManagerError> { let outputs = self.resources.db.fetch_outputs_by_tx_id(tx_id).await?; - let statuses = outputs.into_iter().map(|uo| uo.status).collect(); - let mined_heights = outputs.into_iter().map(|uo| uo.mined_height).collect(); - let mined_height: Option = if let Some(height) = mined_heights.iter().min() { - Some(*height) - } else { - None - }; - let block_hashes = outputs.into_iter().map(|uo| uo.mined_in_block).collect(); - let block_hash: Option = if let Some(hash) = block_hashes.iter().min() { - Some(*hash) - } else { - None - }; - Ok((statuses, mined_height, block_hash)) + let statuses = outputs.clone().into_iter().map(|uo| uo.status).collect(); + // We need the maximum mined height and corresponding block hash (faux transactions outputs can have different + // mined heights) + let (mut last_height, mut max_mined_height, mut block_hash) = (0u64, None, None); + let _ = outputs.iter().map(|uo| { + if let Some(height) = uo.mined_height { + if last_height < height { + last_height = height; + max_mined_height = uo.mined_height; + block_hash = uo.mined_in_block.clone(); + } + } + }); + Ok((statuses, max_mined_height, block_hash)) } async fn claim_sha_atomic_swap_with_hash( diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 35590eb022..d996d7cf74 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -76,7 +76,13 @@ pub enum TransactionServiceRequest { SendShaAtomicSwapTransaction(CommsPublicKey, MicroTari, MicroTari, String), CancelTransaction(TxId), ImportUtxoWithStatus { - amount: MicroTari, source_public_key: CommsPublicKey, message: String, maturity: Option, import_status: ImportStatus, tx_id: Option, current_height: Option + amount: MicroTari, + source_public_key: CommsPublicKey, + message: String, + maturity: Option, + import_status: ImportStatus, + tx_id: Option, + current_height: Option, }, SubmitTransactionToSelf(TxId, Transaction, MicroTari, MicroTari, String), SetLowPowerMode, @@ -128,7 +134,15 @@ impl fmt::Display for TransactionServiceRequest { f.write_str(&format!("SendShaAtomicSwapTransaction (to {}, {}, {})", k, v, msg)) }, Self::CancelTransaction(t) => f.write_str(&format!("CancelTransaction ({})", t)), - Self::ImportUtxoWithStatus {amount, source_public_key, message, maturity, import_status, tx_id, current_height } => f.write_str(&format!( + Self::ImportUtxoWithStatus { + amount, + source_public_key, + message, + maturity, + import_status, + tx_id, + current_height, + } => f.write_str(&format!( "ImportUtxo (from {}, {}, {} with maturity {} and {:?} and {:?} and {:?})", source_public_key, amount, @@ -266,12 +280,12 @@ impl fmt::Display for TransactionEvent { } => { write!( f, - "TransactionScannedUnconfirmed for {} with num confirmations: {}. is_valid: {}", + "FauxTransactionUnconfirmed for {} with num confirmations: {}. is_valid: {}", tx_id, num_confirmations, is_valid ) }, TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid } => { - write!(f, "TransactionScanned for {}. is_valid: {}", tx_id, is_valid) + write!(f, "FauxTransactionConfirmed for {}. is_valid: {}", tx_id, is_valid) }, TransactionEvent::TransactionMined { tx_id, is_valid } => { write!(f, "TransactionMined for {}. is_valid: {}", tx_id, is_valid) @@ -556,7 +570,7 @@ impl TransactionServiceHandle { maturity: Option, import_status: ImportStatus, tx_id: Option, - current_height: Option + current_height: Option, ) -> Result { match self .handle diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs index afc59bbcb7..3572317408 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs @@ -388,8 +388,6 @@ where mined_height: u64, num_confirmations: u64, ) -> Result<(), TransactionServiceProtocolError> { - let is_faux = - *status == TransactionStatus::FauxUnconfirmed || *status == TransactionStatus::FauxConfirmed; self.db .set_transaction_mined_height( tx_id, @@ -398,18 +396,18 @@ where mined_in_block.clone(), num_confirmations, num_confirmations >= self.config.num_confirmations_required, - is_faux, + status.is_faux(), ) .await .for_protocol(self.operation_id.as_u64())?; if num_confirmations >= self.config.num_confirmations_required { - if is_faux { + if status.is_faux() { self.publish_event(TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid: true }) } else { self.publish_event(TransactionEvent::TransactionMined { tx_id, is_valid: true }) } - } else if is_faux { + } else if status.is_faux() { self.publish_event(TransactionEvent::FauxTransactionUnconfirmed { tx_id, num_confirmations, @@ -479,10 +477,8 @@ where tx_id: TxId, status: &TransactionStatus, ) -> Result<(), TransactionServiceProtocolError> { - let is_faux = - *status == TransactionStatus::FauxUnconfirmed || *status == TransactionStatus::FauxConfirmed; self.db - .set_transaction_as_unmined(tx_id, is_faux) + .set_transaction_as_unmined(tx_id) .await .for_protocol(self.operation_id.as_u64())?; diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 757a881b11..3313a47db0 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -34,7 +34,7 @@ use log::*; use rand::rngs::OsRng; use sha2::Sha256; use tari_common_types::{ - transaction::{ImportStatus, TransactionConversionError, TransactionDirection, TransactionStatus, TxId}, + transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, types::{PrivateKey, PublicKey}, }; use tari_comms::{peer_manager::NodeIdentity, types::CommsPublicKey}; @@ -68,7 +68,6 @@ use tokio::{ sync::{mpsc, mpsc::Sender, oneshot}, task::JoinHandle, }; -use tari_common_types::chain_metadata::ChainMetadata; use crate::{ base_node_service::handle::{BaseNodeEvent, BaseNodeServiceHandle}, @@ -654,7 +653,15 @@ where tx_id, current_height, } => self - .add_utxo_import_transaction_with_status(amount, source_public_key, message, maturity, import_status, tx_id, current_height) + .add_utxo_import_transaction_with_status( + amount, + source_public_key, + message, + maturity, + import_status, + tx_id, + current_height, + ) .await .map(TransactionServiceResponse::UtxoImported), TransactionServiceRequest::SubmitTransactionToSelf(tx_id, tx, fee, amount, message) => self @@ -757,10 +764,13 @@ where if let OutputManagerEvent::TxoValidationSuccess(_) = (*event).clone() { let db = self.db.clone(); let output_manager_handle = self.output_manager_service.clone(); - let metadata = self.wallet_db.get_chain_metadata().await?; + let metadata = match self.wallet_db.get_chain_metadata().await { + Ok(data) => data, + Err(_) => None, + }; let tip_height = match metadata { + Some(val) => val.height_of_longest_chain(), None => 0u64, - Some(v) => v.height_of_longest_chain(), }; tokio::spawn(check_faux_transactions(output_manager_handle, db, tip_height)); } @@ -2050,11 +2060,7 @@ where tx_id: Option, current_height: Option, ) -> Result { - let tx_id = if let Some(id) = tx_id { - id - } else { - TxId::new_random(); - }; + let tx_id = if let Some(id) = tx_id { id } else { TxId::new_random() }; self.db .add_utxo_import_transaction_with_status( tx_id, @@ -2068,18 +2074,13 @@ where ) .await?; let transaction_event = match import_status { - ImportStatus::Invalid => { - return Err(TransactionServiceError::ConversionError(TransactionConversionError { - code: i32::MAX, - })) - }, ImportStatus::Imported => TransactionEvent::TransactionImported(tx_id), - ImportStatus::ScannedUnconfirmed => TransactionEvent::FauxTransactionUnconfirmed { + ImportStatus::FauxUnconfirmed => TransactionEvent::FauxTransactionUnconfirmed { tx_id, num_confirmations: 0, is_valid: true, }, - ImportStatus::ScannedConfirmed => TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid: true }, + ImportStatus::FauxConfirmed => TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid: true }, }; let _ = self.event_publisher.send(Arc::new(transaction_event)).map_err(|e| { trace!( diff --git a/base_layer/wallet/src/transaction_service/storage/database.rs b/base_layer/wallet/src/transaction_service/storage/database.rs index 5708153e9a..d55a794272 100644 --- a/base_layer/wallet/src/transaction_service/storage/database.rs +++ b/base_layer/wallet/src/transaction_service/storage/database.rs @@ -136,7 +136,7 @@ pub trait TransactionBackend: Send + Sync + Clone { is_faux: bool, ) -> Result<(), TransactionStorageError>; /// Clears the mined block and height of a transaction - fn set_transaction_as_unmined(&self, tx_id: TxId, is_faux: bool) -> Result<(), TransactionStorageError>; + fn set_transaction_as_unmined(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; /// Reset optional 'mined height' and 'mined in block' fields to nothing fn mark_all_transactions_as_unvalidated(&self) -> Result<(), TransactionStorageError>; /// Light weight method to retrieve pertinent transaction sender info for all pending inbound transactions @@ -145,7 +145,10 @@ pub trait TransactionBackend: Send + Sync + Clone { ) -> Result, TransactionStorageError>; fn fetch_imported_transactions(&self) -> Result, TransactionStorageError>; fn fetch_unconfirmed_faux_transactions(&self) -> Result, TransactionStorageError>; - fn fetch_confirmed_faux_transactions_from_height(&self, height: u64) -> Result, TransactionStorageError>; + fn fetch_confirmed_faux_transactions_from_height( + &self, + height: u64, + ) -> Result, TransactionStorageError>; } #[derive(Clone, PartialEq)] @@ -821,13 +824,9 @@ where T: TransactionBackend + 'static Ok(()) } - pub async fn set_transaction_as_unmined( - &self, - tx_id: TxId, - is_faux: bool, - ) -> Result<(), TransactionStorageError> { + pub async fn set_transaction_as_unmined(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { let db_clone = self.db.clone(); - tokio::task::spawn_blocking(move || db_clone.set_transaction_as_unmined(tx_id, is_faux)) + tokio::task::spawn_blocking(move || db_clone.set_transaction_as_unmined(tx_id)) .await .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(()) diff --git a/base_layer/wallet/src/transaction_service/storage/models.rs b/base_layer/wallet/src/transaction_service/storage/models.rs index e6377f450a..756ed3312b 100644 --- a/base_layer/wallet/src/transaction_service/storage/models.rs +++ b/base_layer/wallet/src/transaction_service/storage/models.rs @@ -160,7 +160,6 @@ impl CompletedTransaction { direction: TransactionDirection, coinbase_block_height: Option, mined_height: Option, - ) -> Self { let transaction_signature = if let Some(excess_sig) = transaction.first_kernel_excess_sig() { excess_sig.clone() diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index 8b70f35f11..a3a6566a86 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -1165,13 +1165,13 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Ok(()) } - fn set_transaction_as_unmined(&self, tx_id: TxId, is_faux: bool) -> Result<(), TransactionStorageError> { + fn set_transaction_as_unmined(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); match CompletedTransactionSql::find(tx_id, &conn) { Ok(v) => { - v.set_as_unmined(&conn, is_faux)?; + v.set_as_unmined(&conn)?; }, Err(TransactionStorageError::DieselError(DieselError::NotFound)) => { return Err(TransactionStorageError::ValueNotFound(DbKey::CompletedTransaction( @@ -1245,17 +1245,25 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { .collect::, TransactionStorageError>>() } - fn fetch_confirmed_faux_transactions_from_height(&self, height: u64) -> Result, TransactionStorageError> { + fn fetch_confirmed_faux_transactions_from_height( + &self, + height: u64, + ) -> Result, TransactionStorageError> { let conn = self.database_connection.get_pooled_connection()?; - CompletedTransactionSql::index_by_status_and_cancelled_from_block_height(TransactionStatus::FauxConfirmed, false, height as i64, &conn)? - .into_iter() - .map(|mut ct: CompletedTransactionSql| { - if let Err(e) = self.decrypt_if_necessary(&mut ct) { - return Err(e); - } - CompletedTransaction::try_from(ct).map_err(TransactionStorageError::from) - }) - .collect::, TransactionStorageError>>() + CompletedTransactionSql::index_by_status_and_cancelled_from_block_height( + TransactionStatus::FauxConfirmed, + false, + height as i64, + &conn, + )? + .into_iter() + .map(|mut ct: CompletedTransactionSql| { + if let Err(e) = self.decrypt_if_necessary(&mut ct) { + return Err(e); + } + CompletedTransaction::try_from(ct).map_err(TransactionStorageError::from) + }) + .collect::, TransactionStorageError>>() } } @@ -1714,7 +1722,7 @@ impl CompletedTransactionSql { Ok(completed_transactions::table .filter(completed_transactions::cancelled.eq(cancelled as i32)) .filter(completed_transactions::status.eq(status as i32)) - .filter(completed_transactions::coinbase_block_height.ge(block_height)) + .filter(completed_transactions::mined_height.ge(block_height)) .load::(conn)?) } @@ -1794,10 +1802,10 @@ impl CompletedTransactionSql { Ok(()) } - pub fn set_as_unmined(&self, conn: &SqliteConnection, is_faux: bool) -> Result<(), TransactionStorageError> { + pub fn set_as_unmined(&self, conn: &SqliteConnection) -> Result<(), TransactionStorageError> { let status = if self.coinbase_block_height.is_some() { Some(TransactionStatus::Coinbase as i32) - } else if is_faux { + } else if self.status == TransactionStatus::FauxConfirmed as i32 { Some(TransactionStatus::FauxUnconfirmed as i32) } else if self.status == TransactionStatus::Broadcast as i32 { Some(TransactionStatus::Broadcast as i32) @@ -2052,8 +2060,8 @@ impl UnconfirmedTransactionInfoSql { .filter( completed_transactions::status .ne(TransactionStatus::Imported as i32) - .ne(TransactionStatus::FauxUnconfirmed as i32) - .ne(TransactionStatus::FauxConfirmed as i32) + .and(completed_transactions::status.ne(TransactionStatus::FauxUnconfirmed as i32)) + .and(completed_transactions::status.ne(TransactionStatus::FauxConfirmed as i32)) .and( completed_transactions::mined_height .is_null() diff --git a/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs index 50e0d1ec8f..eb1f0366a0 100644 --- a/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs +++ b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs @@ -21,15 +21,18 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use log::*; +use tari_common_types::types::BlockHash; use crate::{ output_manager_service::{handle::OutputManagerHandle, storage::OutputStatus}, transaction_service::{ config::TransactionServiceConfig, - storage::database::{TransactionBackend, TransactionDatabase}, + storage::{ + database::{TransactionBackend, TransactionDatabase}, + models::CompletedTransaction, + }, }, }; -use crate::transaction_service::storage::models::CompletedTransaction; const LOG_TARGET: &str = "wallet::transaction_service::service"; @@ -38,27 +41,32 @@ pub async fn check_faux_transactions( db: TransactionDatabase, tip_height: u64, ) { - let mut all_faux_transactions: Vec; - let mut imported_transactions = match db.get_imported_transactions().await { + let mut all_faux_transactions: Vec = match db.get_imported_transactions().await { Ok(txs) => txs, Err(e) => { error!(target: LOG_TARGET, "Problem retrieving imported transactions: {}", e); return; }, }; - all_faux_transactions.append(&mut imported_transactions); let mut unconfirmed_faux = match db.get_unconfirmed_faux_transactions().await { Ok(txs) => txs, Err(e) => { - error!(target: LOG_TARGET, "Problem retrieving unconfirmed faux transactions: {}", e); + error!( + target: LOG_TARGET, + "Problem retrieving unconfirmed faux transactions: {}", e + ); return; }, }; all_faux_transactions.append(&mut unconfirmed_faux); // Reorged faux transactions cannot be detected by excess signature, thus use last known confirmed transaction // height or current tip height with safety margin to determine if these should be returned - let height_with_margin = tip_height.checked_sub(100).unwrap_or(0); - let check_height = if let Some(tx) = db.fetch_last_mined_transaction().await? { + let last_mined_transaction = match db.fetch_last_mined_transaction().await { + Ok(tx) => tx, + Err(_) => None, + }; + let height_with_margin = tip_height.saturating_sub(100); + let check_height = if let Some(tx) = last_mined_transaction { tx.mined_height.unwrap_or(height_with_margin) } else { height_with_margin @@ -66,12 +74,20 @@ pub async fn check_faux_transactions( let mut confirmed_faux = match db.get_confirmed_faux_transactions_from_height(check_height).await { Ok(txs) => txs, Err(e) => { - error!(target: LOG_TARGET, "Problem retrieving confirmed faux transactions: {}", e); + error!( + target: LOG_TARGET, + "Problem retrieving confirmed faux transactions: {}", e + ); return; }, }; all_faux_transactions.append(&mut confirmed_faux); + debug!( + target: LOG_TARGET, + "Checking {} faux transaction statuses", + all_faux_transactions.len() + ); for tx in all_faux_transactions.into_iter() { let (status, mined_height, block_hash) = match output_manager.get_output_statuses_by_tx_id(tx.tx_id).await { Ok(s) => s, @@ -81,29 +97,33 @@ pub async fn check_faux_transactions( }, }; if !status.iter().any(|s| s != &OutputStatus::Unspent) { - debug!( - target: LOG_TARGET, - "Faux Transaction (TxId: {}) updated to confirmed", tx.tx_id - ); - let mined_height= if let Some(height) = mined_height { - *height + let mined_height = if let Some(height) = mined_height { + height } else { - 0 + tip_height }; - let mined_in_block= if let Some(hash) = block_hash { - *gash + let mined_in_block: BlockHash = if let Some(hash) = block_hash { + hash } else { vec![0u8; 32] }; - let confirmations = tip_height.checked_sub(mined_height).unwrap_or(0); - let is_confirmed = confirmations >= TransactionServiceConfig::default().num_confirmations_required; + let is_confirmed = tip_height.saturating_sub(mined_height) >= + TransactionServiceConfig::default().num_confirmations_required; + debug!( + target: LOG_TARGET, + "Updating faux transaction: TxId({}), mined_height({}), is_confirmed({}), num_confirmations({})", + tx.tx_id, + mined_height, + is_confirmed, + tip_height - mined_height, + ); if let Err(e) = db .set_transaction_mined_height( tx.tx_id, true, mined_height, mined_in_block, - tip_height - mined_heigh, + tip_height - mined_height, is_confirmed, true, ) diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index 0625cfb91d..b859669538 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -433,7 +433,9 @@ where TBackend: WalletBackend + 'static let (tx_id, found_outputs) = self.scan_for_outputs(outputs).await?; scan_for_outputs_profiling.push(start.elapsed()); - let (count, amount) = self.import_utxos_to_transaction_service(found_outputs, tx_id, current_height).await?; + let (count, amount) = self + .import_utxos_to_transaction_service(found_outputs, tx_id, current_height) + .await?; self.resources .db @@ -484,8 +486,8 @@ where TBackend: WalletBackend + 'static async fn scan_for_outputs( &mut self, outputs: Vec, - ) -> Result<(TxId, Vec<(UnblindedOutput, String, ImportStatus)>), UtxoScannerError> { - let mut found_outputs: Vec<(UnblindedOutput, String, ImportStatus)> = Vec::new(); + ) -> Result<(TxId, Vec<(UnblindedOutput, String)>), UtxoScannerError> { + let mut found_outputs: Vec<(UnblindedOutput, String)> = Vec::new(); let tx_id = TxId::new_random(); if self.mode == UtxoScannerMode::Recovery { found_outputs.append( @@ -495,13 +497,7 @@ where TBackend: WalletBackend + 'static .scan_for_recoverable_outputs(outputs.clone(), tx_id) .await? .into_iter() - .map(|v| { - ( - v, - format!("Recovered on {}.", Utc::now().naive_utc()), - ImportStatus::Imported, - ) - }) + .map(|uo| (uo, format!("Recovered output on {}.", Utc::now().naive_utc()))) .collect(), ); }; @@ -512,11 +508,10 @@ where TBackend: WalletBackend + 'static .scan_outputs_for_one_sided_payments(outputs.clone(), tx_id) .await? .into_iter() - .map(|v| { + .map(|uo| { ( - v, - format!("Detected one-sided transaction on {}.", Utc::now().naive_utc()), - ImportStatus::ScannedUnconfirmed, + uo, + format!("Detected one-sided transaction output on {}.", Utc::now().naive_utc()), ) }) .collect(), @@ -526,7 +521,7 @@ where TBackend: WalletBackend + 'static async fn import_utxos_to_transaction_service( &mut self, - utxos: Vec<(UnblindedOutput, String, ImportStatus)>, + utxos: Vec<(UnblindedOutput, String)>, tx_id: TxId, current_height: u64, ) -> Result<(u64, MicroTari), UtxoScannerError> { @@ -534,14 +529,20 @@ where TBackend: WalletBackend + 'static let mut total_amount = MicroTari::from(0); let source_public_key = self.resources.node_identity.public_key().clone(); - for uo in utxos { + for (uo, message) in utxos { match self - .import_unblinded_utxo_to_transaction_service(uo.0.clone(), &source_public_key, uo.1, uo.2, tx_id, current_height) + .import_unblinded_utxo_to_transaction_service( + uo.clone(), + &source_public_key, + message, + tx_id, + current_height, + ) .await { Ok(_) => { num_recovered = num_recovered.saturating_add(1); - total_amount += uo.0.value; + total_amount += uo.value; }, Err(e) => return Err(UtxoScannerError::UtxoImportError(e.to_string())), } @@ -578,14 +579,13 @@ where TBackend: WalletBackend + 'static let _ = self.event_sender.send(event); } - /// A faux incoming transaction will be created to provide a record of the event of importing a UTXO. The TxId of - /// the generated transaction is returned. + /// A faux incoming transaction will be created to provide a record of the event of importing a scanned UTXO. The + /// TxId of the generated transaction is returned. pub async fn import_unblinded_utxo_to_transaction_service( &mut self, unblinded_output: UnblindedOutput, source_public_key: &CommsPublicKey, message: String, - import_status: ImportStatus, tx_id: TxId, current_height: u64, ) -> Result { @@ -597,21 +597,20 @@ where TBackend: WalletBackend + 'static source_public_key.clone(), message, Some(unblinded_output.features.maturity), - import_status.clone(), + ImportStatus::FauxUnconfirmed, Some(tx_id), - Some(current_height) + Some(current_height), ) .await?; info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet as '{:?}'", + "UTXO (Commitment: {}) imported into wallet as 'ImportStatus::FauxUnconfirmed'", unblinded_output .as_transaction_input(&self.resources.factories.commitment)? .commitment() .map_err(WalletError::TransactionError)? .to_hex(), - import_status, ); Ok(tx_id) diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index fbb0cf6197..f53d1c6eda 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -385,7 +385,6 @@ where sender_offset_public_key: &PublicKey, script_lock_height: u64, covenant: Covenant, - import_status: ImportStatus, ) -> Result { let unblinded_output = UnblindedOutput::new_current_version( amount, @@ -407,7 +406,7 @@ where source_public_key.clone(), message, Some(features.maturity), - import_status.clone(), + ImportStatus::Imported, None, None, ) @@ -425,7 +424,8 @@ where info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet as '{:?}'", commitment_hex, import_status + "UTXO (Commitment: {}) imported into wallet as 'ImportStatus::Imported'", commitment_hex + ); Ok(tx_id) @@ -439,7 +439,6 @@ where unblinded_output: UnblindedOutput, source_public_key: &CommsPublicKey, message: String, - import_status: ImportStatus, ) -> Result { let tx_id = self .transaction_service @@ -448,7 +447,7 @@ where source_public_key.clone(), message, Some(unblinded_output.features.maturity), - import_status.clone(), + ImportStatus::Imported, None, None, ) @@ -460,13 +459,12 @@ where info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet as '{:?}'", + "UTXO (Commitment: {}) imported into wallet as 'ImportStatus::Imported'", unblinded_output .as_transaction_input(&self.factories.commitment)? .commitment() .map_err(WalletError::TransactionError)? .to_hex(), - import_status ); Ok(tx_id) 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 dbe7909342..10de433c8d 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -1904,12 +1904,12 @@ async fn test_get_status_by_tx_id() { let (mut oms, _, _shutdown, _, _, _, _, _) = setup_output_manager_service(backend, true).await; let (_ti, uo1) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment); - oms.add_unvalidated_output(TxId::from(1), uo1, None).await.unwrap(); + oms.add_unvalidated_output(TxId::from(1u64), uo1, None).await.unwrap(); let (_ti, uo2) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment); - oms.add_unvalidated_output(TxId::from(2), uo2, None).await.unwrap(); + oms.add_unvalidated_output(TxId::from(2u64), uo2, None).await.unwrap(); - let status = oms.get_output_statuses_by_tx_id(TxId::from(1)).await.unwrap(); + let (status, _, _) = oms.get_output_statuses_by_tx_id(TxId::from(1u64)).await.unwrap(); assert_eq!(status.len(), 1); assert_eq!(status[0], OutputStatus::EncumberedToBeReceived); diff --git a/base_layer/wallet/tests/support/output_manager_service_mock.rs b/base_layer/wallet/tests/support/output_manager_service_mock.rs index 3ae2ebb103..1837418439 100644 --- a/base_layer/wallet/tests/support/output_manager_service_mock.rs +++ b/base_layer/wallet/tests/support/output_manager_service_mock.rs @@ -96,7 +96,10 @@ impl OutputManagerServiceMock { ) { info!(target: LOG_TARGET, "Handling Request: {}", request); match request { - OutputManagerRequest::ScanForRecoverableOutputs {outputs: _to, tx_id: _tx_id} => { + OutputManagerRequest::ScanForRecoverableOutputs { + outputs: requested_outputs, + tx_id: _tx_id, + } => { let lock = acquire_lock!(self.state.recoverable_outputs); let outputs = (*lock) .clone() @@ -117,7 +120,10 @@ impl OutputManagerServiceMock { e }); }, - OutputManagerRequest::ScanOutputs {outputs: _to, tx_id: _tx_id} => { + OutputManagerRequest::ScanOutputs { + outputs: _to, + tx_id: _tx_id, + } => { let lock = acquire_lock!(self.state.one_sided_payments); let outputs = (*lock).clone(); let _ = reply_tx diff --git a/base_layer/wallet/tests/support/transaction_service_mock.rs b/base_layer/wallet/tests/support/transaction_service_mock.rs index 40949b8cf8..f4bb390242 100644 --- a/base_layer/wallet/tests/support/transaction_service_mock.rs +++ b/base_layer/wallet/tests/support/transaction_service_mock.rs @@ -95,9 +95,9 @@ impl TransactionServiceMock { info!(target: LOG_TARGET, "Handling Request: {}", request); match request { - TransactionServiceRequest::ImportUtxoWithStatus(_, _, _, _, _, , _) => { + TransactionServiceRequest::ImportUtxoWithStatus { .. } => { let _ = reply_tx - .send(Ok(TransactionServiceResponse::UtxoImported(TxId::from(42)))) + .send(Ok(TransactionServiceResponse::UtxoImported(TxId::from(42u64)))) .map_err(|e| { warn!(target: LOG_TARGET, "Failed to send reply"); e diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index c988bfa6fd..b2632638d7 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -908,7 +908,7 @@ fn recover_one_sided_transaction() { let outputs = completed_tx.transaction.body.outputs().clone(); let unblinded = bob_oms - .scan_outputs_for_one_sided_payments(outputs.clone(), TxId::new_random()) + .scan_outputs_for_one_sided_payments(outputs.clone(), TxId::new_random()) .await .unwrap(); // Bob should be able to claim 1 output. @@ -916,7 +916,10 @@ fn recover_one_sided_transaction() { assert_eq!(value, unblinded[0].value); // Should ignore already existing outputs - let unblinded = bob_oms.scan_outputs_for_one_sided_payments(outputs, TxId::new_random()).await.unwrap(); + let unblinded = bob_oms + .scan_outputs_for_one_sided_payments(outputs, TxId::new_random()) + .await + .unwrap(); assert!(unblinded.is_empty()); }); } @@ -5411,7 +5414,7 @@ fn test_update_faux_tx_on_oms_validation() { alice_ts_interface.base_node_identity.public_key().clone(), "one-sided 1".to_string(), None, - ImportStatus::ScannedUnconfirmed, + ImportStatus::FauxUnconfirmed, None, None, )) @@ -5422,7 +5425,7 @@ fn test_update_faux_tx_on_oms_validation() { alice_ts_interface.base_node_identity.public_key().clone(), "one-sided 2".to_string(), None, - ImportStatus::ScannedConfirmed, + ImportStatus::FauxConfirmed, None, None, )) @@ -5457,27 +5460,27 @@ fn test_update_faux_tx_on_oms_validation() { if let WalletTransaction::Completed(tx) = &transaction { assert_eq!(tx.status, TransactionStatus::FauxUnconfirmed); } else { - panic!("Should find a complete ScannedUnconfirmed transaction"); + panic!("Should find a complete FauxUnconfirmed transaction"); } } if tx_id == tx_id_3 { if let WalletTransaction::Completed(tx) = &transaction { assert_eq!(tx.status, TransactionStatus::FauxConfirmed); } else { - panic!("Should find a complete ScannedConfirmed transaction"); + panic!("Should find a complete FauxConfirmed transaction"); } } } - // This will only change the status of the imported transaction + // This will change the status of the imported transaction alice_ts_interface .output_manager_service_event_publisher .send(Arc::new(OutputManagerEvent::TxoValidationSuccess(1u64))) .unwrap(); let mut found_imported = false; - let mut found_scanned_unconfirmed = false; - let mut found_scanned_confirmed = false; + let mut found_faux_unconfirmed = false; + let mut found_faux_confirmed = false; for _ in 0..20 { runtime.block_on(async { sleep(Duration::from_secs(1)).await }); for tx_id in [tx_id_1, tx_id_2, tx_id_3] { @@ -5486,23 +5489,23 @@ fn test_update_faux_tx_on_oms_validation() { .unwrap() .unwrap(); if let WalletTransaction::Completed(tx) = transaction { - if tx_id == tx_id_1 && tx.status == TransactionStatus::MinedConfirmed { + if tx_id == tx_id_1 && tx.status == TransactionStatus::FauxUnconfirmed && !found_imported { found_imported = true; } - if tx_id == tx_id_2 && tx.status == TransactionStatus::FauxUnconfirmed { - found_scanned_unconfirmed = true; + if tx_id == tx_id_2 && tx.status == TransactionStatus::FauxUnconfirmed && !found_faux_unconfirmed { + found_faux_unconfirmed = true; } - if tx_id == tx_id_3 && tx.status == TransactionStatus::FauxConfirmed { - found_scanned_confirmed = true; + if tx_id == tx_id_3 && tx.status == TransactionStatus::FauxConfirmed && !found_faux_confirmed { + found_faux_confirmed = true; } } } - if found_imported && found_scanned_unconfirmed && found_scanned_confirmed { + if found_imported && found_faux_unconfirmed && found_faux_confirmed { break; } } assert!( - found_imported && found_scanned_unconfirmed && found_scanned_confirmed, + found_imported && found_faux_unconfirmed && found_faux_confirmed, "Should have found the updated statuses" ); } diff --git a/base_layer/wallet/tests/transaction_service_tests/storage.rs b/base_layer/wallet/tests/transaction_service_tests/storage.rs index 1bca65ec66..b23b8876ab 100644 --- a/base_layer/wallet/tests/transaction_service_tests/storage.rs +++ b/base_layer/wallet/tests/transaction_service_tests/storage.rs @@ -551,7 +551,7 @@ pub fn test_db_backend(backend: T) { assert_eq!(unmined_txs.len(), 4); runtime - .block_on(db.set_transaction_as_unmined(completed_txs[0].tx_id, false)) + .block_on(db.set_transaction_as_unmined(completed_txs[0].tx_id)) .unwrap(); let unmined_txs = runtime.block_on(db.fetch_unconfirmed_transactions_info()).unwrap(); @@ -596,7 +596,7 @@ async fn import_tx_and_read_it_from_db() { let sqlite_db = TransactionServiceSqliteDatabase::new(connection, Some(cipher)); let transaction = CompletedTransaction::new( - TxId::from(1), + TxId::from(1u64), PublicKey::default(), PublicKey::default(), MicroTari::from(100000), @@ -618,13 +618,13 @@ async fn import_tx_and_read_it_from_db() { sqlite_db .write(WriteOperation::Insert(DbKeyValuePair::CompletedTransaction( - TxId::from(1), + TxId::from(1u64), Box::new(transaction), ))) .unwrap(); let transaction = CompletedTransaction::new( - TxId::from(2), + TxId::from(2u64), PublicKey::default(), PublicKey::default(), MicroTari::from(100000), @@ -646,13 +646,13 @@ async fn import_tx_and_read_it_from_db() { sqlite_db .write(WriteOperation::Insert(DbKeyValuePair::CompletedTransaction( - TxId::from(2), + TxId::from(2u64), Box::new(transaction), ))) .unwrap(); let transaction = CompletedTransaction::new( - TxId::from(3), + TxId::from(3u64), PublicKey::default(), PublicKey::default(), MicroTari::from(100000), @@ -674,7 +674,7 @@ async fn import_tx_and_read_it_from_db() { sqlite_db .write(WriteOperation::Insert(DbKeyValuePair::CompletedTransaction( - TxId::from(3), + TxId::from(3u64), Box::new(transaction), ))) .unwrap(); @@ -682,17 +682,17 @@ async fn import_tx_and_read_it_from_db() { let db_tx = sqlite_db.fetch_imported_transactions().unwrap(); assert_eq!(db_tx.len(), 1); assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(1)); - assert_eq!(db_tx.first().unwrap().mined_height, 5); + assert_eq!(db_tx.first().unwrap().mined_height, Some(5)); let db_tx = sqlite_db.fetch_unconfirmed_faux_transactions().unwrap(); assert_eq!(db_tx.len(), 1); assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(2)); - assert_eq!(db_tx.first().unwrap().mined_height, 6); + assert_eq!(db_tx.first().unwrap().mined_height, Some(6)); let db_tx = sqlite_db.fetch_confirmed_faux_transactions_from_height(10).unwrap(); assert_eq!(db_tx.len(), 0); let db_tx = sqlite_db.fetch_confirmed_faux_transactions_from_height(4).unwrap(); assert_eq!(db_tx.len(), 1); assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(3)); - assert_eq!(db_tx.first().unwrap().mined_height, 7); + assert_eq!(db_tx.first().unwrap().mined_height, Some(7)); } diff --git a/base_layer/wallet/tests/wallet.rs b/base_layer/wallet/tests/wallet.rs index 5b5c4b08bc..0de92e0415 100644 --- a/base_layer/wallet/tests/wallet.rs +++ b/base_layer/wallet/tests/wallet.rs @@ -48,7 +48,7 @@ use rand::rngs::OsRng; use support::{comms_and_services::get_next_memory_address, utils::make_input}; use tari_common_types::{ chain_metadata::ChainMetadata, - transaction::{ImportStatus, TransactionStatus}, + transaction::TransactionStatus, types::{PrivateKey, PublicKey}, }; use tari_comms::{ @@ -757,8 +757,6 @@ async fn test_import_utxo() { let p = TestParams::new(); let utxo_1 = create_unblinded_output(script.clone(), features.clone(), p.clone(), 20000 * uT); - let utxo_2 = create_unblinded_output(script.clone(), features.clone(), p.clone(), 30000 * uT); - let utxo_3 = create_unblinded_output(script.clone(), features.clone(), p.clone(), 40000 * uT); let output = utxo.as_transaction_output(&factories).unwrap(); let expected_output_hash = output.hash(); @@ -776,50 +774,13 @@ async fn test_import_utxo() { &p.sender_offset_public_key, 0, Covenant::default(), - ImportStatus::Imported, - ) - .await - .unwrap(); - let tx_id_2 = alice_wallet - .import_utxo( - utxo_2.value, - &utxo_2.spending_key, - script.clone(), - input.clone(), - base_node_identity.public_key(), - features.clone(), - "Testing".to_string(), - utxo_2.metadata_signature.clone(), - &p.script_private_key, - &p.sender_offset_public_key, - 0, - Covenant::default(), - ImportStatus::ScannedUnconfirmed, - ) - .await - .unwrap(); - let tx_id_3 = alice_wallet - .import_utxo( - utxo_3.value, - &utxo_3.spending_key, - script, - input, - base_node_identity.public_key(), - features, - "Testing".to_string(), - utxo_3.metadata_signature.clone(), - &p.script_private_key, - &p.sender_offset_public_key, - 0, - Covenant::default(), - ImportStatus::ScannedConfirmed, ) .await .unwrap(); let balance = alice_wallet.output_manager_service.get_balance().await.unwrap(); - assert_eq!(balance.pending_incoming_balance, 90000 * uT); + assert_eq!(balance.pending_incoming_balance, 20000 * uT); let completed_tx_1 = alice_wallet .transaction_service @@ -828,27 +789,9 @@ async fn test_import_utxo() { .unwrap() .remove(&tx_id_1) .expect("Tx should be in collection"); - let completed_tx_2 = alice_wallet - .transaction_service - .get_completed_transactions() - .await - .unwrap() - .remove(&tx_id_2) - .expect("Tx should be in collection"); - let completed_tx_3 = alice_wallet - .transaction_service - .get_completed_transactions() - .await - .unwrap() - .remove(&tx_id_3) - .expect("Tx should be in collection"); assert_eq!(completed_tx_1.amount, 20000 * uT); assert_eq!(completed_tx_1.status, TransactionStatus::Imported); - assert_eq!(completed_tx_2.amount, 30000 * uT); - assert_eq!(completed_tx_2.status, TransactionStatus::FauxUnconfirmed); - assert_eq!(completed_tx_3.amount, 40000 * uT); - assert_eq!(completed_tx_3.status, TransactionStatus::FauxConfirmed); let db = OutputManagerDatabase::new(OutputManagerSqliteDatabase::new(connection, None)); let outputs = db.fetch_outputs_by_tx_id(tx_id).await.unwrap(); assert!(outputs.iter().any(|o| { o.hash == expected_output_hash })); diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index f25c9097e1..4cbaaa5232 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -40,7 +40,11 @@ //! `callback_transaction_mined` - This will be called when a Broadcast transaction is detected as mined via a base //! node request //! -//! `callback_faux_transaction_confirmed` - This will be called when a one-sided transaction is detected as mined +//! `callback_faux_transaction_confirmed` - This will be called when an imported output, recovered output or one-sided +//! transaction is detected as mined +//! +//! `callback_faux_transaction_unconfirmed` - This will be called when a recovered output or one-sided transaction is +//! freshly imported or when an imported transaction transitions from Imported to FauxUnconfirmed //! //! `callback_discovery_process_complete` - This will be called when a `send_transacion(..)` call is made to a peer //! whose address is not known and a discovery process must be conducted. The outcome of the discovery process is diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 45804e5d01..e957cfc596 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -108,7 +108,7 @@ use log4rs::{ use rand::rngs::OsRng; use tari_common_types::{ emoji::{emoji_set, EmojiId, EmojiIdError}, - transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, + transaction::{TransactionDirection, TransactionStatus, TxId}, types::{Commitment, PublicKey}, }; use tari_comms::{ @@ -3232,8 +3232,8 @@ unsafe fn init_logging( /// when a Broadcast transaction is detected as mined AND confirmed. /// `callback_transaction_mined_unconfirmed` - The callback function pointer matching the function signature. This will /// be called when a Broadcast transaction is detected as mined but not yet confirmed. -/// `callback_faux_transaction_confirmed` - The callback function pointer matching the function signature. This will be called -/// when a one-sided transaction is detected as mined AND confirmed. +/// `callback_faux_transaction_confirmed` - The callback function pointer matching the function signature. This will be +/// called when a one-sided transaction is detected as mined AND confirmed. /// `callback_faux_transaction_unconfirmed` - The callback function pointer matching the function signature. This /// will be called when a one-sided transaction is detected as mined but not yet confirmed. /// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called @@ -5035,7 +5035,6 @@ pub unsafe extern "C" fn wallet_import_utxo( &(*sender_offset_public_key).clone(), 0, covenant, - ImportStatus::Imported, )) { Ok(tx_id) => { if let Err(e) = (*wallet) diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index a3bc010ddc..cf66c19cbf 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -274,8 +274,8 @@ const char *completed_transaction_get_message(struct TariCompletedTransaction *t // | 5 | Coinbase | // | 6 | MinedConfirmed | // | 7 | Rejected | -// | 8 | ScannedUnconfirmed | -// | 9 | ScannedConfirmed | +// | 8 | FauxUnconfirmed | +// | 9 | FauxConfirmed | int completed_transaction_get_status(struct TariCompletedTransaction *transaction, int *error_out); // Gets the TransactionID of a TariCompletedTransaction @@ -357,8 +357,8 @@ unsigned long long pending_outbound_transaction_get_timestamp(struct TariPending // | 5 | Coinbase | // | 6 | MinedConfirmed | // | 7 | Rejected | -// | 8 | ScannedUnconfirmed | -// | 9 | ScannedConfirmed | +// | 8 | FauxUnconfirmed | +// | 9 | FauxConfirmed | int pending_outbound_transaction_get_status(struct TariPendingOutboundTransaction *transaction, int *error_out); // Frees memory for a TariPendingOutboundTactions @@ -404,8 +404,8 @@ unsigned long long pending_inbound_transaction_get_timestamp(struct TariPendingI // | 5 | Coinbase | // | 6 | MinedConfirmed | // | 7 | Rejected | -// | 8 | ScannedUnconfirmed | -// | 9 | ScannedConfirmed | +// | 8 | FauxUnconfirmed | +// | 9 | FauxConfirmed | int pending_inbound_transaction_get_status(struct TariPendingInboundTransaction *transaction, int *error_out); // Frees memory for a TariPendingInboundTransaction diff --git a/integration_tests/features/WalletFFI.feature b/integration_tests/features/WalletFFI.feature index 846081c2ad..d42ac97ed8 100644 --- a/integration_tests/features/WalletFFI.feature +++ b/integration_tests/features/WalletFFI.feature @@ -143,24 +143,42 @@ Feature: Wallet FFI And I have wallet RECEIVER connected to base node BASE2 And I have mining node MINER connected to base node BASE1 and wallet SENDER And mining node MINER mines 10 blocks - Then I wait for wallet SENDER to have at least 1000000 uT - And I send 2000000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST + Then I wait for wallet SENDER to have at least 5000000 uT + And I send 2400000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 + And I send 2400000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 + Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 10 blocks - Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT + Then I wait for ffi wallet FFI_WALLET to have at least 4000000 uT And I send 1000000 uT from ffi wallet FFI_WALLET to wallet RECEIVER at fee 20 via one-sided transactions Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 2 blocks Then all nodes are at height 22 - Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_UNCONFIRMED and valid/ + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_UNCONFIRMED and valid And mining node MINER mines 5 blocks Then all nodes are at height 27 - Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_CONFIRMED and valid/ -# And mining node MINER mines 6 blocks - Then I wait for wallet RECEIVER to have at least 1000000 uT - Then I wait for ffi wallet FFI_WALLET to receive 2 mined + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_CONFIRMED and valid And I stop ffi wallet FFI_WALLET + Scenario: As a client I want to receive a one-sided transaction + Given I have a seed node SEED + And I have a base node BASE1 connected to all seed nodes + And I have a base node BASE2 connected to all seed nodes + And I have wallet SENDER connected to base node BASE1 + And I have a ffi wallet FFI_RECEIVER connected to base node BASE2 + And I have mining node MINER connected to base node BASE1 and wallet SENDER + And mining node MINER mines 10 blocks + Then I wait for wallet SENDER to have at least 5000000 uT + Then I send a one-sided transaction of 1000000 uT from SENDER to FFI_RECEIVER at fee 20 + And mining node MINER mines 2 blocks + Then all nodes are at height 12 + Then ffi wallet FFI_RECEIVER detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_FAUX_UNCONFIRMED + And I send 2400000 uT from wallet SENDER to wallet FFI_RECEIVER at fee 20 + Then ffi wallet FFI_RECEIVER detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST + And mining node MINER mines 5 blocks + Then all nodes are at height 17 + Then ffi wallet FFI_RECEIVER detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_FAUX_CONFIRMED + And I stop ffi wallet FFI_RECEIVER + # Scenario: As a client I want to get my balance # It's a subtest of "As a client I want to retrieve a list of transactions I have made and received" diff --git a/integration_tests/features/support/ffi_steps.js b/integration_tests/features/support/ffi_steps.js index 1a9a1fb9d7..7ecb6459ed 100644 --- a/integration_tests/features/support/ffi_steps.js +++ b/integration_tests/features/support/ffi_steps.js @@ -3,16 +3,6 @@ const expect = require("chai").expect; const { sleep, waitForIterate } = require("../../helpers/util"); -When( - "I have ffi wallet {word} connected to base node {word}", - { timeout: 20 * 1000 }, - async function (name, node) { - let wallet = await this.createAndAddFFIWallet(name); - let peer = this.nodes[node].peerAddress().split("::"); - wallet.addBaseNodePeer(peer[0], peer[1]); - } -); - Then("I want to get emoji id of ffi wallet {word}", async function (name) { let wallet = this.getWallet(name); let emoji_id = wallet.identifyEmoji(); @@ -487,9 +477,13 @@ Then( const exactly = "EXACTLY"; expect(comparison === atLeast || comparison === exactly).to.equal(true); const broadcast = "TRANSACTION_STATUS_BROADCAST"; - const scannedUnconfirmed = "TRANSACTION_STATUS_FAUX_UNCONFIRMED"; - const scannedConfirmed = "TRANSACTION_STATUS_FAUX_CONFIRMED"; - expect(status === broadcast || status === scannedUnconfirmed || status === scannedConfirmed).to.equal(true); + const fauxUnconfirmed = "TRANSACTION_STATUS_FAUX_UNCONFIRMED"; + const fauxConfirmed = "TRANSACTION_STATUS_FAUX_CONFIRMED"; + expect( + status === broadcast || + status === fauxUnconfirmed || + status === fauxConfirmed + ).to.equal(true); const wallet = this.getWallet(walletName); console.log("\n"); @@ -505,20 +499,17 @@ Then( " transaction(s)" ); - await waitForIterate( + await waitForIterate( () => { switch (status) { - case broadcast: - return wallet.getCounters().broadcast >= amount; - break; - case scannedUnconfirmed: - return wallet.getCounters().scannedUnconfirmed >= amount; - break; - case scannedConfirmed: - return wallet.getCounters().scanned >= amount; - break; - default: - expect(expr).to.equal("please add this<< TransactionStatus");; + case broadcast: + return wallet.getCounters().broadcast >= amount; + case fauxUnconfirmed: + return wallet.getCounters().fauxUnconfirmed >= amount; + case fauxConfirmed: + return wallet.getCounters().fauxConfirmed >= amount; + default: + expect(status).to.equal("please add this<< TransactionStatus"); } }, true, @@ -527,22 +518,22 @@ Then( ); let amountOfCallbacks; - switch (status) { - case broadcast: - amountOfCallbacks = wallet.getCounters().broadcast; - break; - case scannedUnconfirmed: - amountOfCallbacks = wallet.getCounters().scannedUnconfirmed; - break; - case scannedConfirmed: - amountOfCallbacks = wallet.getCounters().scanned; - break; - default: - expect(expr).to.equal("please add this<< TransactionStatus");; - } + switch (status) { + case broadcast: + amountOfCallbacks = wallet.getCounters().broadcast; + break; + case fauxUnconfirmed: + amountOfCallbacks = wallet.getCounters().fauxUnconfirmed; + break; + case fauxConfirmed: + amountOfCallbacks = wallet.getCounters().fauxConfirmed; + break; + default: + expect(status).to.equal("please add this<< TransactionStatus"); + } if (!(amountOfCallbacks >= amount)) { - console.log("Counter not adequate!"); + console.log("\nCounter not adequate!", wallet.getCounters()); } else { console.log(wallet.getCounters()); } diff --git a/integration_tests/features/support/world.js b/integration_tests/features/support/world.js index e6e96b65cd..412e288f0f 100644 --- a/integration_tests/features/support/world.js +++ b/integration_tests/features/support/world.js @@ -638,7 +638,7 @@ BeforeAll({ timeout: 2400000 }, async function () { const wallet = new WalletProcess("compile"); console.log("Compiling wallet..."); await wallet.init(); - // await wallet.compile(); + await wallet.compile(); const mmProxy = new MergeMiningProxyProcess( "compile", @@ -675,7 +675,7 @@ BeforeAll({ timeout: 2400000 }, async function () { await miningNode.compile(); console.log("Compiling wallet FFI..."); - // await InterfaceFFI.compile(); + await InterfaceFFI.compile(); console.log("Finished compilation."); console.log("Loading FFI interface.."); await InterfaceFFI.init(); diff --git a/integration_tests/helpers/ffi/wallet.js b/integration_tests/helpers/ffi/wallet.js index c79105ce45..fc19fda279 100644 --- a/integration_tests/helpers/ffi/wallet.js +++ b/integration_tests/helpers/ffi/wallet.js @@ -88,10 +88,10 @@ class Wallet { broadcast: this.transactionBroadcast, finalized: this.transactionFinalized, minedUnconfirmed: this.transactionMinedUnconfirmed, - scannedUnconfirmed: this.transactionFauxUnconfirmed, + fauxUnconfirmed: this.transactionFauxUnconfirmed, cancelled: this.transactionCancelled, mined: this.transactionMined, - scanned: this.transactionFauxConfirmed, + fauxConfirmed: this.transactionFauxConfirmed, saf: this.transactionSafMessageReceived, }; } @@ -128,7 +128,9 @@ class Wallet { this.onTransactionMinedUnconfirmed ); this.callback_faux_transaction_confirmed = - InterfaceFFI.createCallbackFauxTransactionConfirmed(this.onFauxTransactionConfirmed); + InterfaceFFI.createCallbackFauxTransactionConfirmed( + this.onFauxTransactionConfirmed + ); this.callback_faux_transaction_unconfirmed = InterfaceFFI.createCallbackFauxTransactionUnconfirmed( this.onFauxTransactionUnconfirmed From fb1a4167fe2da6cb55b1ad2b7bd9f976402b4e6a Mon Sep 17 00:00:00 2001 From: Hansie Odendaal Date: Tue, 8 Feb 2022 12:25:24 +0200 Subject: [PATCH 3/3] final touches --- .../src/output_manager_service/service.rs | 4 +- .../wallet/src/transaction_service/service.rs | 8 ++- .../tasks/check_faux_transaction_status.rs | 42 +++++++++++--- .../utxo_scanner_service/utxo_scanner_task.rs | 11 ++++ base_layer/wallet/src/wallet.rs | 1 - base_layer/wallet/tests/wallet.rs | 18 +++--- integration_tests/features/WalletFFI.feature | 10 ++-- integration_tests/helpers/walletFFIClient.js | 2 +- integration_tests/package-lock.json | 56 +++++++++---------- 9 files changed, 99 insertions(+), 53 deletions(-) diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 908ca3427b..cef6150185 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -435,7 +435,7 @@ where // We need the maximum mined height and corresponding block hash (faux transactions outputs can have different // mined heights) let (mut last_height, mut max_mined_height, mut block_hash) = (0u64, None, None); - let _ = outputs.iter().map(|uo| { + for uo in outputs { if let Some(height) = uo.mined_height { if last_height < height { last_height = height; @@ -443,7 +443,7 @@ where block_hash = uo.mined_in_block.clone(); } } - }); + } Ok((statuses, max_mined_height, block_hash)) } diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 3313a47db0..4389a4c955 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -772,7 +772,13 @@ where Some(val) => val.height_of_longest_chain(), None => 0u64, }; - tokio::spawn(check_faux_transactions(output_manager_handle, db, tip_height)); + let event_publisher = self.event_publisher.clone(); + tokio::spawn(check_faux_transactions( + output_manager_handle, + db, + event_publisher, + tip_height, + )); } } diff --git a/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs index eb1f0366a0..6a5a776fe4 100644 --- a/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs +++ b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs @@ -20,6 +20,8 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use std::sync::Arc; + use log::*; use tari_common_types::types::BlockHash; @@ -27,6 +29,7 @@ use crate::{ output_manager_service::{handle::OutputManagerHandle, storage::OutputStatus}, transaction_service::{ config::TransactionServiceConfig, + handle::{TransactionEvent, TransactionEventSender}, storage::{ database::{TransactionBackend, TransactionDatabase}, models::CompletedTransaction, @@ -39,6 +42,7 @@ const LOG_TARGET: &str = "wallet::transaction_service::service"; pub async fn check_faux_transactions( mut output_manager: OutputManagerHandle, db: TransactionDatabase, + event_publisher: TransactionEventSender, tip_height: u64, ) { let mut all_faux_transactions: Vec = match db.get_imported_transactions().await { @@ -107,32 +111,56 @@ pub async fn check_faux_transactions( } else { vec![0u8; 32] }; + let is_valid = tip_height >= mined_height; let is_confirmed = tip_height.saturating_sub(mined_height) >= TransactionServiceConfig::default().num_confirmations_required; + let num_confirmations = tip_height - mined_height; debug!( target: LOG_TARGET, - "Updating faux transaction: TxId({}), mined_height({}), is_confirmed({}), num_confirmations({})", + "Updating faux transaction: TxId({}), mined_height({}), is_confirmed({}), num_confirmations({}), \ + is_valid({})", tx.tx_id, mined_height, is_confirmed, - tip_height - mined_height, + num_confirmations, + is_valid, ); - if let Err(e) = db + let result = db .set_transaction_mined_height( tx.tx_id, true, mined_height, mined_in_block, - tip_height - mined_height, + num_confirmations, is_confirmed, - true, + is_valid, ) - .await - { + .await; + if let Err(e) = result { error!( target: LOG_TARGET, "Error setting faux transaction to mined confirmed: {}", e ); + } else { + let transaction_event = match is_confirmed { + false => TransactionEvent::FauxTransactionUnconfirmed { + tx_id: tx.tx_id, + num_confirmations: 0, + is_valid, + }, + true => TransactionEvent::FauxTransactionConfirmed { + tx_id: tx.tx_id, + is_valid, + }, + }; + let _ = event_publisher.send(Arc::new(transaction_event)).map_err(|e| { + trace!( + target: LOG_TARGET, + "Error sending event, usually because there are no subscribers: {:?}", + e + ); + e + }); } } } diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index b859669538..5eec755347 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -49,6 +49,7 @@ use tokio::sync::broadcast; use crate::{ error::WalletError, storage::database::WalletBackend, + transaction_service::error::{TransactionServiceError, TransactionStorageError}, utxo_scanner_service::{ error::UtxoScannerError, handle::UtxoScannerEvent, @@ -544,6 +545,16 @@ where TBackend: WalletBackend + 'static num_recovered = num_recovered.saturating_add(1); total_amount += uo.value; }, + Err(WalletError::TransactionServiceError(TransactionServiceError::TransactionStorageError( + TransactionStorageError::DuplicateOutput, + ))) => { + info!( + target: LOG_TARGET, + "Recoverer attempted to add a duplicate output to the database for faux transaction ({}); \ + ignoring it as this is not a real error", + tx_id + ); + }, Err(e) => return Err(UtxoScannerError::UtxoImportError(e.to_string())), } } diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index f53d1c6eda..47ec10a00e 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -425,7 +425,6 @@ where info!( target: LOG_TARGET, "UTXO (Commitment: {}) imported into wallet as 'ImportStatus::Imported'", commitment_hex - ); Ok(tx_id) diff --git a/base_layer/wallet/tests/wallet.rs b/base_layer/wallet/tests/wallet.rs index 0de92e0415..4c56387e0b 100644 --- a/base_layer/wallet/tests/wallet.rs +++ b/base_layer/wallet/tests/wallet.rs @@ -756,20 +756,20 @@ async fn test_import_utxo() { let features = OutputFeatures::create_coinbase(50); let p = TestParams::new(); - let utxo_1 = create_unblinded_output(script.clone(), features.clone(), p.clone(), 20000 * uT); + let utxo = create_unblinded_output(script.clone(), features.clone(), p.clone(), 20000 * uT); let output = utxo.as_transaction_output(&factories).unwrap(); let expected_output_hash = output.hash(); - let tx_id_1 = alice_wallet + let tx_id = alice_wallet .import_utxo( - utxo_1.value, - &utxo_1.spending_key, + utxo.value, + &utxo.spending_key, script.clone(), input.clone(), base_node_identity.public_key(), features.clone(), "Testing".to_string(), - utxo_1.metadata_signature.clone(), + utxo.metadata_signature.clone(), &p.script_private_key, &p.sender_offset_public_key, 0, @@ -782,16 +782,16 @@ async fn test_import_utxo() { assert_eq!(balance.pending_incoming_balance, 20000 * uT); - let completed_tx_1 = alice_wallet + let completed_tx = alice_wallet .transaction_service .get_completed_transactions() .await .unwrap() - .remove(&tx_id_1) + .remove(&tx_id) .expect("Tx should be in collection"); - assert_eq!(completed_tx_1.amount, 20000 * uT); - assert_eq!(completed_tx_1.status, TransactionStatus::Imported); + assert_eq!(completed_tx.amount, 20000 * uT); + assert_eq!(completed_tx.status, TransactionStatus::Imported); let db = OutputManagerDatabase::new(OutputManagerSqliteDatabase::new(connection, None)); let outputs = db.fetch_outputs_by_tx_id(tx_id).await.unwrap(); assert!(outputs.iter().any(|o| { o.hash == expected_output_hash })); diff --git a/integration_tests/features/WalletFFI.feature b/integration_tests/features/WalletFFI.feature index d42ac97ed8..a00b643873 100644 --- a/integration_tests/features/WalletFFI.feature +++ b/integration_tests/features/WalletFFI.feature @@ -61,7 +61,7 @@ Feature: Wallet FFI And mining node MINER mines 10 blocks Then I wait for wallet SENDER to have at least 1000000 uT And I send 2000000 uT without waiting for broadcast from wallet SENDER to wallet FFI_WALLET at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be Broadcast + Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST And wallet SENDER detects all transactions are at least Broadcast And mining node MINER mines 10 blocks Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT @@ -94,11 +94,11 @@ Feature: Wallet FFI And mining node MINER mines 10 blocks Then I wait for wallet SENDER to have at least 1000000 uT And I send 2000000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be Broadcast + Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 10 blocks Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT And I send 1000000 uT from ffi wallet FFI_WALLET to wallet RECEIVER at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be Broadcast + Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be TRANSACTION_STATUS_BROADCAST # The broadcast check does not include delivery; create some holding points to ensure it was received And mining node MINER mines 2 blocks Then all nodes are at height 22 @@ -134,6 +134,7 @@ Feature: Wallet FFI Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT And I stop ffi wallet FFI_WALLET + @critical Scenario: As a client I want to send a one-sided transaction Given I have a seed node SEED And I have a base node BASE1 connected to all seed nodes @@ -159,6 +160,7 @@ Feature: Wallet FFI Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_CONFIRMED and valid And I stop ffi wallet FFI_WALLET + @critical Scenario: As a client I want to receive a one-sided transaction Given I have a seed node SEED And I have a base node BASE1 connected to all seed nodes @@ -172,7 +174,7 @@ Feature: Wallet FFI And mining node MINER mines 2 blocks Then all nodes are at height 12 Then ffi wallet FFI_RECEIVER detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_FAUX_UNCONFIRMED - And I send 2400000 uT from wallet SENDER to wallet FFI_RECEIVER at fee 20 + And I send 1000000 uT from wallet SENDER to wallet FFI_RECEIVER at fee 20 Then ffi wallet FFI_RECEIVER detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 5 blocks Then all nodes are at height 17 diff --git a/integration_tests/helpers/walletFFIClient.js b/integration_tests/helpers/walletFFIClient.js index 6bfd907f12..bc1b230d0c 100644 --- a/integration_tests/helpers/walletFFIClient.js +++ b/integration_tests/helpers/walletFFIClient.js @@ -157,7 +157,7 @@ class WalletFFIClient { seed_words_text, pass_phrase, rolling_log_files = 50, - byte_size_per_log = 102400 + byte_size_per_log = 1048576 ) { this.pass_phrase = pass_phrase; if (seed_words_text) { diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index b0a9b0ba99..63d7981e34 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -653,7 +653,7 @@ }, "any-promise": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", "dev": true }, @@ -747,7 +747,7 @@ }, "assert-plus": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, @@ -759,7 +759,7 @@ }, "assertion-error-formatter": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz", "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", "dev": true, "requires": { @@ -993,7 +993,7 @@ }, "colors": { "version": "1.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "commander": { @@ -1088,7 +1088,7 @@ }, "d": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", "dev": true, "requires": { @@ -1152,7 +1152,7 @@ }, "diff": { "version": "4.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, @@ -1167,7 +1167,7 @@ }, "duration": { "version": "0.2.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/duration/-/duration-0.2.2.tgz", "integrity": "sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==", "dev": true, "requires": { @@ -1259,7 +1259,7 @@ }, "es5-ext": { "version": "0.10.53", - "resolved": false, + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, "requires": { @@ -1270,7 +1270,7 @@ }, "es6-iterator": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "dev": true, "requires": { @@ -1281,7 +1281,7 @@ }, "es6-symbol": { "version": "3.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { @@ -1704,7 +1704,7 @@ }, "ext": { "version": "1.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", "dev": true, "requires": { @@ -1713,7 +1713,7 @@ "dependencies": { "type": { "version": "2.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", "dev": true } @@ -1765,7 +1765,7 @@ }, "figures": { "version": "3.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "requires": { "escape-string-regexp": "^1.0.5" @@ -2042,7 +2042,7 @@ }, "indent-string": { "version": "4.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, @@ -2175,7 +2175,7 @@ }, "is-stream": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, @@ -2328,7 +2328,7 @@ }, "knuth-shuffle-seeded": { "version": "1.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz", "integrity": "sha1-AfG2VzOqdUDuCNiwF0Fk0iCB5OE=", "dev": true, "requires": { @@ -2504,7 +2504,7 @@ }, "mz": { "version": "2.7.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "requires": { @@ -2521,7 +2521,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, @@ -2574,7 +2574,7 @@ }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { @@ -2669,7 +2669,7 @@ }, "pad-right": { "version": "0.2.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", "integrity": "sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q=", "dev": true, "requires": { @@ -2877,7 +2877,7 @@ }, "repeat-string": { "version": "1.6.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, @@ -2942,7 +2942,7 @@ }, "seed-random": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=", "dev": true }, @@ -3068,7 +3068,7 @@ }, "stack-chain": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-2.0.0.tgz", "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==", "dev": true }, @@ -3118,7 +3118,7 @@ }, "string-argv": { "version": "0.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", "dev": true }, @@ -3263,7 +3263,7 @@ }, "thenify": { "version": "3.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { @@ -3272,7 +3272,7 @@ }, "thenify-all": { "version": "1.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "dev": true, "requires": { @@ -3336,7 +3336,7 @@ }, "type": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", "dev": true }, @@ -3398,7 +3398,7 @@ }, "util-arity": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz", "integrity": "sha1-WdAa8f2z/t4KxOYysKtfbOl8kzA=", "dev": true },