diff --git a/applications/tari_console_wallet/src/ui/app.rs b/applications/tari_console_wallet/src/ui/app.rs index 838ca9509a..516b627364 100644 --- a/applications/tari_console_wallet/src/ui/app.rs +++ b/applications/tari_console_wallet/src/ui/app.rs @@ -42,7 +42,7 @@ use tari_wallet::{ contacts_service::storage::sqlite_db::ContactsServiceSqliteDatabase, output_manager_service::storage::sqlite_db::OutputManagerSqliteDatabase, storage::sqlite_db::WalletSqliteDatabase, - transaction_service::storage::{database::CompletedTransaction, sqlite_db::TransactionServiceSqliteDatabase}, + transaction_service::storage::{models::CompletedTransaction, sqlite_db::TransactionServiceSqliteDatabase}, Wallet, }; use tokio::sync::RwLock; diff --git a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs index 42936da575..dd7de425ff 100644 --- a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs @@ -5,7 +5,7 @@ use crate::ui::{ SelectedTransactionList, MAX_WIDTH, }; -use tari_wallet::transaction_service::storage::database::{TransactionDirection, TransactionStatus}; +use tari_wallet::transaction_service::storage::models::{TransactionDirection, TransactionStatus}; use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, diff --git a/applications/tari_console_wallet/src/ui/state/app_state.rs b/applications/tari_console_wallet/src/ui/state/app_state.rs index 672d1731e7..d064437c70 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -6,7 +6,7 @@ use qrcode::{render::unicode, QrCode}; use tari_common::Network; use tari_comms::NodeIdentity; use tari_crypto::tari_utilities::hex::Hex; -use tari_wallet::{transaction_service::storage::database::CompletedTransaction, util::emoji::EmojiId}; +use tari_wallet::{transaction_service::storage::models::CompletedTransaction, util::emoji::EmojiId}; pub struct AppState { pub pending_txs: StatefulList, diff --git a/base_layer/core/src/proto/generated/tari.transaction_protocol.rs b/base_layer/core/src/proto/generated/tari.transaction_protocol.rs index 0dc463290d..7110e730a2 100644 --- a/base_layer/core/src/proto/generated/tari.transaction_protocol.rs +++ b/base_layer/core/src/proto/generated/tari.transaction_protocol.rs @@ -8,6 +8,12 @@ pub struct TransactionFinalizedMessage { pub transaction: ::std::option::Option, } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct TransactionCancelledMessage { + /// The transaction id for the cancelled transaction + #[prost(uint64, tag = "1")] + pub tx_id: u64, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct TransactionMetadata { /// The absolute fee for the transaction #[prost(uint64, tag = "1")] diff --git a/base_layer/core/src/transactions/transaction_protocol/proto/transaction_cancelled.proto b/base_layer/core/src/transactions/transaction_protocol/proto/transaction_cancelled.proto new file mode 100644 index 0000000000..e3ba7901ce --- /dev/null +++ b/base_layer/core/src/transactions/transaction_protocol/proto/transaction_cancelled.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package tari.transaction_protocol; + +message TransactionCancelledMessage { + // The transaction id for the cancelled transaction + uint64 tx_id = 1; +} + diff --git a/base_layer/p2p/src/proto/message_type.proto b/base_layer/p2p/src/proto/message_type.proto index 1692dead44..e1d0339f7f 100644 --- a/base_layer/p2p/src/proto/message_type.proto +++ b/base_layer/p2p/src/proto/message_type.proto @@ -22,6 +22,7 @@ enum TariMessageType { TariMessageTypeMempoolRequest= 71; TariMessageTypeMempoolResponse = 72; TariMessageTypeTransactionFinalized = 73; + TariMessageTypeTransactionCancelled = 74; // -- DAN Messages -- // -- Extended -- diff --git a/base_layer/p2p/src/proto/tari.p2p.message_type.rs b/base_layer/p2p/src/proto/tari.p2p.message_type.rs index c82ed79443..ec42f3d466 100644 --- a/base_layer/p2p/src/proto/tari.p2p.message_type.rs +++ b/base_layer/p2p/src/proto/tari.p2p.message_type.rs @@ -15,8 +15,9 @@ pub enum TariMessageType { BaseNodeResponse = 70, MempoolRequest = 71, MempoolResponse = 72, - /// -- DAN Messages -- TransactionFinalized = 73, + /// -- DAN Messages -- + TransactionCancelled = 74, // -- Extended -- Text = 225, TextAck = 226, diff --git a/base_layer/wallet/migrations/2020-08-17-141407_add_resend_count_and_timestamp_add_status_to_pending_txs/down.sql b/base_layer/wallet/migrations/2020-08-17-141407_add_resend_count_and_timestamp_add_status_to_pending_txs/down.sql new file mode 100644 index 0000000000..8690ad3454 --- /dev/null +++ b/base_layer/wallet/migrations/2020-08-17-141407_add_resend_count_and_timestamp_add_status_to_pending_txs/down.sql @@ -0,0 +1,58 @@ +PRAGMA foreign_keys=off; +ALTER TABLE completed_transactions RENAME TO completed_transactions_old; +CREATE TABLE completed_transactions ( + tx_id INTEGER PRIMARY KEY NOT NULL, + source_public_key BLOB NOT NULL, + destination_public_key BLOB NOT NULL, + amount INTEGER NOT NULL, + fee INTEGER NOT NULL, + transaction_protocol TEXT NOT NULL, + status INTEGER NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME NOT NULL, + cancelled INTEGER NOT NULL DEFAULT 0, + direction INTEGER NULL DEFAULT NULL, + coinbase_block_height INTEGER NULL DEFAULT NULL +); +INSERT INTO completed_transactions (tx_id, source_public_key, destination_public_key, amount, fee, transaction_protocol, status, message, timestamp, cancelled, direction, coinbase_block_height) +SELECT tx_id, source_public_key, destination_public_key, amount, fee, transaction_protocol, status, message, timestamp, cancelled, direction, coinbase_block_height +FROM completed_transactions_old; + +DROP TABLE completed_transactions_old; + +ALTER TABLE inbound_transactions RENAME TO inbound_transactions_old; +CREATE TABLE inbound_transactions ( + tx_id INTEGER PRIMARY KEY NOT NULL, + source_public_key BLOB NOT NULL, + amount INTEGER NOT NULL, + receiver_protocol TEXT NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME NOT NULL, + cancelled INTEGER NOT NULL DEFAULT 0, + direct_send_success INTEGER NOT NULL DEFAULT 0 +); +INSERT INTO inbound_transactions (tx_id, source_public_key, amount, receiver_protocol, message, timestamp, cancelled, direct_send_success) +SELECT tx_id, source_public_key, amount, receiver_protocol, message, timestamp, cancelled, direct_send_success +FROM inbound_transactions_old; + +DROP TABLE inbound_transactions_old; + +ALTER TABLE outbound_transactions RENAME TO outbound_transactions_old; +CREATE TABLE outbound_transactions ( + tx_id INTEGER PRIMARY KEY NOT NULL, + destination_public_key BLOB NOT NULL, + amount INTEGER NOT NULL, + fee INTEGER NOT NULL, + sender_protocol TEXT NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME NOT NULL, + cancelled INTEGER NOT NULL DEFAULT 0, + direct_send_success INTEGER NOT NULL DEFAULT 0 +); +INSERT INTO outbound_transactions (tx_id, destination_public_key, amount, fee, sender_protocol, message, timestamp, cancelled, direct_send_success) +SELECT tx_id, destination_public_key, amount, fee, sender_protocol, message, timestamp, cancelled, direct_send_success +FROM outbound_transactions_old; + +DROP TABLE outbound_transactions_old; + +PRAGMA foreign_keys=on; \ No newline at end of file diff --git a/base_layer/wallet/migrations/2020-08-17-141407_add_resend_count_and_timestamp_add_status_to_pending_txs/up.sql b/base_layer/wallet/migrations/2020-08-17-141407_add_resend_count_and_timestamp_add_status_to_pending_txs/up.sql new file mode 100644 index 0000000000..841fdcffbe --- /dev/null +++ b/base_layer/wallet/migrations/2020-08-17-141407_add_resend_count_and_timestamp_add_status_to_pending_txs/up.sql @@ -0,0 +1,14 @@ +ALTER TABLE completed_transactions + ADD COLUMN send_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE completed_transactions + ADD COLUMN last_send_timestamp DATETIME NULL DEFAULT NULL; + +ALTER TABLE inbound_transactions + ADD COLUMN send_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE inbound_transactions + ADD COLUMN last_send_timestamp DATETIME NULL DEFAULT NULL; + +ALTER TABLE outbound_transactions + ADD COLUMN send_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE outbound_transactions + ADD COLUMN last_send_timestamp DATETIME NULL DEFAULT NULL; diff --git a/base_layer/wallet/src/lib.rs b/base_layer/wallet/src/lib.rs index 7a60d571fc..908042c3bc 100644 --- a/base_layer/wallet/src/lib.rs +++ b/base_layer/wallet/src/lib.rs @@ -5,7 +5,7 @@ #![deny(unused_must_use)] #![deny(unreachable_patterns)] #![deny(unknown_lints)] -#![recursion_limit = "1024"] +#![recursion_limit = "2048"] #![feature(drain_filter)] #![feature(type_alias_impl_trait)] diff --git a/base_layer/wallet/src/schema.rs b/base_layer/wallet/src/schema.rs index 0d1905ee41..a883c13c65 100644 --- a/base_layer/wallet/src/schema.rs +++ b/base_layer/wallet/src/schema.rs @@ -12,6 +12,8 @@ table! { cancelled -> Integer, direction -> Nullable, coinbase_block_height -> Nullable, + send_count -> Integer, + last_send_timestamp -> Nullable, } } @@ -32,6 +34,8 @@ table! { timestamp -> Timestamp, cancelled -> Integer, direct_send_success -> Integer, + send_count -> Integer, + last_send_timestamp -> Nullable, } } @@ -56,6 +60,8 @@ table! { timestamp -> Timestamp, cancelled -> Integer, direct_send_success -> Integer, + send_count -> Integer, + last_send_timestamp -> Nullable, } } diff --git a/base_layer/wallet/src/testnet_utils.rs b/base_layer/wallet/src/testnet_utils.rs index 428d47d225..65f729af05 100644 --- a/base_layer/wallet/src/testnet_utils.rs +++ b/base_layer/wallet/src/testnet_utils.rs @@ -33,8 +33,9 @@ use crate::{ transaction_service::{ handle::TransactionEvent, storage::{ - database::{CompletedTransaction, TransactionBackend, TransactionDirection, TransactionStatus}, + database::TransactionBackend, memory_db::TransactionMemoryDatabase, + models::{CompletedTransaction, TransactionDirection, TransactionStatus}, }, }, wallet::WalletConfig, diff --git a/base_layer/wallet/src/transaction_service/config.rs b/base_layer/wallet/src/transaction_service/config.rs index ecea3f3a41..247d75bc51 100644 --- a/base_layer/wallet/src/transaction_service/config.rs +++ b/base_layer/wallet/src/transaction_service/config.rs @@ -31,8 +31,10 @@ pub struct TransactionServiceConfig { pub chain_monitoring_timeout: Duration, pub direct_send_timeout: Duration, pub broadcast_send_timeout: Duration, - pub low_power_polling_timeout: Duration, /* This is the timeout period that will be used when the wallet is in - * low_power mode */ + pub low_power_polling_timeout: Duration, + pub transaction_resend_period: Duration, + pub resend_response_cooldown: Duration, + pub pending_transaction_cancellation_timeout: Duration, } impl Default for TransactionServiceConfig { @@ -43,6 +45,9 @@ impl Default for TransactionServiceConfig { direct_send_timeout: Duration::from_secs(20), broadcast_send_timeout: Duration::from_secs(30), low_power_polling_timeout: Duration::from_secs(300), + transaction_resend_period: Duration::from_secs(3600), + resend_response_cooldown: Duration::from_secs(300), + pending_transaction_cancellation_timeout: Duration::from_secs(259200), // 3 Days } } } diff --git a/base_layer/wallet/src/transaction_service/error.rs b/base_layer/wallet/src/transaction_service/error.rs index 4a2647e3b5..61f873eb09 100644 --- a/base_layer/wallet/src/transaction_service/error.rs +++ b/base_layer/wallet/src/transaction_service/error.rs @@ -115,12 +115,16 @@ pub enum TransactionServiceError { NodeIdError(#[from] NodeIdError), #[error("Broadcast recv error: `{0}`")] BroadcastRecvError(#[from] RecvError), + #[error("Broadcast send error: `{0}`")] + BroadcastSendError(String), #[error("Oneshot cancelled error: `{0}`")] OneshotCancelled(#[from] Canceled), #[error("Liveness error: `{0}`")] LivenessError(#[from] LivenessError), #[error("Coinbase build error: `{0}`")] CoinbaseBuildError(#[from] CoinbaseBuildError), + #[error("Pending Transaction Timed out")] + Timeout, } #[derive(Debug, Error)] diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index cd3648656b..bddf96320d 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -24,7 +24,7 @@ use crate::{ output_manager_service::TxId, transaction_service::{ error::TransactionServiceError, - storage::database::{CompletedTransaction, InboundTransaction, OutboundTransaction}, + storage::models::{CompletedTransaction, InboundTransaction, OutboundTransaction}, }, }; use aes_gcm::Aes256Gcm; diff --git a/base_layer/wallet/src/transaction_service/mod.rs b/base_layer/wallet/src/transaction_service/mod.rs index de9e9c59ac..744c2702cb 100644 --- a/base_layer/wallet/src/transaction_service/mod.rs +++ b/base_layer/wallet/src/transaction_service/mod.rs @@ -26,6 +26,7 @@ pub mod handle; pub mod protocols; pub mod service; pub mod storage; +pub mod tasks; use crate::{ output_manager_service::handle::OutputManagerHandle, @@ -163,6 +164,19 @@ where T: TransactionBackend .map(map_decode::) .filter_map(ok_or_skip_result) } + + fn transaction_cancelled_stream(&self) -> impl Stream> { + trace!( + target: LOG_TARGET, + "Subscription '{}' for topic '{:?}' created.", + SUBSCRIPTION_LABEL, + TariMessageType::TransactionCancelled + ); + self.subscription_factory + .get_subscription(TariMessageType::TransactionCancelled, SUBSCRIPTION_LABEL) + .map(map_decode::) + .filter_map(ok_or_skip_result) + } } impl ServiceInitializer for TransactionServiceInitializer @@ -183,6 +197,7 @@ where T: TransactionBackend + Clone + 'static let transaction_finalized_stream = self.transaction_finalized_stream(); let mempool_response_stream = self.mempool_response_stream(); let base_node_response_stream = self.base_node_response_stream(); + let transaction_cancelled_stream = self.transaction_cancelled_stream(); let (publisher, _) = broadcast::channel(200); @@ -219,6 +234,7 @@ where T: TransactionBackend + Clone + 'static transaction_finalized_stream, mempool_response_stream, base_node_response_stream, + transaction_cancelled_stream, output_manager_service, outbound_message_service, publisher, diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs index bcb4f06c53..72313141e3 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs @@ -24,7 +24,7 @@ use crate::transaction_service::{ error::{TransactionServiceError, TransactionServiceProtocolError}, handle::TransactionEvent, service::TransactionServiceResources, - storage::database::{TransactionBackend, TransactionStatus}, + storage::{database::TransactionBackend, models::TransactionStatus}, }; use futures::{channel::mpsc::Receiver, FutureExt, StreamExt}; use log::*; diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_chain_monitoring_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_chain_monitoring_protocol.rs index 26b4697536..ecedcbef8c 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_chain_monitoring_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_chain_monitoring_protocol.rs @@ -26,7 +26,7 @@ use crate::{ error::{TransactionServiceError, TransactionServiceProtocolError}, handle::TransactionEvent, service::TransactionServiceResources, - storage::database::{TransactionBackend, TransactionStatus}, + storage::{database::TransactionBackend, models::TransactionStatus}, }, }; use futures::{channel::mpsc::Receiver, FutureExt, StreamExt}; diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_coinbase_monitoring_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_coinbase_monitoring_protocol.rs index 7b1df0f514..610716fbd2 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_coinbase_monitoring_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_coinbase_monitoring_protocol.rs @@ -26,7 +26,7 @@ use crate::{ error::{TransactionServiceError, TransactionServiceProtocolError}, handle::TransactionEvent, service::TransactionServiceResources, - storage::database::{TransactionBackend, TransactionStatus}, + storage::{database::TransactionBackend, models::TransactionStatus}, }, }; use futures::{channel::mpsc::Receiver, FutureExt, StreamExt}; 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 7fb0df7c4d..c92dced28c 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 @@ -26,13 +26,11 @@ use crate::{ error::{TransactionServiceError, TransactionServiceProtocolError}, handle::TransactionEvent, service::TransactionServiceResources, - storage::database::{ - CompletedTransaction, - InboundTransaction, - TransactionBackend, - TransactionDirection, - TransactionStatus, + storage::{ + database::TransactionBackend, + models::{CompletedTransaction, InboundTransaction, TransactionDirection, TransactionStatus}, }, + tasks::send_transaction_reply::send_transaction_reply, }, }; use chrono::Utc; @@ -44,20 +42,16 @@ use futures::{ use log::*; use rand::rngs::OsRng; use std::sync::Arc; -use tari_comms::{peer_manager::NodeId, types::CommsPublicKey}; -use tari_comms_dht::{ - domain_message::OutboundDomainMessage, - envelope::NodeDestination, - outbound::{MessageSendStates, OutboundEncryption, SendMessageResponse}, -}; +use tari_comms::types::CommsPublicKey; + use tari_core::transactions::{ transaction::{OutputFeatures, Transaction}, - transaction_protocol::{proto, recipient::RecipientState, sender::TransactionSenderMessage}, + transaction_protocol::{recipient::RecipientState, sender::TransactionSenderMessage}, types::PrivateKey, ReceiverTransactionProtocol, }; use tari_crypto::keys::SecretKey; -use tari_p2p::tari_message::TariMessageType; +use tokio::time::delay_for; const LOG_TARGET: &str = "wallet::transaction_service::protocols::receive_protocol"; @@ -146,7 +140,6 @@ where TBackend: TransactionBackend + Clone + 'static } let amount = data.amount; - let spending_key = self .resources .output_manager_service @@ -162,120 +155,55 @@ where TBackend: TransactionBackend + Clone + 'static OutputFeatures::default(), &self.resources.factories, ); - let recipient_reply = rtp - .get_signed_data() - .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))? - .clone(); - let mut store_and_forward_send_result = false; - let mut direct_send_result = false; - - let tx_id = recipient_reply.tx_id; - let proto_message: proto::RecipientSignedMessage = recipient_reply.into(); - match self - .resources - .outbound_message_service - .send_direct( - self.source_pubkey.clone(), - OutboundDomainMessage::new(TariMessageType::ReceiverPartialTransactionReply, proto_message.clone()), - ) - .await - { - Ok(result) => match result { - SendMessageResponse::Queued(send_states) => { - if self.wait_on_dial(send_states).await { - direct_send_result = true; - } else { - store_and_forward_send_result = self - .send_transaction_reply_store_and_forward( - tx_id, - self.source_pubkey.clone(), - proto_message.clone(), - ) - .await - .map_err(|e| TransactionServiceProtocolError::new(self.id, e))?; - } - }, - SendMessageResponse::Failed(err) => { - warn!( - target: LOG_TARGET, - "Transaction Reply Send Direct for TxID {} failed: {}", self.id, err - ); - store_and_forward_send_result = self - .send_transaction_reply_store_and_forward( - tx_id, - self.source_pubkey.clone(), - proto_message.clone(), - ) - .await - .map_err(|e| TransactionServiceProtocolError::new(self.id, e))?; - }, - SendMessageResponse::PendingDiscovery(rx) => { - store_and_forward_send_result = self - .send_transaction_reply_store_and_forward( - tx_id, - self.source_pubkey.clone(), - proto_message.clone(), - ) - .await - .map_err(|e| TransactionServiceProtocolError::new(self.id, e))?; - // now wait for discovery to complete - match rx.await { - Ok(send_msg_response) => { - if let SendMessageResponse::Queued(send_states) = send_msg_response { - debug!( - target: LOG_TARGET, - "Discovery of {} completed for TxID: {}", self.source_pubkey, self.id - ); - direct_send_result = self.wait_on_dial(send_states).await; - } - }, - Err(e) => { - debug!( - target: LOG_TARGET, - "Error waiting for Discovery while sending message to TxId: {} {:?}", self.id, e - ); - }, - } - }, - }, - Err(e) => { - warn!(target: LOG_TARGET, "Direct Transaction Reply Send failed: {:?}", e); - }, - } - - // Otherwise add it to our pending transaction list and return reply let inbound_transaction = InboundTransaction::new( - tx_id, + data.tx_id, self.source_pubkey.clone(), amount, - rtp.clone(), + rtp, TransactionStatus::Pending, data.message.clone(), Utc::now().naive_utc(), ); + + let send_result = send_transaction_reply( + inbound_transaction.clone(), + self.resources.outbound_message_service.clone(), + self.resources.config.direct_send_timeout, + ) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, e))?; + self.resources .db - .add_pending_inbound_transaction(tx_id, inbound_transaction.clone()) + .add_pending_inbound_transaction(inbound_transaction.tx_id, inbound_transaction) .await .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; - if !direct_send_result && !store_and_forward_send_result { + self.resources + .db + .increment_send_count(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + + if !send_result { error!( target: LOG_TARGET, - "Transaction with TX_ID = {} received from {}. Reply could not be sent!", tx_id, self.source_pubkey, + "Transaction with TX_ID = {} received from {}. Reply could not be sent!", + data.tx_id, + self.source_pubkey, ); } else { info!( target: LOG_TARGET, - "Transaction with TX_ID = {} received from {}. Reply Sent", tx_id, self.source_pubkey, + "Transaction with TX_ID = {} received from {}. Reply Sent", data.tx_id, self.source_pubkey, ); } trace!( target: LOG_TARGET, "Transaction (TX_ID: {}) - Amount: {} - Message: {}", - tx_id, + data.tx_id, amount, data.message, ); @@ -283,7 +211,7 @@ where TBackend: TransactionBackend + Clone + 'static let _ = self .resources .event_publisher - .send(Arc::new(TransactionEvent::ReceivedTransaction(tx_id))) + .send(Arc::new(TransactionEvent::ReceivedTransaction(data.tx_id))) .map_err(|e| { trace!(target: LOG_TARGET, "Error sending event due to no subscribers: {:?}", e); e @@ -297,91 +225,6 @@ where TBackend: TransactionBackend + Clone + 'static } } - async fn send_transaction_reply_store_and_forward( - &mut self, - tx_id: TxId, - source_pubkey: CommsPublicKey, - msg: proto::RecipientSignedMessage, - ) -> Result - { - match self - .resources - .outbound_message_service - .broadcast( - NodeDestination::NodeId(Box::new(NodeId::from_key(&source_pubkey)?)), - OutboundEncryption::EncryptFor(Box::new(source_pubkey.clone())), - vec![], - OutboundDomainMessage::new(TariMessageType::ReceiverPartialTransactionReply, msg), - ) - .await - { - Ok(send_states) => { - info!( - target: LOG_TARGET, - "Sending Transaction Reply (TxId: {}) to Neighbours for Store and Forward successful with Message \ - Tags: {:?}", - tx_id, - send_states.to_tags(), - ); - }, - Err(e) => { - warn!( - target: LOG_TARGET, - "Sending Transaction Reply (TxId: {}) to neighbours for Store and Forward failed: {:?}", tx_id, e, - ); - }, - }; - - Ok(true) - } - - /// This function contains the logic to wait on a dial and send of a queued message - async fn wait_on_dial(&self, send_states: MessageSendStates) -> bool { - if send_states.len() == 1 { - debug!( - target: LOG_TARGET, - "Transaction Reply (TxId: {}) Direct Send to {} queued with Message {}", - self.id, - self.source_pubkey, - send_states[0].tag, - ); - let (sent, failed) = send_states - .wait_n_timeout(self.resources.config.direct_send_timeout, 1) - .await; - if !sent.is_empty() { - info!( - target: LOG_TARGET, - "Direct Send process of Transaction Reply TX_ID: {} was successful with Message: {}", - self.id, - sent[0] - ); - true - } else { - if failed.is_empty() { - warn!( - target: LOG_TARGET, - "Direct Send process for Transaction Reply TX_ID: {} timed out", self.id - ); - } else { - warn!( - target: LOG_TARGET, - "Direct Send process for Transaction Reply TX_ID: {} and Message {} was unsuccessful and no \ - message was sent", - self.id, - failed[0] - ); - } - false - } - } else { - warn!( - target: LOG_TARGET, - "Transaction Reply Send Direct for TxID: {} failed", self.id - ); - false - } - } - async fn wait_for_finalization(&mut self) -> Result<(), TransactionServiceProtocolError> { let mut receiver = self .transaction_finalize_receiver @@ -406,10 +249,75 @@ where TBackend: TransactionBackend + Clone + 'static }, }; + // Determine the time remaining before this transaction times out + let elapsed_time = Utc::now() + .naive_utc() + .signed_duration_since(inbound_tx.timestamp) + .to_std() + .map_err(|_| { + TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::ConversionError("duration::OutOfRangeError".to_string()), + ) + })?; + + let timeout_duration = match self + .resources + .config + .pending_transaction_cancellation_timeout + .checked_sub(elapsed_time) + { + None => { + // This will cancel the transaction and exit this protocol + return self.timeout_transaction().await; + }, + Some(t) => t, + }; + let mut timeout_delay = delay_for(timeout_duration).fuse(); + + // check to see if a resend is due + let resend = match inbound_tx.last_send_timestamp { + None => true, + Some(timestamp) => { + let elapsed_time = Utc::now() + .naive_utc() + .signed_duration_since(timestamp) + .to_std() + .map_err(|_| { + TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::ConversionError("duration::OutOfRangeError".to_string()), + ) + })?; + elapsed_time > self.resources.config.transaction_resend_period + }, + }; + + if resend { + if let Err(e) = send_transaction_reply( + inbound_tx.clone(), + self.resources.outbound_message_service.clone(), + self.resources.config.direct_send_timeout, + ) + .await + { + warn!( + target: LOG_TARGET, + "Error resending Transaction Reply (TxId: {}): {:?}", self.id, e + ); + } + self.resources + .db + .increment_send_count(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + } + #[allow(unused_assignments)] let mut incoming_finalized_transaction = None; loop { loop { + let mut resend_timeout = delay_for(self.resources.config.transaction_resend_period).fuse(); futures::select! { (spk, tx_id, tx) = receiver.select_next_some() => { incoming_finalized_transaction = Some(tx); @@ -430,6 +338,27 @@ where TBackend: TransactionBackend + Clone + 'static self.id, TransactionServiceError::TransactionCancelled, )); + }, + () = resend_timeout => { + match send_transaction_reply( + inbound_tx.clone(), + self.resources.outbound_message_service.clone(), + self.resources.config.direct_send_timeout, + ) + .await { + Ok(_) => self.resources + .db + .increment_send_count(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?, + Err(e) => warn!( + target: LOG_TARGET, + "Error resending Transaction Reply (TxId: {}): {:?}", self.id, e + ), + } + }, + () = timeout_delay => { + return self.timeout_transaction().await; } } } @@ -508,4 +437,55 @@ where TBackend: TransactionBackend + Clone + 'static } Ok(()) } + + async fn timeout_transaction(&mut self) -> Result<(), TransactionServiceProtocolError> { + info!( + target: LOG_TARGET, + "Cancelling Transaction Receive Protocol (TxId: {}) due to timeout after no counterparty response", self.id + ); + + self.resources + .db + .cancel_pending_transaction(self.id) + .await + .map_err(|e| { + warn!( + target: LOG_TARGET, + "Pending Transaction does not exist and could not be cancelled: {:?}", e + ); + TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)) + })?; + + self.resources + .output_manager_service + .cancel_transaction(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + + let _ = self + .resources + .event_publisher + .send(Arc::new(TransactionEvent::TransactionCancelled(self.id))) + .map_err(|e| { + trace!( + target: LOG_TARGET, + "Error sending event because there are no subscribers: {:?}", + e + ); + TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::BroadcastSendError(format!("{:?}", e)), + ) + }); + + info!( + target: LOG_TARGET, + "Pending Transaction (TxId: {}) timed out after no response from counterparty", self.id + ); + + Err(TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::Timeout, + )) + } } 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 d1239f7a81..ace62fdd90 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 @@ -30,12 +30,14 @@ use crate::transaction_service::{ error::{TransactionServiceError, TransactionServiceProtocolError}, handle::TransactionEvent, service::TransactionServiceResources, - storage::database::{ - CompletedTransaction, - OutboundTransaction, - TransactionBackend, - TransactionDirection, - TransactionStatus, + storage::{ + database::TransactionBackend, + models::{CompletedTransaction, OutboundTransaction, TransactionDirection, TransactionStatus}, + }, + tasks::{ + send_finalized_transaction::send_finalized_transaction_message, + send_transaction_cancelled::send_transaction_cancelled_message, + wait_on_dial::wait_on_dial, }, }; use futures::channel::oneshot; @@ -43,15 +45,16 @@ use tari_comms::{peer_manager::NodeId, types::CommsPublicKey}; use tari_comms_dht::{ domain_message::OutboundDomainMessage, envelope::NodeDestination, - outbound::{MessageSendStates, OutboundEncryption, SendMessageResponse}, + outbound::{OutboundEncryption, SendMessageResponse}, }; use tari_core::transactions::{ tari_amount::MicroTari, - transaction::{KernelFeatures, Transaction, TransactionError}, + transaction::{KernelFeatures, TransactionError}, transaction_protocol::{proto, recipient::RecipientSignedMessage, sender::SingleRoundSenderData}, SenderTransactionProtocol, }; use tari_p2p::tari_message::TariMessageType; +use tokio::time::delay_for; const LOG_TARGET: &str = "wallet::transaction_service::protocols::send_protocol"; @@ -186,6 +189,12 @@ where TBackend: TransactionBackend + Clone + 'static .await .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + self.resources + .db + .increment_send_count(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + info!( target: LOG_TARGET, "Pending Outbound Transaction TxId: {:?} added. Waiting for Reply or Cancellation", self.id, @@ -223,9 +232,76 @@ where TBackend: TransactionBackend + Clone + 'static )); } + // Determine the time remaining before this transaction times out + let elapsed_time = Utc::now() + .naive_utc() + .signed_duration_since(outbound_tx.timestamp) + .to_std() + .map_err(|_| { + TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::ConversionError("duration::OutOfRangeError".to_string()), + ) + })?; + + let timeout_duration = match self + .resources + .config + .pending_transaction_cancellation_timeout + .checked_sub(elapsed_time) + { + None => { + // This will cancel the transaction and exit this protocol + return self.timeout_transaction().await; + }, + Some(t) => t, + }; + let mut timeout_delay = delay_for(timeout_duration).fuse(); + + // check to see if a resend is due + let resend = match outbound_tx.last_send_timestamp { + None => true, + Some(timestamp) => { + let elapsed_time = Utc::now() + .naive_utc() + .signed_duration_since(timestamp) + .to_std() + .map_err(|_| { + TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::ConversionError("duration::OutOfRangeError".to_string()), + ) + })?; + elapsed_time > self.resources.config.transaction_resend_period + }, + }; + + if resend { + if let Err(e) = self + .send_transaction( + outbound_tx + .sender_protocol + .get_single_round_message() + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?, + ) + .await + { + warn!( + target: LOG_TARGET, + "Error resending Transaction (TxId: {}): {:?}", self.id, e + ); + } + self.resources + .db + .increment_send_count(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + } + #[allow(unused_assignments)] let mut reply = None; loop { + let mut resend_timeout = delay_for(self.resources.config.transaction_resend_period).fuse(); futures::select! { (spk, rr) = receiver.select_next_some() => { let rr_tx_id = rr.tx_id; @@ -243,11 +319,39 @@ where TBackend: TransactionBackend + Clone + 'static } }, _ = cancellation_receiver => { - info!(target: LOG_TARGET, "Cancelling Transaction Send Protocol for TxId: {}", self.id); + info!(target: LOG_TARGET, "Cancelling Transaction Send Protocol (TxId: {})", self.id); + let _ = send_transaction_cancelled_message(self.id,self.dest_pubkey.clone(), self.resources.outbound_message_service.clone(), ).await.map_err(|e| { + warn!( + target: LOG_TARGET, + "Error sending Transaction Cancelled (TxId: {}) message: {:?}", self.id, e + ) + }); + self.resources + .db + .increment_send_count(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; return Err(TransactionServiceProtocolError::new( self.id, TransactionServiceError::TransactionCancelled, )); + }, + () = resend_timeout => { + if let Err(e) = self.send_transaction(outbound_tx.sender_protocol.get_single_round_message().map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?).await { + warn!( + target: LOG_TARGET, + "Error resending Transaction (TxId: {}): {:?}", self.id, e + ); + } else { + self.resources + .db + .increment_send_count(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + } + }, + () = timeout_delay => { + return self.timeout_transaction().await; } } } @@ -310,7 +414,21 @@ where TBackend: TransactionBackend + Clone + 'static "Transaction Recipient Reply for TX_ID = {} received", tx_id, ); - self.send_transaction_finalized_message(tx.clone()).await?; + send_finalized_transaction_message( + tx_id, + tx.clone(), + self.dest_pubkey.clone(), + self.resources.outbound_message_service.clone(), + self.resources.config.direct_send_timeout, + ) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, e))?; + + self.resources + .db + .increment_send_count(tx_id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; let _ = self .resources @@ -330,7 +448,7 @@ where TBackend: TransactionBackend + Clone + 'static /// Attempt to send the transaction to the recipient both directly and via Store-and-forward. If both fail to send /// the transaction will be cancelled. - /// # Arguments + /// # Argumentswallet_sync_with_base_node /// `msg`: The transaction data message to be sent async fn send_transaction( &mut self, @@ -357,7 +475,15 @@ where TBackend: TransactionBackend + Clone + 'static { Ok(result) => match result { SendMessageResponse::Queued(send_states) => { - if self.wait_on_dial(send_states, "Transaction").await { + if wait_on_dial( + send_states, + self.id, + self.dest_pubkey.clone(), + "Transaction", + self.resources.config.direct_send_timeout, + ) + .await + { direct_send_result = true; } else { store_and_forward_send_result = self.send_transaction_store_and_forward(msg.clone()).await?; @@ -380,7 +506,14 @@ where TBackend: TransactionBackend + Clone + 'static target: LOG_TARGET, "Discovery of {} completed for TxID: {}", self.dest_pubkey, self.id ); - direct_send_result = self.wait_on_dial(send_states, "Transaction").await; + direct_send_result = wait_on_dial( + send_states, + self.id, + self.dest_pubkey.clone(), + "Transaction", + self.resources.config.direct_send_timeout, + ) + .await; } }, Err(e) => { @@ -433,52 +566,6 @@ where TBackend: TransactionBackend + Clone + 'static }) } - /// This function contains the logic to wait on a dial and send of a queued message - async fn wait_on_dial(&self, send_states: MessageSendStates, message: &str) -> bool { - if send_states.len() == 1 { - debug!( - target: LOG_TARGET, - "{} (TxId: {}) Direct Send to {} queued with Message {}", - message, - self.id, - self.dest_pubkey, - send_states[0].tag, - ); - let (sent, failed) = send_states - .wait_n_timeout(self.resources.config.direct_send_timeout, 1) - .await; - if !sent.is_empty() { - info!( - target: LOG_TARGET, - "Direct Send process for {} TX_ID: {} was successful with Message: {}", message, self.id, sent[0] - ); - true - } else { - if failed.is_empty() { - warn!( - target: LOG_TARGET, - "Direct Send process for {} TX_ID: {} timed out", message, self.id - ); - } else { - warn!( - target: LOG_TARGET, - "Direct Send process for {} TX_ID: {} and Message {} was unsuccessful and no message was sent", - message, - self.id, - failed[0] - ); - } - false - } - } else { - warn!( - target: LOG_TARGET, - "{} Send Direct for TxID: {} failed", message, self.id - ); - false - } - } - /// Contains all the logic to send the transaction to the recipient via store and forward /// # Arguments /// `msg`: The transaction data message to be sent @@ -552,126 +639,72 @@ where TBackend: TransactionBackend + Clone + 'static } } - async fn send_transaction_finalized_message( - &mut self, - transaction: Transaction, - ) -> Result<(), TransactionServiceProtocolError> - { - let finalized_transaction_message = proto::TransactionFinalizedMessage { - tx_id: self.id, - transaction: Some(transaction.clone().into()), - }; - let mut store_and_forward_send_result = false; - let mut direct_send_result = false; - match self - .resources - .outbound_message_service - .send_direct( - self.dest_pubkey.clone(), - OutboundDomainMessage::new( - TariMessageType::TransactionFinalized, - finalized_transaction_message.clone(), - ), + async fn timeout_transaction(&mut self) -> Result<(), TransactionServiceProtocolError> { + info!( + target: LOG_TARGET, + "Cancelling Transaction Send Protocol (TxId: {}) due to timeout after no counterparty response", self.id + ); + let _ = send_transaction_cancelled_message( + self.id, + self.dest_pubkey.clone(), + self.resources.outbound_message_service.clone(), + ) + .await + .map_err(|e| { + warn!( + target: LOG_TARGET, + "Error sending Transaction Cancelled (TxId: {}) message: {:?}", self.id, e ) + }); + self.resources + .db + .increment_send_count(self.id) .await - { - Ok(result) => match result { - SendMessageResponse::Queued(send_states) => { - if self.wait_on_dial(send_states, "Finalized Transaction").await { - direct_send_result = true; - } else { - store_and_forward_send_result = self - .send_transaction_finalized_message_store_and_forward(finalized_transaction_message.clone()) - .await?; - } - }, - SendMessageResponse::Failed(err) => { - warn!( - target: LOG_TARGET, - "Finalized Transaction Send Direct for TxID {} failed: {}", self.id, err - ); - store_and_forward_send_result = self - .send_transaction_finalized_message_store_and_forward(finalized_transaction_message.clone()) - .await?; - }, - SendMessageResponse::PendingDiscovery(rx) => { - store_and_forward_send_result = self - .send_transaction_finalized_message_store_and_forward(finalized_transaction_message.clone()) - .await?; - // now wait for discovery to complete - match rx.await { - Ok(send_msg_response) => { - if let SendMessageResponse::Queued(send_states) = send_msg_response { - debug!( - target: LOG_TARGET, - "Discovery of {} completed for TxID: {}", self.dest_pubkey, self.id - ); - direct_send_result = self.wait_on_dial(send_states, "Finalized Transaction").await; - } - }, - Err(e) => { - warn!( - target: LOG_TARGET, - "Error waiting for Discovery while sending message to TxId: {} {:?}", self.id, e - ); - }, - } - }, - }, - Err(e) => { - return Err(TransactionServiceProtocolError::new( - self.id, - TransactionServiceError::from(e), - )) - }, - } - if !direct_send_result && !store_and_forward_send_result { - return Err(TransactionServiceProtocolError::new( - self.id, - TransactionServiceError::OutboundSendFailure, - )); - } - Ok(()) - } + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; - async fn send_transaction_finalized_message_store_and_forward( - &mut self, - msg: proto::TransactionFinalizedMessage, - ) -> Result - { - match self - .resources - .outbound_message_service - .broadcast( - NodeDestination::NodeId(Box::new(NodeId::from_key(&self.dest_pubkey).map_err(|e| { - TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)) - })?)), - OutboundEncryption::EncryptFor(Box::new(self.dest_pubkey.clone())), - vec![], - OutboundDomainMessage::new(TariMessageType::TransactionFinalized, msg.clone()), - ) + self.resources + .db + .cancel_pending_transaction(self.id) .await - { - Ok(send_states) => { - info!( + .map_err(|e| { + warn!( target: LOG_TARGET, - "Sending Finalized Transaction (TxId: {}) to Neighbours for Store and Forward successful with \ - Message Tags: {:?}", - self.id, - send_states.to_tags(), + "Pending Transaction does not exist and could not be cancelled: {:?}", e ); - }, - Err(e) => { - warn!( + TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)) + })?; + + self.resources + .output_manager_service + .cancel_transaction(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + + let _ = self + .resources + .event_publisher + .send(Arc::new(TransactionEvent::TransactionCancelled(self.id))) + .map_err(|e| { + trace!( target: LOG_TARGET, - "Sending Finalized Transaction (TxId: {}) to neighbours for Store and Forward failed: {:?}", - self.id, + "Error sending event because there are no subscribers: {:?}", e ); - }, - }; + TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::BroadcastSendError(format!("{:?}", e)), + ) + }); + + info!( + target: LOG_TARGET, + "Pending Transaction (TxId: {}) timed out after no response from counterparty", self.id + ); - Ok(true) + Err(TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::Timeout, + )) } } diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index c19d8dcd57..1639fe6540 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -33,16 +33,18 @@ use crate::{ transaction_receive_protocol::{TransactionReceiveProtocol, TransactionReceiveProtocolStage}, transaction_send_protocol::{TransactionSendProtocol, TransactionSendProtocolStage}, }, - storage::database::{ - CompletedTransaction, - TransactionBackend, - TransactionDatabase, - TransactionDirection, - TransactionStatus, + storage::{ + database::{TransactionBackend, TransactionDatabase}, + models::{CompletedTransaction, TransactionDirection, TransactionStatus}, + }, + tasks::{ + send_finalized_transaction::send_finalized_transaction_message, + send_transaction_cancelled::send_transaction_cancelled_message, + send_transaction_reply::send_transaction_reply, }, }, }; -use chrono::Utc; +use chrono::{NaiveDateTime, Utc}; use futures::{ channel::{mpsc, mpsc::Sender, oneshot}, pin_mut, @@ -105,8 +107,15 @@ pub struct PendingCoinbaseSpendingKey { /// `pending_inbound_transactions` - List of transaction protocols that have been received and responded to. /// `completed_transaction` - List of sent transactions that have been responded to and are completed. -pub struct TransactionService -where TBackend: TransactionBackend + Clone + 'static +pub struct TransactionService< + TTxStream, + TTxReplyStream, + TTxFinalizedStream, + MReplyStream, + BNResponseStream, + TBackend, + TTxCancelledStream, +> where TBackend: TransactionBackend + Clone + 'static { config: TransactionServiceConfig, db: TransactionDatabase, @@ -116,6 +125,7 @@ where TBackend: TransactionBackend + Clone + 'static transaction_finalized_stream: Option, mempool_response_stream: Option, base_node_response_stream: Option, + transaction_cancelled_stream: Option, request_stream: Option< reply_channel::Receiver>, >, @@ -134,14 +144,23 @@ where TBackend: TransactionBackend + Clone + 'static } #[allow(clippy::too_many_arguments)] -impl - TransactionService +impl + TransactionService< + TTxStream, + TTxReplyStream, + TTxFinalizedStream, + MReplyStream, + BNResponseStream, + TBackend, + TTxCancelledStream, + > where TTxStream: Stream>, TTxReplyStream: Stream>, TTxFinalizedStream: Stream>, MReplyStream: Stream>, BNResponseStream: Stream>, + TTxCancelledStream: Stream>, TBackend: TransactionBackend + Clone + 'static, { pub fn new( @@ -156,6 +175,7 @@ where transaction_finalized_stream: TTxFinalizedStream, mempool_response_stream: MReplyStream, base_node_response_stream: BNResponseStream, + transaction_cancelled_stream: TTxCancelledStream, output_manager_service: OutputManagerHandle, outbound_message_service: OutboundMessageRequester, event_publisher: TransactionEventSender, @@ -187,6 +207,7 @@ where transaction_finalized_stream: Some(transaction_finalized_stream), mempool_response_stream: Some(mempool_response_stream), base_node_response_stream: Some(base_node_response_stream), + transaction_cancelled_stream: Some(transaction_cancelled_stream), request_stream: Some(request_stream), event_publisher, node_identity, @@ -241,6 +262,12 @@ where .expect("Transaction Service initialized without base_node_response_stream") .fuse(); pin_mut!(base_node_response_stream); + let transaction_cancelled_stream = self + .transaction_cancelled_stream + .take() + .expect("Transaction Service initialized without transaction_cancelled_stream") + .fuse(); + pin_mut!(transaction_cancelled_stream); let mut send_transaction_protocol_handles: FuturesUnordered< JoinHandle>, @@ -372,6 +399,15 @@ where Err(resp) }); } + // Incoming messages from the Comms layer + msg = transaction_cancelled_stream.select_next_some() => { + let (origin_public_key, inner_msg) = msg.clone().into_origin_and_inner(); + trace!(target: LOG_TARGET, "Handling Transaction Cancelled message, Trace: {}", msg.dht_header.message_tag); + match self.handle_transaction_cancelled_message(origin_public_key, inner_msg, ).await { + Err(e) => warn!(target: LOG_TARGET, "Error handing Transaction Cancelled Message: {:?}", e), + _ => (), + } + } join_result = send_transaction_protocol_handles.select_next_some() => { trace!(target: LOG_TARGET, "Send Protocol for Transaction has ended with result {:?}", join_result); match join_result { @@ -619,6 +655,107 @@ where let tx_id = recipient_reply.tx_id; + // First we check if this Reply is for a cancelled Pending Outbound Tx or a Completed Tx + let cancelled_outbound_tx = self.db.get_cancelled_pending_outbound_transaction(tx_id).await; + let completed_tx = self.db.get_completed_transaction_cancelled_or_not(tx_id).await; + + // This closure will check if the timestamps are beyond the cooldown period + let check_cooldown = |timestamp: Option| { + if let Some(t) = timestamp { + // Check if the last reply is beyond the resend cooldown + if let Ok(elapsed_time) = Utc::now().naive_utc().signed_duration_since(t).to_std() { + if elapsed_time < self.resources.config.resend_response_cooldown { + trace!( + target: LOG_TARGET, + "A repeated Transaction Reply (TxId: {}) has been received before the resend cooldown has \ + expired. Ignoring.", + tx_id + ); + return false; + } + } + } + true + }; + + if let Ok(ctx) = completed_tx { + // Check that it is from the same person + if ctx.destination_public_key != source_pubkey { + return Err(TransactionServiceError::InvalidSourcePublicKey); + } + if !check_cooldown(ctx.last_send_timestamp) { + return Ok(()); + } + + if ctx.cancelled { + // Send a cancellation message + debug!( + target: LOG_TARGET, + "A repeated Transaction Reply (TxId: {}) has been received for cancelled completed transaction. \ + Transaction Cancelled response is being sent.", + tx_id + ); + tokio::spawn(send_transaction_cancelled_message( + tx_id, + source_pubkey.clone(), + self.resources.outbound_message_service.clone(), + )); + } else { + // Resend the reply + debug!( + target: LOG_TARGET, + "A repeated Transaction Reply (TxId: {}) has been received. Reply is being resent.", tx_id + ); + tokio::spawn(send_finalized_transaction_message( + tx_id, + ctx.transaction, + source_pubkey.clone(), + self.resources.outbound_message_service.clone(), + self.resources.config.direct_send_timeout, + )); + } + + if let Err(e) = self.resources.db.increment_send_count(tx_id).await { + warn!( + target: LOG_TARGET, + "Could not increment send count for completed transaction TxId {}: {:?}", tx_id, e + ); + } + return Ok(()); + } + + if let Ok(otx) = cancelled_outbound_tx { + // Check that it is from the same person + if otx.destination_public_key != source_pubkey { + return Err(TransactionServiceError::InvalidSourcePublicKey); + } + if !check_cooldown(otx.last_send_timestamp) { + return Ok(()); + } + + // Send a cancellation message + debug!( + target: LOG_TARGET, + "A repeated Transaction Reply (TxId: {}) has been received for cancelled pending outbound \ + transaction. Transaction Cancelled response is being sent.", + tx_id + ); + tokio::spawn(send_transaction_cancelled_message( + tx_id, + source_pubkey.clone(), + self.resources.outbound_message_service.clone(), + )); + + if let Err(e) = self.resources.db.increment_send_count(tx_id).await { + warn!( + target: LOG_TARGET, + "Could not increment send count for completed transaction TxId {}: {:?}", tx_id, e + ); + } + return Ok(()); + } + + // Is this a new Transaction Reply for an existing pending transaction? let sender = match self.pending_transaction_reply_senders.get_mut(&tx_id) { None => return Err(TransactionServiceError::TransactionDoesNotExistError), Some(s) => s, @@ -714,6 +851,32 @@ where Ok(()) } + /// Handle a Transaction Cancelled message received from the Comms layer + pub async fn handle_transaction_cancelled_message( + &mut self, + source_pubkey: CommsPublicKey, + transaction_cancelled: proto::TransactionCancelledMessage, + ) -> Result<(), TransactionServiceError> + { + let tx_id = transaction_cancelled.tx_id; + + // Check that an inbound transaction exists to be cancelled and that the Source Public key for that transaction + // is the same as the cancellation message + if let Ok(inbound_tx) = self.db.get_pending_inbound_transaction(tx_id).await { + if inbound_tx.source_public_key == source_pubkey { + self.cancel_transaction(tx_id).await?; + } else { + trace!( + target: LOG_TARGET, + "Received a Transaction Cancelled (TxId: {}) message from an unknown source, ignoring", + tx_id + ); + } + } + + Ok(()) + } + #[allow(clippy::map_entry)] async fn restart_all_send_transaction_protocols( &mut self, @@ -778,12 +941,59 @@ where traced_message_tag ); + // Check if this transaction has already been received. + if let Ok(inbound_tx) = self.db.get_pending_inbound_transaction(data.tx_id).await { + // Check that it is from the same person + if inbound_tx.source_public_key != source_pubkey { + return Err(TransactionServiceError::InvalidSourcePublicKey); + } + // Check if the last reply is beyond the resend cooldown + if let Some(timestamp) = inbound_tx.last_send_timestamp { + let elapsed_time = Utc::now() + .naive_utc() + .signed_duration_since(timestamp) + .to_std() + .map_err(|_| { + TransactionServiceError::ConversionError("duration::OutOfRangeError".to_string()) + })?; + if elapsed_time < self.resources.config.resend_response_cooldown { + trace!( + target: LOG_TARGET, + "A repeated Transaction (TxId: {}) has been received before the resend cooldown has \ + expired. Ignoring.", + inbound_tx.tx_id + ); + return Ok(()); + } + } + debug!( + target: LOG_TARGET, + "A repeated Transaction (TxId: {}) has been received. Reply is being resent.", inbound_tx.tx_id + ); + let tx_id = inbound_tx.tx_id; + // Ok we will resend the reply + tokio::spawn(send_transaction_reply( + inbound_tx, + self.resources.outbound_message_service.clone(), + self.resources.config.direct_send_timeout, + )); + if let Err(e) = self.resources.db.increment_send_count(tx_id).await { + warn!( + target: LOG_TARGET, + "Could not increment send count for inbound transaction TxId {}: {:?}", tx_id, e + ); + } + + return Ok(()); + } + if self.finalized_transaction_senders.contains_key(&data.tx_id) || self.receiver_transaction_cancellation_senders.contains_key(&data.tx_id) { trace!( target: LOG_TARGET, - "Transaction (TxId: {}) has already been received, this is probably a repeated message, Trace: {}.", + "Transaction (TxId: {}) has already been received, this is probably a repeated message, Trace: + {}.", data.tx_id, traced_message_tag ); @@ -993,7 +1203,7 @@ where .map_err(|resp| { error!( target: LOG_TARGET, - "Error restarting protocols for all coinbase transactions: {:?}", resp + "Error restarting protocols for all pending inbound transactions: {:?}", resp ); resp }); @@ -1004,7 +1214,7 @@ where .map_err(|resp| { error!( target: LOG_TARGET, - "Error restarting protocols for all pending inbound transactions: {:?}", resp + "Error restarting protocols for all coinbase transactions: {:?}", resp ); resp }); @@ -1647,7 +1857,7 @@ where service::OutputManagerService, storage::{database::OutputManagerDatabase, memory_db::OutputManagerMemoryDatabase}, }, - transaction_service::{handle::TransactionServiceHandle, storage::database::InboundTransaction}, + transaction_service::{handle::TransactionServiceHandle, storage::models::InboundTransaction}, }; use futures::stream; use tari_core::{ diff --git a/base_layer/wallet/src/transaction_service/storage/database.rs b/base_layer/wallet/src/transaction_service/storage/database.rs index d3ba0f6a4d..9ccfdc9735 100644 --- a/base_layer/wallet/src/transaction_service/storage/database.rs +++ b/base_layer/wallet/src/transaction_service/storage/database.rs @@ -20,25 +20,32 @@ // 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 crate::{output_manager_service::TxId, transaction_service::error::TransactionStorageError}; +use crate::{ + output_manager_service::TxId, + transaction_service::{ + error::TransactionStorageError, + storage::models::{ + CompletedTransaction, + InboundTransaction, + OutboundTransaction, + TransactionDirection, + TransactionStatus, + }, + }, +}; use aes_gcm::Aes256Gcm; -use chrono::{NaiveDateTime, Utc}; +#[cfg(feature = "test_harness")] +use chrono::NaiveDateTime; +use chrono::Utc; use log::*; -use serde::{Deserialize, Serialize}; + use std::{ collections::HashMap, - convert::TryFrom, fmt::{Display, Error, Formatter}, sync::Arc, }; use tari_comms::types::CommsPublicKey; -use tari_core::transactions::{ - tari_amount::MicroTari, - transaction::Transaction, - types::{BlindingFactor, PrivateKey}, - ReceiverTransactionProtocol, - SenderTransactionProtocol, -}; +use tari_core::transactions::{tari_amount::MicroTari, transaction::Transaction, types::BlindingFactor}; const LOG_TARGET: &str = "wallet::transaction_service::database"; @@ -77,7 +84,7 @@ pub trait TransactionBackend: Send + Sync { fn cancel_completed_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; /// Cancel Completed transaction, this will update the transaction status fn cancel_pending_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; - /// Search all oending transaction for the provided tx_id and if it exists return the public key of the counterparty + /// Search all pending transaction for the provided tx_id and if it exists return the public key of the counterparty fn get_pending_transaction_counterparty_pub_key_by_tx_id( &self, tx_id: TxId, @@ -97,218 +104,8 @@ pub trait TransactionBackend: Send + Sync { fn apply_encryption(&self, cipher: Aes256Gcm) -> Result<(), TransactionStorageError>; /// Remove encryption from the backend. fn remove_encryption(&self) -> Result<(), TransactionStorageError>; -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum TransactionStatus { - /// This transaction has been completed between the parties but has not been broadcast to the base layer network. - Completed, - /// This transaction has been broadcast to the base layer network and is currently in one or more base node - /// mempools. - Broadcast, - /// This transaction has been mined and included in a block. - Mined, - /// This transaction was generated as part of importing a spendable UTXO - Imported, - /// This transaction is still being negotiated by the parties - Pending, - /// This is a created Coinbase Transaction - Coinbase, -} - -impl TryFrom for TransactionStatus { - type Error = TransactionStorageError; - - fn try_from(value: i32) -> Result { - match value { - 0 => Ok(TransactionStatus::Completed), - 1 => Ok(TransactionStatus::Broadcast), - 2 => Ok(TransactionStatus::Mined), - 3 => Ok(TransactionStatus::Imported), - 4 => Ok(TransactionStatus::Pending), - _ => Err(TransactionStorageError::ConversionError), - } - } -} - -impl Default for TransactionStatus { - fn default() -> Self { - TransactionStatus::Pending - } -} - -impl Display for TransactionStatus { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - // No struct or tuple variants - match self { - TransactionStatus::Completed => write!(f, "Completed"), - TransactionStatus::Broadcast => write!(f, "Broadcast"), - TransactionStatus::Mined => write!(f, "Mined"), - TransactionStatus::Imported => write!(f, "Imported"), - TransactionStatus::Pending => write!(f, "Pending"), - TransactionStatus::Coinbase => write!(f, "Coinbase"), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct InboundTransaction { - pub tx_id: TxId, - pub source_public_key: CommsPublicKey, - pub amount: MicroTari, - pub receiver_protocol: ReceiverTransactionProtocol, - pub status: TransactionStatus, - pub message: String, - pub timestamp: NaiveDateTime, - pub cancelled: bool, - pub direct_send_success: bool, -} - -impl InboundTransaction { - pub fn new( - tx_id: TxId, - source_public_key: CommsPublicKey, - amount: MicroTari, - receiver_protocol: ReceiverTransactionProtocol, - status: TransactionStatus, - message: String, - timestamp: NaiveDateTime, - ) -> Self - { - Self { - tx_id, - source_public_key, - amount, - receiver_protocol, - status, - message, - timestamp, - cancelled: false, - direct_send_success: false, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct OutboundTransaction { - pub tx_id: TxId, - pub destination_public_key: CommsPublicKey, - pub amount: MicroTari, - pub fee: MicroTari, - pub sender_protocol: SenderTransactionProtocol, - pub status: TransactionStatus, - pub message: String, - pub timestamp: NaiveDateTime, - pub cancelled: bool, - pub direct_send_success: bool, -} - -impl OutboundTransaction { - #[allow(clippy::too_many_arguments)] - pub fn new( - tx_id: TxId, - destination_public_key: CommsPublicKey, - amount: MicroTari, - fee: MicroTari, - sender_protocol: SenderTransactionProtocol, - status: TransactionStatus, - message: String, - timestamp: NaiveDateTime, - direct_send_success: bool, - ) -> Self - { - Self { - tx_id, - destination_public_key, - amount, - fee, - sender_protocol, - status, - message, - timestamp, - cancelled: false, - direct_send_success, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct CompletedTransaction { - pub tx_id: TxId, - pub source_public_key: CommsPublicKey, - pub destination_public_key: CommsPublicKey, - pub amount: MicroTari, - pub fee: MicroTari, - pub transaction: Transaction, - pub status: TransactionStatus, - pub message: String, - pub timestamp: NaiveDateTime, - pub cancelled: bool, - pub direction: TransactionDirection, - pub coinbase_block_height: Option, -} - -impl CompletedTransaction { - #[allow(clippy::too_many_arguments)] - pub fn new( - tx_id: TxId, - source_public_key: CommsPublicKey, - destination_public_key: CommsPublicKey, - amount: MicroTari, - fee: MicroTari, - transaction: Transaction, - status: TransactionStatus, - message: String, - timestamp: NaiveDateTime, - direction: TransactionDirection, - coinbase_block_height: Option, - ) -> Self - { - Self { - tx_id, - source_public_key, - destination_public_key, - amount, - fee, - transaction, - status, - message, - timestamp, - cancelled: false, - direction, - coinbase_block_height, - } - } -} -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub enum TransactionDirection { - Inbound, - Outbound, - Unknown, -} - -impl TryFrom for TransactionDirection { - type Error = TransactionStorageError; - - fn try_from(value: i32) -> Result { - match value { - 0 => Ok(TransactionDirection::Inbound), - 1 => Ok(TransactionDirection::Outbound), - 2 => Ok(TransactionDirection::Unknown), - _ => Err(TransactionStorageError::ConversionError), - } - } -} - -impl Display for TransactionDirection { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - // No struct or tuple variants - match self { - TransactionDirection::Inbound => write!(f, "Inbound"), - TransactionDirection::Outbound => write!(f, "Outbound"), - TransactionDirection::Unknown => write!(f, "Unknown"), - } - } + /// Increment the send counter and timestamp of a transaction + fn increment_send_count(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; } #[derive(Debug, Clone, PartialEq)] @@ -324,7 +121,6 @@ pub enum DbKey { CancelledCompletedTransactions, CancelledPendingOutboundTransaction(TxId), CancelledPendingInboundTransaction(TxId), - CancelledCompletedTransaction(TxId), } #[derive(Debug)] @@ -348,77 +144,6 @@ pub enum WriteOperation { Remove(DbKey), } -impl From for InboundTransaction { - fn from(ct: CompletedTransaction) -> Self { - Self { - tx_id: ct.tx_id, - source_public_key: ct.source_public_key, - amount: ct.amount, - receiver_protocol: ReceiverTransactionProtocol::new_placeholder(), - status: ct.status, - message: ct.message, - timestamp: ct.timestamp, - cancelled: ct.cancelled, - direct_send_success: false, - } - } -} - -impl From for OutboundTransaction { - fn from(ct: CompletedTransaction) -> Self { - Self { - tx_id: ct.tx_id, - destination_public_key: ct.destination_public_key, - amount: ct.amount, - fee: ct.fee, - sender_protocol: SenderTransactionProtocol::new_placeholder(), - status: ct.status, - message: ct.message, - timestamp: ct.timestamp, - cancelled: ct.cancelled, - direct_send_success: false, - } - } -} - -impl From for CompletedTransaction { - fn from(tx: OutboundTransaction) -> Self { - Self { - tx_id: tx.tx_id, - source_public_key: Default::default(), - destination_public_key: tx.destination_public_key, - amount: tx.amount, - fee: tx.fee, - status: tx.status, - message: tx.message, - timestamp: tx.timestamp, - cancelled: tx.cancelled, - transaction: Transaction::new(vec![], vec![], vec![], PrivateKey::default()), - direction: TransactionDirection::Outbound, - coinbase_block_height: None, - } - } -} - -impl From for CompletedTransaction { - fn from(tx: InboundTransaction) -> Self { - Self { - tx_id: tx.tx_id, - source_public_key: tx.source_public_key, - destination_public_key: Default::default(), - amount: tx.amount, - fee: MicroTari::from(0), - status: tx.status, - message: tx.message, - timestamp: tx.timestamp, - cancelled: tx.cancelled, - transaction: Transaction::new(vec![], vec![], vec![], PrivateKey::default()), - direction: TransactionDirection::Inbound, - coinbase_block_height: None, - } - } -} - /// This structure holds an inner type that implements the `TransactionBackend` trait and contains the more complex /// data access logic required by the module built onto the functionality defined by the trait #[derive(Clone)] @@ -612,12 +337,32 @@ where T: TransactionBackend + 'static ) -> Result { let db_clone = self.db.clone(); - let key = if cancelled { - DbKey::CancelledCompletedTransaction(tx_id) - } else { - DbKey::CompletedTransaction(tx_id) - }; - let t = tokio::task::spawn_blocking(move || match db_clone.fetch(&key) { + let key = DbKey::CompletedTransaction(tx_id); + let t = tokio::task::spawn_blocking(move || match db_clone.fetch(&DbKey::CompletedTransaction(tx_id)) { + Ok(None) => Err(TransactionStorageError::ValueNotFound(key)), + Ok(Some(DbValue::CompletedTransaction(pt))) => { + if pt.cancelled == cancelled { + Ok(pt) + } else { + Err(TransactionStorageError::ValueNotFound(key)) + } + }, + Ok(Some(other)) => unexpected_result(key, other), + Err(e) => log_error(key, e), + }) + .await + .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(*t) + } + + pub async fn get_completed_transaction_cancelled_or_not( + &self, + tx_id: TxId, + ) -> Result + { + let db_clone = self.db.clone(); + let key = DbKey::CompletedTransaction(tx_id); + let t = tokio::task::spawn_blocking(move || match db_clone.fetch(&DbKey::CompletedTransaction(tx_id)) { Ok(None) => Err(TransactionStorageError::ValueNotFound(key)), Ok(Some(DbValue::CompletedTransaction(pt))) => Ok(pt), Ok(Some(other)) => unexpected_result(key, other), @@ -899,6 +644,14 @@ where T: TransactionBackend + 'static .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string())) .and_then(|inner_result| inner_result) } + + pub async fn increment_send_count(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { + let db_clone = self.db.clone(); + tokio::task::spawn_blocking(move || db_clone.increment_send_count(tx_id)) + .await + .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(()) + } } impl Display for DbKey { @@ -924,7 +677,6 @@ impl Display for DbKey { DbKey::CancelledPendingInboundTransaction(_) => { f.write_str(&"Cancelled Pending Inbound Transaction".to_string()) }, - DbKey::CancelledCompletedTransaction(_) => f.write_str(&"Cancelled Completed Transaction".to_string()), } } } diff --git a/base_layer/wallet/src/transaction_service/storage/memory_db.rs b/base_layer/wallet/src/transaction_service/storage/memory_db.rs index a26a63fc64..96229f818e 100644 --- a/base_layer/wallet/src/transaction_service/storage/memory_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/memory_db.rs @@ -24,22 +24,16 @@ use crate::{ output_manager_service::TxId, transaction_service::{ error::TransactionStorageError, - storage::database::{ - CompletedTransaction, - DbKey, - DbKeyValuePair, - DbValue, - InboundTransaction, - OutboundTransaction, - TransactionBackend, - TransactionStatus, - WriteOperation, + storage::{ + database::{DbKey, DbKeyValuePair, DbValue, TransactionBackend, WriteOperation}, + models::{CompletedTransaction, InboundTransaction, OutboundTransaction, TransactionStatus}, }, }, }; use aes_gcm::Aes256Gcm; #[cfg(feature = "test_harness")] use chrono::NaiveDateTime; +use chrono::Utc; use std::{ collections::HashMap, sync::{Arc, RwLock}, @@ -101,9 +95,7 @@ impl TransactionBackend for TransactionMemoryDatabase { DbKey::CompletedTransaction(t) => { let mut result = None; if let Some(v) = db.completed_transactions.get(t) { - if !v.cancelled { - result = Some(DbValue::CompletedTransaction(Box::new(v.clone()))); - } + result = Some(DbValue::CompletedTransaction(Box::new(v.clone()))); } result }, @@ -184,15 +176,6 @@ impl TransactionBackend for TransactionMemoryDatabase { } result }, - DbKey::CancelledCompletedTransaction(t) => { - let mut result = None; - if let Some(v) = db.completed_transactions.get(t) { - if v.cancelled { - result = Some(DbValue::CompletedTransaction(Box::new(v.clone()))); - } - } - result - }, }; Ok(result) @@ -207,7 +190,7 @@ impl TransactionBackend for TransactionMemoryDatabase { DbKey::PendingInboundTransaction(k) => { db.pending_inbound_transactions.get(k).map_or(false, |v| !v.cancelled) }, - DbKey::CompletedTransaction(k) => db.completed_transactions.get(k).map_or(false, |v| !v.cancelled), + DbKey::CompletedTransaction(k) => db.completed_transactions.get(k).is_some(), DbKey::PendingOutboundTransactions => false, DbKey::PendingInboundTransactions => false, DbKey::CompletedTransactions => false, @@ -220,7 +203,6 @@ impl TransactionBackend for TransactionMemoryDatabase { DbKey::CancelledPendingInboundTransaction(k) => { db.pending_inbound_transactions.get(k).map_or(false, |v| v.cancelled) }, - DbKey::CancelledCompletedTransaction(k) => db.completed_transactions.get(k).map_or(false, |v| v.cancelled), }; Ok(result) @@ -268,7 +250,7 @@ impl TransactionBackend for TransactionMemoryDatabase { )); } }, - DbKey::CompletedTransaction(k) | DbKey::CancelledCompletedTransaction(k) => { + DbKey::CompletedTransaction(k) => { if let Some(p) = db.completed_transactions.remove(&k) { return Ok(Some(DbValue::CompletedTransaction(Box::new(p)))); } else { @@ -472,4 +454,23 @@ impl TransactionBackend for TransactionMemoryDatabase { Ok(()) } + + fn increment_send_count(&self, tx_id: u64) -> Result<(), TransactionStorageError> { + let mut db = acquire_write_lock!(self.db); + + if let Some(tx) = db.completed_transactions.get_mut(&tx_id) { + tx.send_count += 1; + tx.last_send_timestamp = Some(Utc::now().naive_utc()); + } else if let Some(tx) = db.pending_outbound_transactions.get_mut(&tx_id) { + tx.send_count += 1; + tx.last_send_timestamp = Some(Utc::now().naive_utc()); + } else if let Some(tx) = db.pending_inbound_transactions.get_mut(&tx_id) { + tx.send_count += 1; + tx.last_send_timestamp = Some(Utc::now().naive_utc()); + } else { + return Err(TransactionStorageError::ValuesNotFound); + } + + Ok(()) + } } diff --git a/base_layer/wallet/src/transaction_service/storage/mod.rs b/base_layer/wallet/src/transaction_service/storage/mod.rs index c6c73087f7..664d40d2de 100644 --- a/base_layer/wallet/src/transaction_service/storage/mod.rs +++ b/base_layer/wallet/src/transaction_service/storage/mod.rs @@ -22,4 +22,5 @@ pub mod database; pub mod memory_db; +pub mod models; pub mod sqlite_db; diff --git a/base_layer/wallet/src/transaction_service/storage/models.rs b/base_layer/wallet/src/transaction_service/storage/models.rs new file mode 100644 index 0000000000..bb6099f873 --- /dev/null +++ b/base_layer/wallet/src/transaction_service/storage/models.rs @@ -0,0 +1,339 @@ +// Copyright 2020. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{output_manager_service::TxId, transaction_service::error::TransactionStorageError}; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use std::{ + convert::TryFrom, + fmt::{Display, Error, Formatter}, +}; +use tari_comms::types::CommsPublicKey; +use tari_core::transactions::{ + tari_amount::MicroTari, + transaction::Transaction, + types::PrivateKey, + ReceiverTransactionProtocol, + SenderTransactionProtocol, +}; +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum TransactionStatus { + /// This transaction has been completed between the parties but has not been broadcast to the base layer network. + Completed, + /// This transaction has been broadcast to the base layer network and is currently in one or more base node + /// mempools. + Broadcast, + /// This transaction has been mined and included in a block. + Mined, + /// This transaction was generated as part of importing a spendable UTXO + Imported, + /// This transaction is still being negotiated by the parties + Pending, + /// This is a created Coinbase Transaction + Coinbase, +} + +impl TryFrom for TransactionStatus { + type Error = TransactionStorageError; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(TransactionStatus::Completed), + 1 => Ok(TransactionStatus::Broadcast), + 2 => Ok(TransactionStatus::Mined), + 3 => Ok(TransactionStatus::Imported), + 4 => Ok(TransactionStatus::Pending), + _ => Err(TransactionStorageError::ConversionError), + } + } +} + +impl Default for TransactionStatus { + fn default() -> Self { + TransactionStatus::Pending + } +} + +impl Display for TransactionStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + // No struct or tuple variants + match self { + TransactionStatus::Completed => write!(f, "Completed"), + TransactionStatus::Broadcast => write!(f, "Broadcast"), + TransactionStatus::Mined => write!(f, "Mined"), + TransactionStatus::Imported => write!(f, "Imported"), + TransactionStatus::Pending => write!(f, "Pending"), + TransactionStatus::Coinbase => write!(f, "Coinbase"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct InboundTransaction { + pub tx_id: TxId, + pub source_public_key: CommsPublicKey, + pub amount: MicroTari, + pub receiver_protocol: ReceiverTransactionProtocol, + pub status: TransactionStatus, + pub message: String, + pub timestamp: NaiveDateTime, + pub cancelled: bool, + pub direct_send_success: bool, + pub send_count: u32, + pub last_send_timestamp: Option, +} + +impl InboundTransaction { + pub fn new( + tx_id: TxId, + source_public_key: CommsPublicKey, + amount: MicroTari, + receiver_protocol: ReceiverTransactionProtocol, + status: TransactionStatus, + message: String, + timestamp: NaiveDateTime, + ) -> Self + { + Self { + tx_id, + source_public_key, + amount, + receiver_protocol, + status, + message, + timestamp, + cancelled: false, + direct_send_success: false, + send_count: 0, + last_send_timestamp: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct OutboundTransaction { + pub tx_id: TxId, + pub destination_public_key: CommsPublicKey, + pub amount: MicroTari, + pub fee: MicroTari, + pub sender_protocol: SenderTransactionProtocol, + pub status: TransactionStatus, + pub message: String, + pub timestamp: NaiveDateTime, + pub cancelled: bool, + pub direct_send_success: bool, + pub send_count: u32, + pub last_send_timestamp: Option, +} + +impl OutboundTransaction { + #[allow(clippy::too_many_arguments)] + pub fn new( + tx_id: TxId, + destination_public_key: CommsPublicKey, + amount: MicroTari, + fee: MicroTari, + sender_protocol: SenderTransactionProtocol, + status: TransactionStatus, + message: String, + timestamp: NaiveDateTime, + direct_send_success: bool, + ) -> Self + { + Self { + tx_id, + destination_public_key, + amount, + fee, + sender_protocol, + status, + message, + timestamp, + cancelled: false, + direct_send_success, + send_count: 0, + last_send_timestamp: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CompletedTransaction { + pub tx_id: TxId, + pub source_public_key: CommsPublicKey, + pub destination_public_key: CommsPublicKey, + pub amount: MicroTari, + pub fee: MicroTari, + pub transaction: Transaction, + pub status: TransactionStatus, + pub message: String, + pub timestamp: NaiveDateTime, + pub cancelled: bool, + pub direction: TransactionDirection, + pub coinbase_block_height: Option, + pub send_count: u32, + pub last_send_timestamp: Option, +} + +impl CompletedTransaction { + #[allow(clippy::too_many_arguments)] + pub fn new( + tx_id: TxId, + source_public_key: CommsPublicKey, + destination_public_key: CommsPublicKey, + amount: MicroTari, + fee: MicroTari, + transaction: Transaction, + status: TransactionStatus, + message: String, + timestamp: NaiveDateTime, + direction: TransactionDirection, + coinbase_block_height: Option, + ) -> Self + { + Self { + tx_id, + source_public_key, + destination_public_key, + amount, + fee, + transaction, + status, + message, + timestamp, + cancelled: false, + direction, + coinbase_block_height, + send_count: 0, + last_send_timestamp: None, + } + } +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum TransactionDirection { + Inbound, + Outbound, + Unknown, +} + +impl TryFrom for TransactionDirection { + type Error = TransactionStorageError; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(TransactionDirection::Inbound), + 1 => Ok(TransactionDirection::Outbound), + 2 => Ok(TransactionDirection::Unknown), + _ => Err(TransactionStorageError::ConversionError), + } + } +} + +impl Display for TransactionDirection { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + // No struct or tuple variants + match self { + TransactionDirection::Inbound => write!(f, "Inbound"), + TransactionDirection::Outbound => write!(f, "Outbound"), + TransactionDirection::Unknown => write!(f, "Unknown"), + } + } +} + +impl From for InboundTransaction { + fn from(ct: CompletedTransaction) -> Self { + Self { + tx_id: ct.tx_id, + source_public_key: ct.source_public_key, + amount: ct.amount, + receiver_protocol: ReceiverTransactionProtocol::new_placeholder(), + status: ct.status, + message: ct.message, + timestamp: ct.timestamp, + cancelled: ct.cancelled, + direct_send_success: false, + send_count: 0, + last_send_timestamp: None, + } + } +} + +impl From for OutboundTransaction { + fn from(ct: CompletedTransaction) -> Self { + Self { + tx_id: ct.tx_id, + destination_public_key: ct.destination_public_key, + amount: ct.amount, + fee: ct.fee, + sender_protocol: SenderTransactionProtocol::new_placeholder(), + status: ct.status, + message: ct.message, + timestamp: ct.timestamp, + cancelled: ct.cancelled, + direct_send_success: false, + send_count: 0, + last_send_timestamp: None, + } + } +} + +impl From for CompletedTransaction { + fn from(tx: OutboundTransaction) -> Self { + Self { + tx_id: tx.tx_id, + source_public_key: Default::default(), + destination_public_key: tx.destination_public_key, + amount: tx.amount, + fee: tx.fee, + status: tx.status, + message: tx.message, + timestamp: tx.timestamp, + cancelled: tx.cancelled, + transaction: Transaction::new(vec![], vec![], vec![], PrivateKey::default()), + direction: TransactionDirection::Outbound, + coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, + } + } +} + +impl From for CompletedTransaction { + fn from(tx: InboundTransaction) -> Self { + Self { + tx_id: tx.tx_id, + source_public_key: tx.source_public_key, + destination_public_key: Default::default(), + amount: tx.amount, + fee: MicroTari::from(0), + status: tx.status, + message: tx.message, + timestamp: tx.timestamp, + cancelled: tx.cancelled, + transaction: Transaction::new(vec![], vec![], vec![], PrivateKey::default()), + direction: TransactionDirection::Inbound, + coinbase_block_height: None, + send_count: 0, + last_send_timestamp: 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 c4116cd697..8b5566b50d 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -25,23 +25,21 @@ use crate::{ schema::{completed_transactions, inbound_transactions, outbound_transactions}, transaction_service::{ error::TransactionStorageError, - storage::database::{ - CompletedTransaction, - DbKey, - DbKeyValuePair, - DbValue, - InboundTransaction, - OutboundTransaction, - TransactionBackend, - TransactionDirection, - TransactionStatus, - WriteOperation, + storage::{ + database::{DbKey, DbKeyValuePair, DbValue, TransactionBackend, WriteOperation}, + models::{ + CompletedTransaction, + InboundTransaction, + OutboundTransaction, + TransactionDirection, + TransactionStatus, + }, }, }, util::encryption::{decrypt_bytes_integral_nonce, encrypt_bytes_integral_nonce, Encryptable}, }; use aes_gcm::{self, aead::Error as AeadError, Aes256Gcm}; -use chrono::NaiveDateTime; +use chrono::{NaiveDateTime, Utc}; use diesel::{prelude::*, result::Error as DieselError, SqliteConnection}; use log::*; use std::{ @@ -83,7 +81,7 @@ impl TransactionServiceSqliteDatabase { fn insert(&self, kvp: DbKeyValuePair, conn: MutexGuard) -> Result<(), TransactionStorageError> { match kvp { DbKeyValuePair::PendingOutboundTransaction(k, v) => { - if OutboundTransactionSql::find(k, false, &(*conn)).is_ok() { + if OutboundTransactionSql::find_by_cancelled(k, false, &(*conn)).is_ok() { return Err(TransactionStorageError::DuplicateOutput); } let mut o = OutboundTransactionSql::try_from(*v)?; @@ -100,7 +98,7 @@ impl TransactionServiceSqliteDatabase { i.commit(&(*conn))?; }, DbKeyValuePair::CompletedTransaction(k, v) => { - if CompletedTransactionSql::find(k, false, &(*conn)).is_ok() { + if CompletedTransactionSql::find_by_cancelled(k, false, &(*conn)).is_ok() { return Err(TransactionStorageError::DuplicateOutput); } let mut c = CompletedTransactionSql::try_from(*v)?; @@ -119,7 +117,8 @@ impl TransactionServiceSqliteDatabase { ) -> Result, TransactionStorageError> { match key { - DbKey::PendingOutboundTransaction(k) => match OutboundTransactionSql::find(k, false, &(*conn)) { + DbKey::PendingOutboundTransaction(k) => match OutboundTransactionSql::find_by_cancelled(k, false, &(*conn)) + { Ok(mut v) => { v.delete(&(*conn))?; self.decrypt_if_necessary(&mut v)?; @@ -145,7 +144,7 @@ impl TransactionServiceSqliteDatabase { ), Err(e) => Err(e), }, - DbKey::CompletedTransaction(k) => match CompletedTransactionSql::find(k, false, &(*conn)) { + DbKey::CompletedTransaction(k) => match CompletedTransactionSql::find_by_cancelled(k, false, &(*conn)) { Ok(mut v) => { v.delete(&(*conn))?; self.decrypt_if_necessary(&mut v)?; @@ -164,18 +163,20 @@ impl TransactionServiceSqliteDatabase { DbKey::CancelledPendingOutboundTransactions => Err(TransactionStorageError::OperationNotSupported), DbKey::CancelledPendingInboundTransactions => Err(TransactionStorageError::OperationNotSupported), DbKey::CancelledCompletedTransactions => Err(TransactionStorageError::OperationNotSupported), - DbKey::CancelledPendingOutboundTransaction(k) => match OutboundTransactionSql::find(k, true, &(*conn)) { - Ok(mut v) => { - v.delete(&(*conn))?; - self.decrypt_if_necessary(&mut v)?; - Ok(Some(DbValue::PendingOutboundTransaction(Box::new( - OutboundTransaction::try_from(v)?, - )))) - }, - Err(TransactionStorageError::DieselError(DieselError::NotFound)) => Err( - TransactionStorageError::ValueNotFound(DbKey::CancelledPendingOutboundTransaction(k)), - ), - Err(e) => Err(e), + DbKey::CancelledPendingOutboundTransaction(k) => { + match OutboundTransactionSql::find_by_cancelled(k, true, &(*conn)) { + Ok(mut v) => { + v.delete(&(*conn))?; + self.decrypt_if_necessary(&mut v)?; + Ok(Some(DbValue::PendingOutboundTransaction(Box::new( + OutboundTransaction::try_from(v)?, + )))) + }, + Err(TransactionStorageError::DieselError(DieselError::NotFound)) => Err( + TransactionStorageError::ValueNotFound(DbKey::CancelledPendingOutboundTransaction(k)), + ), + Err(e) => Err(e), + } }, DbKey::CancelledPendingInboundTransaction(k) => match InboundTransactionSql::find(k, true, &(*conn)) { Ok(mut v) => { @@ -190,19 +191,6 @@ impl TransactionServiceSqliteDatabase { ), Err(e) => Err(e), }, - DbKey::CancelledCompletedTransaction(k) => match CompletedTransactionSql::find(k, true, &(*conn)) { - Ok(mut v) => { - v.delete(&(*conn))?; - self.decrypt_if_necessary(&mut v)?; - Ok(Some(DbValue::CompletedTransaction(Box::new( - CompletedTransaction::try_from(v)?, - )))) - }, - Err(TransactionStorageError::DieselError(DieselError::NotFound)) => Err( - TransactionStorageError::ValueNotFound(DbKey::CancelledCompletedTransaction(k)), - ), - Err(e) => Err(e), - }, } } @@ -230,16 +218,18 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { let conn = acquire_lock!(self.database_connection); let result = match key { - DbKey::PendingOutboundTransaction(t) => match OutboundTransactionSql::find(*t, false, &(*conn)) { - Ok(mut o) => { - self.decrypt_if_necessary(&mut o)?; - - Some(DbValue::PendingOutboundTransaction(Box::new( - OutboundTransaction::try_from(o)?, - ))) - }, - Err(TransactionStorageError::DieselError(DieselError::NotFound)) => None, - Err(e) => return Err(e), + DbKey::PendingOutboundTransaction(t) => { + match OutboundTransactionSql::find_by_cancelled(*t, false, &(*conn)) { + Ok(mut o) => { + self.decrypt_if_necessary(&mut o)?; + + Some(DbValue::PendingOutboundTransaction(Box::new( + OutboundTransaction::try_from(o)?, + ))) + }, + Err(TransactionStorageError::DieselError(DieselError::NotFound)) => None, + Err(e) => return Err(e), + } }, DbKey::PendingInboundTransaction(t) => match InboundTransactionSql::find(*t, false, &(*conn)) { Ok(mut i) => { @@ -251,7 +241,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Err(TransactionStorageError::DieselError(DieselError::NotFound)) => None, Err(e) => return Err(e), }, - DbKey::CompletedTransaction(t) => match CompletedTransactionSql::find(*t, false, &(*conn)) { + DbKey::CompletedTransaction(t) => match CompletedTransactionSql::find(*t, &(*conn)) { Ok(mut c) => { self.decrypt_if_necessary(&mut c)?; Some(DbValue::CompletedTransaction(Box::new(CompletedTransaction::try_from( @@ -315,16 +305,18 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Some(DbValue::CompletedTransactions(result)) }, - DbKey::CancelledPendingOutboundTransaction(t) => match OutboundTransactionSql::find(*t, true, &(*conn)) { - Ok(mut o) => { - self.decrypt_if_necessary(&mut o)?; - - Some(DbValue::PendingOutboundTransaction(Box::new( - OutboundTransaction::try_from(o)?, - ))) - }, - Err(TransactionStorageError::DieselError(DieselError::NotFound)) => None, - Err(e) => return Err(e), + DbKey::CancelledPendingOutboundTransaction(t) => { + match OutboundTransactionSql::find_by_cancelled(*t, true, &(*conn)) { + Ok(mut o) => { + self.decrypt_if_necessary(&mut o)?; + + Some(DbValue::PendingOutboundTransaction(Box::new( + OutboundTransaction::try_from(o)?, + ))) + }, + Err(TransactionStorageError::DieselError(DieselError::NotFound)) => None, + Err(e) => return Err(e), + } }, DbKey::CancelledPendingInboundTransaction(t) => match InboundTransactionSql::find(*t, true, &(*conn)) { Ok(mut i) => { @@ -336,16 +328,6 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Err(TransactionStorageError::DieselError(DieselError::NotFound)) => None, Err(e) => return Err(e), }, - DbKey::CancelledCompletedTransaction(t) => match CompletedTransactionSql::find(*t, true, &(*conn)) { - Ok(mut c) => { - self.decrypt_if_necessary(&mut c)?; - Some(DbValue::CompletedTransaction(Box::new(CompletedTransaction::try_from( - c, - )?))) - }, - Err(TransactionStorageError::DieselError(DieselError::NotFound)) => None, - Err(e) => return Err(e), - }, }; Ok(result) @@ -355,18 +337,21 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { let conn = acquire_lock!(self.database_connection); let result = match key { - DbKey::PendingOutboundTransaction(k) => OutboundTransactionSql::find(*k, false, &(*conn)).is_ok(), + DbKey::PendingOutboundTransaction(k) => { + OutboundTransactionSql::find_by_cancelled(*k, false, &(*conn)).is_ok() + }, DbKey::PendingInboundTransaction(k) => InboundTransactionSql::find(*k, false, &(*conn)).is_ok(), - DbKey::CompletedTransaction(k) => CompletedTransactionSql::find(*k, false, &(*conn)).is_ok(), + DbKey::CompletedTransaction(k) => CompletedTransactionSql::find(*k, &(*conn)).is_ok(), DbKey::PendingOutboundTransactions => false, DbKey::PendingInboundTransactions => false, DbKey::CompletedTransactions => false, DbKey::CancelledPendingOutboundTransactions => false, DbKey::CancelledPendingInboundTransactions => false, DbKey::CancelledCompletedTransactions => false, - DbKey::CancelledPendingOutboundTransaction(k) => OutboundTransactionSql::find(*k, true, &(*conn)).is_ok(), + DbKey::CancelledPendingOutboundTransaction(k) => { + OutboundTransactionSql::find_by_cancelled(*k, true, &(*conn)).is_ok() + }, DbKey::CancelledPendingInboundTransaction(k) => InboundTransactionSql::find(*k, true, &(*conn)).is_ok(), - DbKey::CancelledCompletedTransaction(k) => CompletedTransactionSql::find(*k, true, &(*conn)).is_ok(), }; Ok(result) @@ -384,9 +369,11 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { fn transaction_exists(&self, tx_id: u64) -> Result { let conn = acquire_lock!(self.database_connection); - Ok(OutboundTransactionSql::find(tx_id, false, &(*conn)).is_ok() || - InboundTransactionSql::find(tx_id, false, &(*conn)).is_ok() || - CompletedTransactionSql::find(tx_id, false, &(*conn)).is_ok()) + Ok( + OutboundTransactionSql::find_by_cancelled(tx_id, false, &(*conn)).is_ok() || + InboundTransactionSql::find(tx_id, false, &(*conn)).is_ok() || + CompletedTransactionSql::find_by_cancelled(tx_id, false, &(*conn)).is_ok(), + ) } fn get_pending_transaction_counterparty_pub_key_by_tx_id( @@ -396,7 +383,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { { let conn = acquire_lock!(self.database_connection); - if let Ok(mut outbound_tx_sql) = OutboundTransactionSql::find(tx_id, false, &(*conn)) { + if let Ok(mut outbound_tx_sql) = OutboundTransactionSql::find_by_cancelled(tx_id, false, &(*conn)) { self.decrypt_if_necessary(&mut outbound_tx_sql)?; let outbound_tx = OutboundTransaction::try_from(outbound_tx_sql)?; return Ok(outbound_tx.destination_public_key); @@ -418,11 +405,11 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { { let conn = acquire_lock!(self.database_connection); - if CompletedTransactionSql::find(tx_id, false, &(*conn)).is_ok() { + if CompletedTransactionSql::find_by_cancelled(tx_id, false, &(*conn)).is_ok() { return Err(TransactionStorageError::TransactionAlreadyExists); } - match OutboundTransactionSql::find(tx_id, false, &(*conn)) { + match OutboundTransactionSql::find_by_cancelled(tx_id, false, &(*conn)) { Ok(v) => { let mut completed_tx_sql = CompletedTransactionSql::try_from(completed_transaction)?; self.encrypt_if_necessary(&mut completed_tx_sql)?; @@ -447,7 +434,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { { let conn = acquire_lock!(self.database_connection); - if CompletedTransactionSql::find(tx_id, false, &(*conn)).is_ok() { + if CompletedTransactionSql::find_by_cancelled(tx_id, false, &(*conn)).is_ok() { return Err(TransactionStorageError::TransactionAlreadyExists); } @@ -471,7 +458,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { fn broadcast_completed_transaction(&self, tx_id: u64) -> Result<(), TransactionStorageError> { let conn = acquire_lock!(self.database_connection); - match CompletedTransactionSql::find(tx_id, false, &(*conn)) { + match CompletedTransactionSql::find_by_cancelled(tx_id, false, &(*conn)) { Ok(v) => { if TransactionStatus::try_from(v.status)? == TransactionStatus::Completed { v.update( @@ -480,6 +467,8 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { timestamp: None, cancelled: None, direction: None, + send_count: None, + last_send_timestamp: None, }), &(*conn), )?; @@ -498,7 +487,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { fn mine_completed_transaction(&self, tx_id: u64) -> Result<(), TransactionStorageError> { let conn = acquire_lock!(self.database_connection); - match CompletedTransactionSql::find(tx_id, false, &(*conn)) { + match CompletedTransactionSql::find_by_cancelled(tx_id, false, &(*conn)) { Ok(v) => { v.update( UpdateCompletedTransactionSql::from(UpdateCompletedTransaction { @@ -506,6 +495,8 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { timestamp: None, cancelled: None, direction: None, + send_count: None, + last_send_timestamp: None, }), &(*conn), )?; @@ -522,7 +513,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { fn cancel_completed_transaction(&self, tx_id: u64) -> Result<(), TransactionStorageError> { let conn = acquire_lock!(self.database_connection); - match CompletedTransactionSql::find(tx_id, false, &(*conn)) { + match CompletedTransactionSql::find_by_cancelled(tx_id, false, &(*conn)) { Ok(v) => { v.cancel(&(*conn))?; }, @@ -543,7 +534,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { v.cancel(&(*conn))?; }, Err(_) => { - match OutboundTransactionSql::find(tx_id, false, &(*conn)) { + match OutboundTransactionSql::find_by_cancelled(tx_id, false, &(*conn)) { Ok(v) => { v.cancel(&(*conn))?; }, @@ -566,18 +557,22 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { cancelled: None, direct_send_success: Some(1i32), receiver_protocol: None, + send_count: None, + last_send_timestamp: None, }, &(*conn), )?; }, Err(_) => { - match OutboundTransactionSql::find(tx_id, false, &(*conn)) { + match OutboundTransactionSql::find_by_cancelled(tx_id, false, &(*conn)) { Ok(v) => { v.update( UpdateOutboundTransactionSql { cancelled: None, direct_send_success: Some(1i32), sender_protocol: None, + send_count: None, + last_send_timestamp: None, }, &(*conn), )?; @@ -601,13 +596,15 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { { let conn = acquire_lock!(self.database_connection); - if let Ok(tx) = CompletedTransactionSql::find(tx_id, false, &(*conn)) { + if let Ok(tx) = CompletedTransactionSql::find_by_cancelled(tx_id, false, &(*conn)) { tx.update( UpdateCompletedTransactionSql::from(UpdateCompletedTransaction { status: None, timestamp: Some(timestamp), cancelled: None, direction: None, + send_count: None, + last_send_timestamp: None, }), &(*conn), )?; @@ -726,6 +723,45 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { } Ok(()) } + + fn increment_send_count(&self, tx_id: u64) -> Result<(), TransactionStorageError> { + let conn = acquire_lock!(self.database_connection); + + if let Ok(tx) = CompletedTransactionSql::find(tx_id, &conn) { + let update = UpdateCompletedTransactionSql { + status: None, + timestamp: None, + cancelled: None, + direction: None, + transaction_protocol: None, + send_count: Some(tx.send_count + 1), + last_send_timestamp: Some(Some(Utc::now().naive_utc())), + }; + tx.update(update, &conn)?; + } else if let Ok(tx) = OutboundTransactionSql::find(tx_id, &conn) { + let update = UpdateOutboundTransactionSql { + cancelled: None, + direct_send_success: None, + sender_protocol: None, + send_count: Some(tx.send_count + 1), + last_send_timestamp: Some(Some(Utc::now().naive_utc())), + }; + tx.update(update, &conn)?; + } else if let Ok(tx) = InboundTransactionSql::find(tx_id, false, &conn) { + let update = UpdateInboundTransactionSql { + cancelled: None, + direct_send_success: None, + receiver_protocol: None, + send_count: Some(tx.send_count + 1), + last_send_timestamp: Some(Some(Utc::now().naive_utc())), + }; + tx.update(update, &conn)?; + } else { + return Err(TransactionStorageError::ValuesNotFound); + } + + Ok(()) + } } #[derive(Clone, Debug, Queryable, Insertable, PartialEq)] @@ -739,6 +775,8 @@ struct InboundTransactionSql { timestamp: NaiveDateTime, cancelled: i32, direct_send_success: i32, + send_count: i32, + last_send_timestamp: Option, } impl InboundTransactionSql { @@ -813,6 +851,8 @@ impl InboundTransactionSql { cancelled: Some(1i32), direct_send_success: None, receiver_protocol: None, + send_count: None, + last_send_timestamp: None, }, conn, ) @@ -824,6 +864,8 @@ impl InboundTransactionSql { cancelled: None, direct_send_success: None, receiver_protocol: Some(self.receiver_protocol.clone()), + send_count: None, + last_send_timestamp: None, }, conn, ) @@ -863,6 +905,8 @@ impl TryFrom for InboundTransactionSql { timestamp: i.timestamp, cancelled: i.cancelled as i32, direct_send_success: i.direct_send_success as i32, + send_count: i.send_count as i32, + last_send_timestamp: i.last_send_timestamp, }) } } @@ -882,6 +926,8 @@ impl TryFrom for InboundTransaction { timestamp: i.timestamp, cancelled: i.cancelled != 0, direct_send_success: i.direct_send_success != 0, + send_count: i.send_count as u32, + last_send_timestamp: i.last_send_timestamp, }) } } @@ -892,6 +938,8 @@ pub struct UpdateInboundTransactionSql { cancelled: Option, direct_send_success: Option, receiver_protocol: Option, + send_count: Option, + last_send_timestamp: Option>, } /// A structure to represent a Sql compatible version of the OutboundTransaction struct @@ -907,6 +955,8 @@ struct OutboundTransactionSql { timestamp: NaiveDateTime, cancelled: i32, direct_send_success: i32, + send_count: i32, + last_send_timestamp: Option, } impl OutboundTransactionSql { @@ -931,7 +981,13 @@ impl OutboundTransactionSql { .load::(conn)?) } - pub fn find( + pub fn find(tx_id: TxId, conn: &SqliteConnection) -> Result { + Ok(outbound_transactions::table + .filter(outbound_transactions::tx_id.eq(tx_id as i64)) + .first::(conn)?) + } + + pub fn find_by_cancelled( tx_id: TxId, cancelled: bool, conn: &SqliteConnection, @@ -981,6 +1037,8 @@ impl OutboundTransactionSql { cancelled: Some(1i32), direct_send_success: None, sender_protocol: None, + send_count: None, + last_send_timestamp: None, }, conn, ) @@ -992,6 +1050,8 @@ impl OutboundTransactionSql { cancelled: None, direct_send_success: None, sender_protocol: Some(self.sender_protocol.clone()), + send_count: None, + last_send_timestamp: None, }, conn, ) @@ -1032,6 +1092,8 @@ impl TryFrom for OutboundTransactionSql { timestamp: o.timestamp, cancelled: o.cancelled as i32, direct_send_success: o.direct_send_success as i32, + send_count: o.send_count as i32, + last_send_timestamp: o.last_send_timestamp, }) } } @@ -1052,6 +1114,8 @@ impl TryFrom for OutboundTransaction { timestamp: o.timestamp, cancelled: o.cancelled != 0, direct_send_success: o.direct_send_success != 0, + send_count: o.send_count as u32, + last_send_timestamp: o.last_send_timestamp, }) } } @@ -1062,6 +1126,8 @@ pub struct UpdateOutboundTransactionSql { cancelled: Option, direct_send_success: Option, sender_protocol: Option, + send_count: Option, + last_send_timestamp: Option>, } /// A structure to represent a Sql compatible version of the CompletedTransaction struct @@ -1080,6 +1146,8 @@ struct CompletedTransactionSql { cancelled: i32, direction: Option, coinbase_block_height: Option, + send_count: i32, + last_send_timestamp: Option, } impl CompletedTransactionSql { @@ -1115,7 +1183,13 @@ impl CompletedTransactionSql { .load::(conn)?) } - pub fn find( + pub fn find(tx_id: TxId, conn: &SqliteConnection) -> Result { + Ok(completed_transactions::table + .filter(completed_transactions::tx_id.eq(tx_id as i64)) + .first::(conn)?) + } + + pub fn find_by_cancelled( tx_id: TxId, cancelled: bool, conn: &SqliteConnection, @@ -1167,6 +1241,8 @@ impl CompletedTransactionSql { cancelled: Some(1i32), direction: None, transaction_protocol: None, + send_count: None, + last_send_timestamp: None, }, conn, )?; @@ -1182,6 +1258,8 @@ impl CompletedTransactionSql { cancelled: None, direction: None, transaction_protocol: Some(self.transaction_protocol.clone()), + send_count: None, + last_send_timestamp: None, }, conn, )?; @@ -1203,6 +1281,8 @@ impl CompletedTransactionSql { timestamp: None, cancelled: None, direction: Some(TransactionDirection::Outbound), + send_count: None, + last_send_timestamp: None, }), conn, )?; @@ -1213,6 +1293,8 @@ impl CompletedTransactionSql { timestamp: None, cancelled: None, direction: Some(TransactionDirection::Inbound), + send_count: None, + last_send_timestamp: None, }), conn, )?; @@ -1259,6 +1341,8 @@ impl TryFrom for CompletedTransactionSql { cancelled: c.cancelled as i32, direction: Some(c.direction as i32), coinbase_block_height: c.coinbase_block_height.map(|b| b as i64), + send_count: c.send_count as i32, + last_send_timestamp: c.last_send_timestamp, }) } } @@ -1282,6 +1366,8 @@ impl TryFrom for CompletedTransaction { cancelled: c.cancelled != 0, direction: TransactionDirection::try_from(c.direction.unwrap_or(2i32))?, coinbase_block_height: c.coinbase_block_height.map(|b| b as u64), + send_count: c.send_count as u32, + last_send_timestamp: c.last_send_timestamp, }) } } @@ -1292,6 +1378,8 @@ pub struct UpdateCompletedTransaction { timestamp: Option, cancelled: Option, direction: Option, + send_count: Option, + last_send_timestamp: Option>, } #[derive(AsChangeset)] @@ -1302,6 +1390,8 @@ pub struct UpdateCompletedTransactionSql { cancelled: Option, direction: Option, transaction_protocol: Option, + send_count: Option, + last_send_timestamp: Option>, } /// Map a Rust friendly UpdateCompletedTransaction to the Sql data type form @@ -1313,6 +1403,8 @@ impl From for UpdateCompletedTransactionSql { cancelled: u.cancelled.map(|c| c as i32), direction: u.direction.map(|d| d as i32), transaction_protocol: None, + send_count: u.send_count.map(|c| c as i32), + last_send_timestamp: u.last_send_timestamp, } } } @@ -1323,12 +1415,11 @@ mod test { use crate::transaction_service::storage::sqlite_db::UpdateCompletedTransactionSql; use crate::{ transaction_service::storage::{ - database::{ + database::{DbKey, TransactionBackend}, + models::{ CompletedTransaction, - DbKey, InboundTransaction, OutboundTransaction, - TransactionBackend, TransactionDirection, TransactionStatus, }, @@ -1408,6 +1499,8 @@ mod test { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; let outbound_tx2 = OutboundTransactionSql::try_from(OutboundTransaction { @@ -1421,6 +1514,8 @@ mod test { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }) .unwrap(); @@ -1435,7 +1530,8 @@ mod test { assert_eq!(outbound_txs.len(), 2); let returned_outbound_tx = - OutboundTransaction::try_from(OutboundTransactionSql::find(1u64, false, &conn).unwrap()).unwrap(); + OutboundTransaction::try_from(OutboundTransactionSql::find_by_cancelled(1u64, false, &conn).unwrap()) + .unwrap(); assert_eq!( OutboundTransactionSql::try_from(returned_outbound_tx).unwrap(), OutboundTransactionSql::try_from(outbound_tx1.clone()).unwrap() @@ -1459,6 +1555,8 @@ mod test { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; let inbound_tx2 = InboundTransaction { tx_id: 3, @@ -1470,6 +1568,8 @@ mod test { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; InboundTransactionSql::try_from(inbound_tx1.clone()) @@ -1506,6 +1606,8 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }; let completed_tx2 = CompletedTransaction { tx_id: 3, @@ -1520,6 +1622,8 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }; CompletedTransactionSql::try_from(completed_tx1.clone()) @@ -1540,7 +1644,8 @@ mod test { assert_eq!(completed_txs.len(), 2); let returned_completed_tx = - CompletedTransaction::try_from(CompletedTransactionSql::find(2u64, false, &conn).unwrap()).unwrap(); + CompletedTransaction::try_from(CompletedTransactionSql::find_by_cancelled(2u64, false, &conn).unwrap()) + .unwrap(); assert_eq!( CompletedTransactionSql::try_from(returned_completed_tx).unwrap(), CompletedTransactionSql::try_from(completed_tx1.clone()).unwrap() @@ -1557,7 +1662,7 @@ mod test { .is_err()); assert!(InboundTransactionSql::find(inbound_tx1.tx_id, false, &conn).is_err()); - assert!(OutboundTransactionSql::find(inbound_tx1.tx_id, false, &conn).is_ok()); + assert!(OutboundTransactionSql::find_by_cancelled(inbound_tx1.tx_id, false, &conn).is_ok()); OutboundTransactionSql::try_from(outbound_tx1.clone()) .unwrap() .delete(&conn) @@ -1566,9 +1671,9 @@ mod test { .unwrap() .delete(&conn) .is_err()); - assert!(OutboundTransactionSql::find(outbound_tx1.tx_id, false, &conn).is_err()); + assert!(OutboundTransactionSql::find_by_cancelled(outbound_tx1.tx_id, false, &conn).is_err()); - assert!(CompletedTransactionSql::find(completed_tx1.tx_id, false, &conn).is_ok()); + assert!(CompletedTransactionSql::find_by_cancelled(completed_tx1.tx_id, false, &conn).is_ok()); CompletedTransactionSql::try_from(completed_tx1.clone()) .unwrap() .delete(&conn) @@ -1577,7 +1682,7 @@ mod test { .unwrap() .delete(&conn) .is_err()); - assert!(CompletedTransactionSql::find(completed_tx1.tx_id, false, &conn).is_err()); + assert!(CompletedTransactionSql::find_by_cancelled(completed_tx1.tx_id, false, &conn).is_err()); InboundTransactionSql::try_from(inbound_tx1.clone()) .unwrap() @@ -1596,7 +1701,7 @@ mod test { .commit(&conn) .unwrap(); - assert!(OutboundTransactionSql::find(outbound_tx1.tx_id, true, &conn).is_err()); + assert!(OutboundTransactionSql::find_by_cancelled(outbound_tx1.tx_id, true, &conn).is_err()); OutboundTransactionSql::try_from(outbound_tx1.clone()) .unwrap() .cancel(&conn) @@ -1609,13 +1714,13 @@ mod test { .commit(&conn) .unwrap(); - assert!(CompletedTransactionSql::find(completed_tx1.tx_id, true, &conn).is_err()); + assert!(CompletedTransactionSql::find_by_cancelled(completed_tx1.tx_id, true, &conn).is_err()); CompletedTransactionSql::try_from(completed_tx1.clone()) .unwrap() .cancel(&conn) .unwrap(); - assert!(CompletedTransactionSql::find(completed_tx1.tx_id, false, &conn).is_err()); - assert!(CompletedTransactionSql::find(completed_tx1.tx_id, true, &conn).is_ok()); + assert!(CompletedTransactionSql::find_by_cancelled(completed_tx1.tx_id, false, &conn).is_err()); + assert!(CompletedTransactionSql::find_by_cancelled(completed_tx1.tx_id, true, &conn).is_ok()); let coinbase_tx1 = CompletedTransaction { tx_id: 101, @@ -1630,6 +1735,8 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: Some(2), + send_count: 0, + last_send_timestamp: None, }; let coinbase_tx2 = CompletedTransaction { @@ -1645,6 +1752,8 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: Some(2), + send_count: 0, + last_send_timestamp: None, }; let coinbase_tx3 = CompletedTransaction { @@ -1660,6 +1769,8 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: Some(3), + send_count: 0, + last_send_timestamp: None, }; CompletedTransactionSql::try_from(coinbase_tx1.clone()) @@ -1683,7 +1794,7 @@ mod test { assert!(coinbase_txs.iter().find(|c| c.tx_id == 103).is_none()); #[cfg(feature = "test_harness")] - CompletedTransactionSql::find(completed_tx2.tx_id, false, &conn) + CompletedTransactionSql::find_by_cancelled(completed_tx2.tx_id, false, &conn) .unwrap() .update( UpdateCompletedTransactionSql { @@ -1692,6 +1803,8 @@ mod test { cancelled: None, direction: None, transaction_protocol: None, + send_count: None, + last_send_timestamp: None, }, &conn, ) @@ -1727,6 +1840,8 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }) .unwrap(); completed_tx1.direction = None; @@ -1745,6 +1860,8 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }) .unwrap(); completed_tx2.direction = None; @@ -1795,6 +1912,8 @@ mod test { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; let mut inbound_tx_sql = InboundTransactionSql::try_from(inbound_tx.clone()).unwrap(); inbound_tx_sql.commit(&conn).unwrap(); @@ -1816,13 +1935,15 @@ mod test { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; let mut outbound_tx_sql = OutboundTransactionSql::try_from(outbound_tx.clone()).unwrap(); outbound_tx_sql.commit(&conn).unwrap(); outbound_tx_sql.encrypt(&cipher).unwrap(); outbound_tx_sql.update_encryption(&conn).unwrap(); - let mut db_outbound_tx = OutboundTransactionSql::find(2, false, &conn).unwrap(); + let mut db_outbound_tx = OutboundTransactionSql::find_by_cancelled(2, false, &conn).unwrap(); db_outbound_tx.decrypt(&cipher).unwrap(); let decrypted_outbound_tx = OutboundTransaction::try_from(db_outbound_tx).unwrap(); assert_eq!(outbound_tx, decrypted_outbound_tx); @@ -1840,13 +1961,15 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }; let mut completed_tx_sql = CompletedTransactionSql::try_from(completed_tx.clone()).unwrap(); completed_tx_sql.commit(&conn).unwrap(); completed_tx_sql.encrypt(&cipher).unwrap(); completed_tx_sql.update_encryption(&conn).unwrap(); - let mut db_completed_tx = CompletedTransactionSql::find(3, false, &conn).unwrap(); + let mut db_completed_tx = CompletedTransactionSql::find_by_cancelled(3, false, &conn).unwrap(); db_completed_tx.decrypt(&cipher).unwrap(); let decrypted_completed_tx = CompletedTransaction::try_from(db_completed_tx).unwrap(); assert_eq!(completed_tx, decrypted_completed_tx); @@ -1874,6 +1997,8 @@ mod test { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; let inbound_tx_sql = InboundTransactionSql::try_from(inbound_tx.clone()).unwrap(); inbound_tx_sql.commit(&conn).unwrap(); @@ -1889,6 +2014,8 @@ mod test { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; let outbound_tx_sql = OutboundTransactionSql::try_from(outbound_tx.clone()).unwrap(); outbound_tx_sql.commit(&conn).unwrap(); @@ -1906,6 +2033,8 @@ mod test { cancelled: false, direction: TransactionDirection::Unknown, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }; let completed_tx_sql = CompletedTransactionSql::try_from(completed_tx.clone()).unwrap(); completed_tx_sql.commit(&conn).unwrap(); diff --git a/base_layer/wallet/src/transaction_service/tasks/mod.rs b/base_layer/wallet/src/transaction_service/tasks/mod.rs new file mode 100644 index 0000000000..65a6c83890 --- /dev/null +++ b/base_layer/wallet/src/transaction_service/tasks/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2020. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod send_finalized_transaction; +pub mod send_transaction_cancelled; +pub mod send_transaction_reply; +pub mod wait_on_dial; diff --git a/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs b/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs new file mode 100644 index 0000000000..7153286c0f --- /dev/null +++ b/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs @@ -0,0 +1,178 @@ +// Copyright 2020. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use crate::{ + output_manager_service::TxId, + transaction_service::{error::TransactionServiceError, tasks::wait_on_dial::wait_on_dial}, +}; +use log::*; +use std::time::Duration; +use tari_comms::{peer_manager::NodeId, types::CommsPublicKey}; +use tari_comms_dht::{ + domain_message::OutboundDomainMessage, + envelope::NodeDestination, + outbound::{OutboundEncryption, OutboundMessageRequester, SendMessageResponse}, +}; +use tari_core::transactions::{transaction::Transaction, transaction_protocol::proto}; +use tari_p2p::tari_message::TariMessageType; + +const LOG_TARGET: &str = "wallet::transaction_service::tasks::send_finalized_transaction"; + +pub async fn send_finalized_transaction_message( + tx_id: TxId, + transaction: Transaction, + destination_public_key: CommsPublicKey, + mut outbound_message_service: OutboundMessageRequester, + direct_send_timeout: Duration, +) -> Result<(), TransactionServiceError> +{ + let finalized_transaction_message = proto::TransactionFinalizedMessage { + tx_id, + transaction: Some(transaction.clone().into()), + }; + let mut store_and_forward_send_result = false; + let mut direct_send_result = false; + match outbound_message_service + .send_direct( + destination_public_key.clone(), + OutboundDomainMessage::new( + TariMessageType::TransactionFinalized, + finalized_transaction_message.clone(), + ), + ) + .await + { + Ok(result) => match result { + SendMessageResponse::Queued(send_states) => { + if wait_on_dial( + send_states, + tx_id, + destination_public_key.clone(), + "Finalized Transaction", + direct_send_timeout, + ) + .await + { + direct_send_result = true; + } else { + store_and_forward_send_result = send_transaction_finalized_message_store_and_forward( + tx_id, + destination_public_key.clone(), + finalized_transaction_message.clone(), + &mut outbound_message_service, + ) + .await?; + } + }, + SendMessageResponse::Failed(err) => { + warn!( + target: LOG_TARGET, + "Finalized Transaction Send Direct for TxID {} failed: {}", tx_id, err + ); + store_and_forward_send_result = send_transaction_finalized_message_store_and_forward( + tx_id, + destination_public_key.clone(), + finalized_transaction_message.clone(), + &mut outbound_message_service, + ) + .await?; + }, + SendMessageResponse::PendingDiscovery(rx) => { + store_and_forward_send_result = send_transaction_finalized_message_store_and_forward( + tx_id, + destination_public_key.clone(), + finalized_transaction_message.clone(), + &mut outbound_message_service, + ) + .await?; + // now wait for discovery to complete + match rx.await { + Ok(send_msg_response) => { + if let SendMessageResponse::Queued(send_states) = send_msg_response { + debug!( + target: LOG_TARGET, + "Discovery of {} completed for TxID: {}", destination_public_key, tx_id + ); + direct_send_result = wait_on_dial( + send_states, + tx_id, + destination_public_key.clone(), + "Finalized Transaction", + direct_send_timeout, + ) + .await; + } + }, + Err(e) => { + warn!( + target: LOG_TARGET, + "Error waiting for Discovery while sending message to TxId: {} {:?}", tx_id, e + ); + }, + } + }, + }, + Err(e) => { + warn!(target: LOG_TARGET, "Direct Finalized Transaction Send failed: {:?}", e); + }, + } + if !direct_send_result && !store_and_forward_send_result { + return Err(TransactionServiceError::OutboundSendFailure); + } + Ok(()) +} + +async fn send_transaction_finalized_message_store_and_forward( + tx_id: TxId, + destination_pubkey: CommsPublicKey, + msg: proto::TransactionFinalizedMessage, + outbound_message_service: &mut OutboundMessageRequester, +) -> Result +{ + match outbound_message_service + .broadcast( + NodeDestination::NodeId(Box::new(NodeId::from_key(&destination_pubkey)?)), + OutboundEncryption::EncryptFor(Box::new(destination_pubkey.clone())), + vec![], + OutboundDomainMessage::new(TariMessageType::TransactionFinalized, msg.clone()), + ) + .await + { + Ok(send_states) => { + info!( + target: LOG_TARGET, + "Sending Finalized Transaction (TxId: {}) to Neighbours for Store and Forward successful with Message \ + Tags: {:?}", + tx_id, + send_states.to_tags(), + ); + }, + Err(e) => { + warn!( + target: LOG_TARGET, + "Sending Finalized Transaction (TxId: {}) to neighbours for Store and Forward failed: {:?}", tx_id, e + ); + return Ok(false); + }, + }; + + Ok(true) +} diff --git a/base_layer/wallet/src/transaction_service/tasks/send_transaction_cancelled.rs b/base_layer/wallet/src/transaction_service/tasks/send_transaction_cancelled.rs new file mode 100644 index 0000000000..8b6338c5fe --- /dev/null +++ b/base_layer/wallet/src/transaction_service/tasks/send_transaction_cancelled.rs @@ -0,0 +1,58 @@ +// Copyright 2020. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use crate::{output_manager_service::TxId, transaction_service::error::TransactionServiceError}; +use tari_comms::{peer_manager::NodeId, types::CommsPublicKey}; +use tari_comms_dht::{ + domain_message::OutboundDomainMessage, + envelope::NodeDestination, + outbound::{OutboundEncryption, OutboundMessageRequester}, +}; +use tari_core::transactions::transaction_protocol::proto; +use tari_p2p::tari_message::TariMessageType; + +pub async fn send_transaction_cancelled_message( + tx_id: TxId, + destination_public_key: CommsPublicKey, + mut outbound_message_service: OutboundMessageRequester, +) -> Result<(), TransactionServiceError> +{ + let proto_message = proto::TransactionCancelledMessage { tx_id }; + + // Send both direct and SAF we are not going to monitor the progress on these messages for potential resend as + // they are just courtesy messages + let _ = outbound_message_service + .send_direct( + destination_public_key.clone(), + OutboundDomainMessage::new(TariMessageType::TransactionCancelled, proto_message.clone()), + ) + .await?; + + let _ = outbound_message_service + .broadcast( + NodeDestination::NodeId(Box::new(NodeId::from_key(&destination_public_key)?)), + OutboundEncryption::EncryptFor(Box::new(destination_public_key.clone())), + vec![], + OutboundDomainMessage::new(TariMessageType::SenderPartialTransaction, proto_message), + ) + .await?; + Ok(()) +} diff --git a/base_layer/wallet/src/transaction_service/tasks/send_transaction_reply.rs b/base_layer/wallet/src/transaction_service/tasks/send_transaction_reply.rs new file mode 100644 index 0000000000..9dccbd22bf --- /dev/null +++ b/base_layer/wallet/src/transaction_service/tasks/send_transaction_reply.rs @@ -0,0 +1,176 @@ +// Copyright 2020. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::output_manager_service::TxId; +use log::*; +use tari_comms_dht::{domain_message::OutboundDomainMessage, outbound::SendMessageResponse}; +use tari_p2p::tari_message::TariMessageType; + +use crate::transaction_service::{ + error::TransactionServiceError, + storage::models::InboundTransaction, + tasks::wait_on_dial::wait_on_dial, +}; +use std::time::Duration; +use tari_comms::{peer_manager::NodeId, types::CommsPublicKey}; +use tari_comms_dht::{ + envelope::NodeDestination, + outbound::{OutboundEncryption, OutboundMessageRequester}, +}; +use tari_core::transactions::transaction_protocol::proto; + +const LOG_TARGET: &str = "wallet::transaction_service::tasks::send_transaction_reply"; + +/// A task to resend a transaction reply message if a repeated Send Transaction is received from a Sender +pub async fn send_transaction_reply( + inbound_transaction: InboundTransaction, + mut outbound_message_service: OutboundMessageRequester, + direct_send_timeout: Duration, +) -> Result +{ + let recipient_reply = inbound_transaction.receiver_protocol.get_signed_data()?.clone(); + + let mut store_and_forward_send_result = false; + let mut direct_send_result = false; + + let tx_id = inbound_transaction.tx_id; + let proto_message: proto::RecipientSignedMessage = recipient_reply.into(); + match outbound_message_service + .send_direct( + inbound_transaction.source_public_key.clone(), + OutboundDomainMessage::new(TariMessageType::ReceiverPartialTransactionReply, proto_message.clone()), + ) + .await + { + Ok(result) => match result { + SendMessageResponse::Queued(send_states) => { + if wait_on_dial( + send_states, + tx_id, + inbound_transaction.source_public_key.clone(), + "Transaction Reply", + direct_send_timeout, + ) + .await + { + direct_send_result = true; + } else { + store_and_forward_send_result = send_transaction_reply_store_and_forward( + tx_id, + inbound_transaction.source_public_key.clone(), + proto_message.clone(), + &mut outbound_message_service, + ) + .await?; + } + }, + SendMessageResponse::Failed(err) => { + warn!( + target: LOG_TARGET, + "Transaction Reply Send Direct for TxID {} failed: {}", tx_id, err + ); + store_and_forward_send_result = send_transaction_reply_store_and_forward( + tx_id, + inbound_transaction.source_public_key.clone(), + proto_message.clone(), + &mut outbound_message_service, + ) + .await?; + }, + SendMessageResponse::PendingDiscovery(rx) => { + store_and_forward_send_result = send_transaction_reply_store_and_forward( + tx_id, + inbound_transaction.source_public_key.clone(), + proto_message.clone(), + &mut outbound_message_service, + ) + .await?; + // now wait for discovery to complete + match rx.await { + Ok(send_msg_response) => { + if let SendMessageResponse::Queued(send_states) = send_msg_response { + debug!( + target: LOG_TARGET, + "Discovery of {} completed for TxID: {}", inbound_transaction.source_public_key, tx_id + ); + direct_send_result = wait_on_dial( + send_states, + tx_id, + inbound_transaction.source_public_key.clone(), + "Transaction Reply", + direct_send_timeout, + ) + .await; + } + }, + Err(e) => { + debug!( + target: LOG_TARGET, + "Error waiting for Discovery while sending message to TxId: {} {:?}", tx_id, e + ); + }, + } + }, + }, + Err(e) => { + warn!(target: LOG_TARGET, "Direct Transaction Reply Send failed: {:?}", e); + }, + } + Ok(direct_send_result || store_and_forward_send_result) +} + +async fn send_transaction_reply_store_and_forward( + tx_id: TxId, + destination_pubkey: CommsPublicKey, + msg: proto::RecipientSignedMessage, + outbound_message_service: &mut OutboundMessageRequester, +) -> Result +{ + match outbound_message_service + .broadcast( + NodeDestination::NodeId(Box::new(NodeId::from_key(&destination_pubkey)?)), + OutboundEncryption::EncryptFor(Box::new(destination_pubkey.clone())), + vec![], + OutboundDomainMessage::new(TariMessageType::ReceiverPartialTransactionReply, msg), + ) + .await + { + Ok(send_states) => { + info!( + target: LOG_TARGET, + "Sending Transaction Reply (TxId: {}) to Neighbours for Store and Forward successful with Message \ + Tags: {:?}", + tx_id, + send_states.to_tags(), + ); + }, + Err(e) => { + warn!( + target: LOG_TARGET, + "Sending Transaction Reply (TxId: {}) to neighbours for Store and Forward failed: {:?}", tx_id, e, + ); + return Ok(false); + }, + }; + + Ok(true) +} diff --git a/base_layer/wallet/src/transaction_service/tasks/wait_on_dial.rs b/base_layer/wallet/src/transaction_service/tasks/wait_on_dial.rs new file mode 100644 index 0000000000..bce5a6f64e --- /dev/null +++ b/base_layer/wallet/src/transaction_service/tasks/wait_on_dial.rs @@ -0,0 +1,77 @@ +// Copyright 2020. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::output_manager_service::TxId; +use log::*; +use std::time::Duration; +use tari_comms::types::CommsPublicKey; +use tari_comms_dht::outbound::MessageSendStates; + +const LOG_TARGET: &str = "wallet::transaction_service::tasks"; + +/// This function contains the logic to wait on a dial and send of a queued message +pub async fn wait_on_dial( + send_states: MessageSendStates, + tx_id: TxId, + destination_pubkey: CommsPublicKey, + message: &str, + direct_send_timeout: Duration, +) -> bool +{ + if send_states.len() == 1 { + debug!( + target: LOG_TARGET, + "{} (TxId: {}) Direct Send to {} queued with Message {}", + message, + tx_id, + destination_pubkey, + send_states[0].tag, + ); + let (sent, failed) = send_states.wait_n_timeout(direct_send_timeout, 1).await; + if !sent.is_empty() { + info!( + target: LOG_TARGET, + "Direct Send process for {} TX_ID: {} was successful with Message: {}", message, tx_id, sent[0] + ); + true + } else { + if failed.is_empty() { + warn!( + target: LOG_TARGET, + "Direct Send process for {} TX_ID: {} timed out", message, tx_id + ); + } else { + warn!( + target: LOG_TARGET, + "Direct Send process for {} TX_ID: {} and Message {} was unsuccessful and no message was sent", + message, + tx_id, + failed[0] + ); + } + false + } + } else { + warn!(target: LOG_TARGET, "{} Send Direct for TxID: {} failed", message, tx_id); + false + } +} diff --git a/base_layer/wallet/tests/transaction_service/service.rs b/base_layer/wallet/tests/transaction_service/service.rs index 3494ef6a44..e77af46975 100644 --- a/base_layer/wallet/tests/transaction_service/service.rs +++ b/base_layer/wallet/tests/transaction_service/service.rs @@ -24,7 +24,7 @@ use crate::support::{ comms_and_services::{create_dummy_message, get_next_memory_address, setup_comms_services}, utils::{make_input, random_string, TestParams}, }; -use chrono::Utc; +use chrono::{Duration as ChronoDuration, Utc}; use futures::{ channel::{mpsc, mpsc::Sender}, stream, @@ -94,7 +94,10 @@ use tari_wallet::{ config::OutputManagerServiceConfig, handle::OutputManagerHandle, service::OutputManagerService, - storage::{database::OutputManagerDatabase, memory_db::OutputManagerMemoryDatabase}, + storage::{ + database::{OutputManagerBackend, OutputManagerDatabase}, + memory_db::OutputManagerMemoryDatabase, + }, OutputManagerServiceInitializer, }, storage::sqlite_utilities::run_migration_and_create_sqlite_connection, @@ -103,18 +106,15 @@ use tari_wallet::{ handle::{TransactionEvent, TransactionServiceHandle}, service::TransactionService, storage::{ - database::{ + database::{DbKeyValuePair, TransactionBackend, TransactionDatabase, WriteOperation}, + memory_db::TransactionMemoryDatabase, + models::{ CompletedTransaction, - DbKeyValuePair, InboundTransaction, OutboundTransaction, - TransactionBackend, - TransactionDatabase, TransactionDirection, TransactionStatus, - WriteOperation, }, - memory_db::TransactionMemoryDatabase, sqlite_db::TransactionServiceSqliteDatabase, }, TransactionServiceInitializer, @@ -195,8 +195,38 @@ pub fn setup_transaction_service( runtime: &mut Runtime, factories: CryptoFactories, - backend: T, - mined_request_timeout: Option, + tx_backend: T, + config: Option, +) -> ( + TransactionServiceHandle, + OutputManagerHandle, + OutboundServiceMockState, + Sender>, + Sender>, + Sender>, + Sender>, + Sender>, + Sender>, +) +{ + setup_transaction_service_no_comms_and_oms_backend( + runtime, + factories, + tx_backend, + OutputManagerMemoryDatabase::new(), + config, + ) +} + +pub fn setup_transaction_service_no_comms_and_oms_backend< + T: TransactionBackend + Clone + 'static, + S: OutputManagerBackend + Clone + 'static, +>( + runtime: &mut Runtime, + factories: CryptoFactories, + tx_backend: T, + oms_backend: S, + config: Option, ) -> ( TransactionServiceHandle, OutputManagerHandle, @@ -206,6 +236,7 @@ pub fn setup_transaction_service_no_comms>, Sender>, Sender>, + Sender>, ) { let (oms_request_sender, oms_request_receiver) = reply_channel::unbounded(); @@ -221,6 +252,7 @@ pub fn setup_transaction_service_no_comms) -> Option { + let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); + let tx_sender_msg = match envelope_body.decode_part::(1) { + Err(_) => return None, + Ok(d) => match d { + None => return None, + Some(r) => r, + }, + }; + + match TransactionSenderMessage::try_from(tx_sender_msg) { + Ok(msr) => Some(msr), + Err(_) => None, + } +} + +// These are helpers functions to attempt to decode the various types of comms messages when using the Mock outbound +// service +fn try_decode_transaction_reply_message(bytes: Vec) -> Option { + let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); + let tx_reply_msg = match envelope_body.decode_part::(1) { + Err(_) => return None, + Ok(d) => match d { + None => return None, + Some(r) => r, + }, + }; + + match RecipientSignedMessage::try_from(tx_reply_msg) { + Ok(msr) => Some(msr), + Err(_) => None, + } +} + +fn try_decode_finalized_transaction_message(bytes: Vec) -> Option { + let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); + match envelope_body.decode_part::(1) { + Err(_) => None, + Ok(d) => d, + } +} + +fn try_decode_transaction_cancelled_message(bytes: Vec) -> Option { + let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); + match envelope_body.decode_part::(1) { + Err(_) => None, + Ok(d) => d, + } +} + +fn try_decode_mempool_request(bytes: Vec) -> Option { + let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); + let msr = match envelope_body.decode_part::(1) { + Err(_) => return None, + Ok(d) => match d { + None => return None, + Some(r) => r, + }, + }; + + match MempoolServiceRequest::try_from(msr) { + Ok(msr) => Some(msr), + Err(_) => None, + } +} + +fn try_decode_base_node_request(bytes: Vec) -> Option { + let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); + match envelope_body.decode_part::(1) { + Err(_) => return None, + Ok(d) => match d { + None => return None, + Some(r) => return Some(r), + }, + }; +} + fn manage_single_transaction( alice_backend: T, bob_backend: T, @@ -753,6 +869,7 @@ fn test_accepting_unknown_tx_id_and_malformed_reply(al mut alice_tx_finalized, _, _, + _, ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), alice_backend, None); let mut alice_event_stream = alice_ts.get_event_stream_fused(); let bob_node_identity = NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); - let (_bob_ts, mut bob_output_manager, _bob_outbound_service, _bob_tx_sender, _bob_tx_ack_sender, _, _, _) = + let (_bob_ts, mut bob_output_manager, _bob_outbound_service, _bob_tx_sender, _bob_tx_ack_sender, _, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), bob_backend, None); let (_utxo, uo) = make_input(&mut OsRng, MicroTari(250000), &factories.commitment); @@ -973,12 +1091,13 @@ fn finalize_tx_with_missing_output(alic mut alice_tx_finalized, _, _, + _, ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), alice_backend, None); let mut alice_event_stream = alice_ts.get_event_stream_fused(); let bob_node_identity = NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); - let (_bob_ts, mut bob_output_manager, _bob_outbound_service, _bob_tx_sender, _bob_tx_ack_sender, _, _, _) = + let (_bob_ts, mut bob_output_manager, _bob_outbound_service, _bob_tx_sender, _bob_tx_ack_sender, _, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), bob_backend, None); let (_utxo, uo) = make_input(&mut OsRng, MicroTari(250000), &factories.commitment); @@ -1253,6 +1372,7 @@ fn transaction_mempool_broadcast() { _, mut alice_mempool_response_sender, mut alice_base_node_response_sender, + _, ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), TransactionMemoryDatabase::new(), None); let mut alice_event_stream = alice_ts.get_event_stream_fused(); @@ -1260,7 +1380,7 @@ fn transaction_mempool_broadcast() { .block_on(alice_ts.set_base_node_public_key(base_node_identity.public_key().clone())) .unwrap(); - let (_bob_ts, _bob_output_manager, bob_outbound_service, mut bob_tx_sender, _, _, _, _) = + let (_bob_ts, _bob_output_manager, bob_outbound_service, mut bob_tx_sender, _, _, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), TransactionMemoryDatabase::new(), None); let (_utxo, uo) = make_input(&mut OsRng, MicroTari(250000), &factories.commitment); @@ -1581,49 +1701,6 @@ fn transaction_mempool_broadcast() { assert_eq!(alice_completed_tx.status, TransactionStatus::Mined); } -fn try_decode_mempool_request(bytes: Vec) -> Option { - let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); - let msr = match envelope_body.decode_part::(1) { - Err(_) => return None, - Ok(d) => match d { - None => return None, - Some(r) => r, - }, - }; - - match MempoolServiceRequest::try_from(msr) { - Ok(msr) => Some(msr), - Err(_) => None, - } -} - -fn try_decode_sender_message(bytes: Vec) -> Option { - let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); - let tx_sender_msg = match envelope_body.decode_part::(1) { - Err(_) => return None, - Ok(d) => match d { - None => return None, - Some(r) => r, - }, - }; - - match TransactionSenderMessage::try_from(tx_sender_msg) { - Ok(msr) => Some(msr), - Err(_) => None, - } -} - -fn try_decode_base_node_request(bytes: Vec) -> Option { - let envelope_body = EnvelopeBody::decode(&mut bytes.as_slice()).unwrap(); - match envelope_body.decode_part::(1) { - Err(_) => return None, - Ok(d) => match d { - None => return None, - Some(r) => return Some(r), - }, - }; -} - #[test] fn test_power_mode_updates() { let factories = CryptoFactories::default(); @@ -1648,6 +1725,8 @@ fn test_power_mode_updates() { cancelled: false, direction: TransactionDirection::Outbound, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }; let completed_tx2 = CompletedTransaction { @@ -1663,6 +1742,8 @@ fn test_power_mode_updates() { cancelled: false, direction: TransactionDirection::Outbound, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }; backend @@ -1681,7 +1762,7 @@ fn test_power_mode_updates() { let base_node_identity = NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); - let (mut alice_ts, _, alice_outbound_service, _, _, _, _, _) = + let (mut alice_ts, _, alice_outbound_service, _, _, _, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), backend, None); runtime @@ -1733,6 +1814,8 @@ fn broadcast_all_completed_transactions_on_startup() { cancelled: false, direction: TransactionDirection::Outbound, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }; let completed_tx2 = CompletedTransaction { @@ -1765,7 +1848,7 @@ fn broadcast_all_completed_transactions_on_startup() { ))) .unwrap(); - let (mut alice_ts, _, _, _, _, _, _, _) = + let (mut alice_ts, _, _, _, _, _, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), db, None); runtime @@ -1826,11 +1909,12 @@ fn transaction_base_node_monitoring() { _, mut alice_mempool_response_sender, mut alice_base_node_response_sender, + _, ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), TransactionMemoryDatabase::new(), None); let mut alice_event_stream = alice_ts.get_event_stream_fused(); - let (_, _, bob_outbound_service, mut bob_tx_sender, _, _, _, _) = + let (_, _, bob_outbound_service, mut bob_tx_sender, _, _, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), TransactionMemoryDatabase::new(), None); runtime.block_on(alice_ts.set_low_power_mode()).unwrap(); @@ -2253,6 +2337,8 @@ fn query_all_completed_transactions_on_startup() { cancelled: false, direction: TransactionDirection::Outbound, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }; let completed_tx2 = CompletedTransaction { @@ -2285,7 +2371,7 @@ fn query_all_completed_transactions_on_startup() { ))) .unwrap(); - let (mut alice_ts, _, _, _, _, _, _, _) = + let (mut alice_ts, _, _, _, _, _, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), db, None); let mut alice_event_stream = alice_ts.get_event_stream_fused(); @@ -2344,19 +2430,21 @@ fn transaction_cancellation_when_not_in_mempool() { _, mut alice_mempool_response_sender, mut alice_base_node_response_sender, - ) = setup_transaction_service_no_comms( - &mut runtime, - factories.clone(), - TransactionMemoryDatabase::new(), - Some(Duration::from_secs(5)), - ); + _, + ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), TransactionMemoryDatabase::new(), None); + let mut alice_event_stream = alice_ts.get_event_stream_fused(); - let (mut bob_ts, _, bob_outbound_service, mut bob_tx_sender, _, _, _, _) = setup_transaction_service_no_comms( + let (mut bob_ts, _, bob_outbound_service, mut bob_tx_sender, _, _, _, _, _) = setup_transaction_service_no_comms( &mut runtime, factories.clone(), TransactionMemoryDatabase::new(), - Some(Duration::from_secs(20)), + Some(TransactionServiceConfig { + broadcast_monitoring_timeout: Duration::from_secs(20), + chain_monitoring_timeout: Duration::from_secs(20), + ..Default::default() + }), ); + runtime .block_on(bob_ts.set_base_node_public_key(base_node_identity.public_key().clone())) .unwrap(); @@ -2589,8 +2677,26 @@ fn test_transaction_cancellation(backen let bob_node_identity = NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); - let (mut alice_ts, mut alice_output_manager, _alice_outbound_service, mut alice_tx_sender, _, _, _, _) = - setup_transaction_service_no_comms(&mut runtime, factories.clone(), backend, Some(Duration::from_secs(20))); + let ( + mut alice_ts, + mut alice_output_manager, + alice_outbound_service, + mut alice_tx_sender, + _, + _, + _, + _, + mut alice_tx_cancelled_sender, + ) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + backend, + Some(TransactionServiceConfig { + broadcast_monitoring_timeout: Duration::from_secs(20), + chain_monitoring_timeout: Duration::from_secs(20), + ..Default::default() + }), + ); let mut alice_event_stream = alice_ts.get_event_stream_fused(); let alice_total_available = 250000 * uT; @@ -2639,8 +2745,23 @@ fn test_transaction_cancellation(backen } } + let _ = alice_outbound_service.take_calls(); + runtime.block_on(alice_ts.cancel_transaction(tx_id)).unwrap(); + // We expect 1 sent direct and via SAF + alice_outbound_service + .wait_call_count(2, Duration::from_secs(30)) + .expect("alice call wait 1"); + + let call = alice_outbound_service.pop_call().unwrap(); + let alice_cancel_message = try_decode_transaction_cancelled_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(alice_cancel_message.tx_id, tx_id, "DIRECT"); + + let call = alice_outbound_service.pop_call().unwrap(); + let alice_cancel_message = try_decode_transaction_cancelled_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(alice_cancel_message.tx_id, tx_id, "SAF"); + assert!(runtime .block_on(alice_ts.get_pending_outbound_transactions()) .unwrap() @@ -2668,10 +2789,7 @@ fn test_transaction_cancellation(backen let tx_id2 = tx_sender_msg.tx_id; let proto_message = proto::TransactionSenderMessage::single(tx_sender_msg.into()); runtime - .block_on(alice_tx_sender.send(create_dummy_message( - proto_message, - &PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), - ))) + .block_on(alice_tx_sender.send(create_dummy_message(proto_message, &bob_node_identity.public_key()))) .unwrap(); runtime.block_on(async { @@ -2703,6 +2821,100 @@ fn test_transaction_cancellation(backen .unwrap() .remove(&tx_id2) .is_none()); + + // Lets cancel the last one using a Comms stack message + let mut builder = SenderTransactionProtocol::builder(1); + let amount = MicroTari::from(10_000); + let input = UnblindedOutput::new(MicroTari::from(100_000), PrivateKey::random(&mut OsRng), None); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari::from(177)) + .with_offset(PrivateKey::random(&mut OsRng)) + .with_private_nonce(PrivateKey::random(&mut OsRng)) + .with_amount(0, amount) + .with_message("Yo!".to_string()) + .with_input( + input.as_transaction_input(&factories.commitment, OutputFeatures::default()), + input.clone(), + ) + .with_change_secret(PrivateKey::random(&mut OsRng)); + + let mut stp = builder.build::(&factories).unwrap(); + let tx_sender_msg = stp.build_single_round_message().unwrap(); + let tx_id3 = tx_sender_msg.tx_id; + let proto_message = proto::TransactionSenderMessage::single(tx_sender_msg.into()); + runtime + .block_on(alice_tx_sender.send(create_dummy_message(proto_message, &bob_node_identity.public_key()))) + .unwrap(); + + runtime.block_on(async { + let mut delay = delay_for(Duration::from_secs(60)).fuse(); + loop { + futures::select! { + event = alice_event_stream.select_next_some() => { + if let TransactionEvent::ReceivedTransaction(_) = &*event.unwrap() { + break; + } + }, + () = delay => { + break; + }, + } + } + }); + + runtime + .block_on(alice_ts.get_pending_inbound_transactions()) + .unwrap() + .remove(&tx_id3) + .expect("Pending Transaction 3 should be in list"); + + let proto_message = proto::TransactionCancelledMessage { tx_id: tx_id3 }; + // Sent from the wrong source address so should not cancel + runtime + .block_on(alice_tx_cancelled_sender.send(create_dummy_message( + proto_message, + &PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + ))) + .unwrap(); + + runtime.block_on(async { delay_for(Duration::from_secs(5)).await }); + + runtime + .block_on(alice_ts.get_pending_inbound_transactions()) + .unwrap() + .remove(&tx_id3) + .expect("Pending Transaction 3 should be in list"); + + let proto_message = proto::TransactionCancelledMessage { tx_id: tx_id3 }; + runtime + .block_on(alice_tx_cancelled_sender.send(create_dummy_message(proto_message, &bob_node_identity.public_key()))) + .unwrap(); + + runtime.block_on(async { + let mut delay = delay_for(Duration::from_secs(30)).fuse(); + let mut cancelled = false; + loop { + futures::select! { + event = alice_event_stream.select_next_some() => { + if let TransactionEvent::TransactionCancelled(_) = &*event.unwrap() { + cancelled = true; + break; + } + }, + () = delay => { + break; + }, + } + } + assert!(cancelled, "Should received cancelled event"); + }); + + assert!(runtime + .block_on(alice_ts.get_pending_inbound_transactions()) + .unwrap() + .remove(&tx_id3) + .is_none()); } #[test] @@ -2740,12 +2952,8 @@ fn test_direct_vs_saf_send_of_tx_reply_and_finalize() { _, _, _, - ) = setup_transaction_service_no_comms( - &mut runtime, - factories.clone(), - TransactionMemoryDatabase::new(), - Some(Duration::from_secs(5)), - ); + _, + ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), TransactionMemoryDatabase::new(), None); let alice_total_available = 250000 * uT; let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); @@ -2785,11 +2993,15 @@ fn test_direct_vs_saf_send_of_tx_reply_and_finalize() { assert_eq!(tx_id, msg_tx_id); // Test sending the Reply to a receiver with Direct and then with SAF and never both - let (_bob_ts, _, bob_outbound_service, mut bob_tx_sender, _, _, _, _) = setup_transaction_service_no_comms( + let (_bob_ts, _, bob_outbound_service, mut bob_tx_sender, _, _, _, _, _) = setup_transaction_service_no_comms( &mut runtime, factories.clone(), TransactionMemoryDatabase::new(), - Some(Duration::from_secs(20)), + Some(TransactionServiceConfig { + broadcast_monitoring_timeout: Duration::from_secs(20), + chain_monitoring_timeout: Duration::from_secs(20), + ..Default::default() + }), ); bob_outbound_service.set_behaviour(MockBehaviour { @@ -2819,11 +3031,15 @@ fn test_direct_vs_saf_send_of_tx_reply_and_finalize() { runtime.block_on(async { delay_for(Duration::from_secs(5)).await }); assert_eq!(bob_outbound_service.call_count(), 0, "Should be no more calls"); - let (_bob2_ts, _, bob2_outbound_service, mut bob2_tx_sender, _, _, _, _) = setup_transaction_service_no_comms( + let (_bob2_ts, _, bob2_outbound_service, mut bob2_tx_sender, _, _, _, _, _) = setup_transaction_service_no_comms( &mut runtime, factories.clone(), TransactionMemoryDatabase::new(), - Some(Duration::from_secs(20)), + Some(TransactionServiceConfig { + broadcast_monitoring_timeout: Duration::from_secs(20), + chain_monitoring_timeout: Duration::from_secs(20), + ..Default::default() + }), ); bob2_outbound_service.set_behaviour(MockBehaviour { direct: ResponseType::Failed, @@ -2959,12 +3175,8 @@ fn test_tx_direct_send_behaviour() { _, _, _, - ) = setup_transaction_service_no_comms( - &mut runtime, - factories.clone(), - TransactionMemoryDatabase::new(), - Some(Duration::from_secs(5)), - ); + _, + ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), TransactionMemoryDatabase::new(), None); let mut alice_event_stream = alice_ts.get_event_stream_fused(); let (_utxo, uo) = make_input(&mut OsRng, 1000000 * uT, &factories.commitment); @@ -3217,6 +3429,8 @@ fn test_restarting_transaction_protocols() { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; alice_backend @@ -3237,6 +3451,8 @@ fn test_restarting_transaction_protocols() { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }; bob_backend .write(WriteOperation::Insert(DbKeyValuePair::PendingOutboundTransaction( @@ -3246,7 +3462,7 @@ fn test_restarting_transaction_protocols() { .unwrap(); // Test that Bob's node restarts the send protocol - let (mut bob_ts, _bob_oms, _bob_outbound_service, _, mut bob_tx_reply, _, _, _) = + let (mut bob_ts, _bob_oms, _bob_outbound_service, _, mut bob_tx_reply, _, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), bob_backend, None); let mut bob_event_stream = bob_ts.get_event_stream_fused(); @@ -3279,7 +3495,7 @@ fn test_restarting_transaction_protocols() { }); // Test Alice's node restarts the receive protocol - let (mut alice_ts, _alice_oms, _alice_outbound_service, _, _, mut alice_tx_finalized, _, _) = + let (mut alice_ts, _alice_oms, _alice_outbound_service, _, _, mut alice_tx_finalized, _, _, _) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), alice_backend, None); let mut alice_event_stream = alice_ts.get_event_stream_fused(); @@ -3337,12 +3553,8 @@ fn test_handling_coinbase_transactions() { _, _, mut alice_base_node_response_sender, - ) = setup_transaction_service_no_comms( - &mut runtime, - factories.clone(), - TransactionMemoryDatabase::new(), - Some(Duration::from_secs(5)), - ); + _, + ) = setup_transaction_service_no_comms(&mut runtime, factories.clone(), TransactionMemoryDatabase::new(), None); let mut alice_event_stream = alice_ts.get_event_stream_fused(); let blockheight1 = 10; @@ -3652,3 +3864,735 @@ fn test_handling_coinbase_transactions() { assert_eq!(fetch_count, 2); assert_eq!(metadata_count, 2); } + +#[test] +fn test_transaction_resending() { + let factories = CryptoFactories::default(); + let mut runtime = Runtime::new().unwrap(); + + let alice_node_identity = + NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); + let bob_node_identity = + NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); + // Setup Alice wallet with no comms stack + let alice_db_name = format!("{}.sqlite3", random_string(8).as_str()); + let alice_temp_dir = tempdir().unwrap(); + let alice_db_folder = alice_temp_dir.path().to_str().unwrap().to_string(); + let alice_connection = + run_migration_and_create_sqlite_connection(&format!("{}/{}", alice_db_folder, alice_db_name)).unwrap(); + + let ( + mut alice_ts, + mut alice_output_manager, + alice_outbound_service, + _alice_tx_sender, + mut alice_tx_reply_sender, + _, + _, + _, + _, + ) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + TransactionServiceSqliteDatabase::new(alice_connection, None), + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + ..Default::default() + }), + ); + + // Send a transaction to Bob + let alice_total_available = 250000 * uT; + let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); + runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + + let amount_sent = 10000 * uT; + + let tx_id = runtime + .block_on(alice_ts.send_transaction( + bob_node_identity.public_key().clone(), + amount_sent, + 100 * uT, + "Testing Message".to_string(), + )) + .unwrap(); + + // Check that there were repeats + alice_outbound_service + .wait_call_count(2, Duration::from_secs(30)) + .expect("Alice call wait 1"); + + let mut alice_sender_message = TransactionSenderMessage::None; + for _ in 0..2 { + let call = alice_outbound_service.pop_call().unwrap(); + alice_sender_message = try_decode_sender_message(call.1.to_vec().clone()).unwrap(); + if let TransactionSenderMessage::Single(data) = alice_sender_message.clone() { + assert_eq!(data.tx_id, tx_id); + } else { + assert!(false, "Should be a Single Transaction Sender Message") + } + } + + // Setup Bob's wallet with no comms stack + let bob_db_name = format!("{}.sqlite3", random_string(8).as_str()); + let bob_temp_dir = tempdir().unwrap(); + let bob_db_folder = bob_temp_dir.path().to_str().unwrap().to_string(); + let bob_connection = + run_migration_and_create_sqlite_connection(&format!("{}/{}", bob_db_folder, bob_db_name)).unwrap(); + + let (_bob_ts, _bob_output_manager, bob_outbound_service, mut bob_tx_sender, mut _bob_tx_reply_sender, _, _, _, _) = + setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + TransactionServiceSqliteDatabase::new(bob_connection, None), + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + ..Default::default() + }), + ); + + // Pass sender message to Bob's wallet + runtime + .block_on(bob_tx_sender.send(create_dummy_message( + alice_sender_message.clone().into(), + alice_node_identity.public_key(), + ))) + .unwrap(); + + // Check that the reply was repeated + bob_outbound_service + .wait_call_count(2, Duration::from_secs(30)) + .expect("Bob call wait 1"); + + let mut bob_reply_message; + for _ in 0..2 { + let call = bob_outbound_service.pop_call().unwrap(); + bob_reply_message = try_decode_transaction_reply_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(bob_reply_message.tx_id, tx_id); + } + + // See if sending a second message too soon is ignored + runtime + .block_on(bob_tx_sender.send(create_dummy_message( + alice_sender_message.clone().into(), + alice_node_identity.public_key(), + ))) + .unwrap(); + + assert!(bob_outbound_service.wait_call_count(1, Duration::from_secs(4)).is_err()); + + // Wait for the cooldown to expire but before the resend period has elapsed see if a repeat illicts a reponse. + runtime.block_on(async { delay_for(Duration::from_secs(2)).await }); + runtime + .block_on(bob_tx_sender.send(create_dummy_message( + alice_sender_message.into(), + alice_node_identity.public_key(), + ))) + .unwrap(); + bob_outbound_service + .wait_call_count(1, Duration::from_secs(30)) + .expect("Bob call wait 2"); + let call = bob_outbound_service.pop_call().unwrap(); + bob_reply_message = try_decode_transaction_reply_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(bob_reply_message.tx_id, tx_id); + + let _ = alice_outbound_service.take_calls(); + + // Send the reply to Alice + runtime + .block_on(alice_tx_reply_sender.send(create_dummy_message( + bob_reply_message.clone().into(), + bob_node_identity.public_key(), + ))) + .unwrap(); + + alice_outbound_service + .wait_call_count(1, Duration::from_secs(30)) + .expect("Alice call wait 2"); + + let call = alice_outbound_service.pop_call().unwrap(); + let alice_finalize_message = try_decode_finalized_transaction_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(alice_finalize_message.tx_id, tx_id); + + // See if sending a second message before cooldown and see if it is ignored + runtime + .block_on(alice_tx_reply_sender.send(create_dummy_message( + bob_reply_message.clone().into(), + bob_node_identity.public_key(), + ))) + .unwrap(); + + assert!(alice_outbound_service + .wait_call_count(1, Duration::from_secs(4)) + .is_err()); + + // Wait for the cooldown to expire but before the resend period has elapsed see if a repeat illicts a reponse. + runtime.block_on(async { delay_for(Duration::from_secs(2)).await }); + + runtime + .block_on(alice_tx_reply_sender.send(create_dummy_message( + bob_reply_message.clone().into(), + bob_node_identity.public_key(), + ))) + .unwrap(); + + alice_outbound_service + .wait_call_count(1, Duration::from_secs(30)) + .expect("Alice call wait 3"); + + let call = alice_outbound_service.pop_call().unwrap(); + let alice_finalize_message = try_decode_finalized_transaction_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(alice_finalize_message.tx_id, tx_id); +} + +#[test] +fn test_resend_on_startup() { + // Test that messages are resent on startup if enough time has passed + let factories = CryptoFactories::default(); + let mut runtime = Runtime::new().unwrap(); + + let alice_node_identity = + NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); + + // First we will check the Send Transction message + let mut builder = SenderTransactionProtocol::builder(1); + let amount = MicroTari::from(10_000); + let input = UnblindedOutput::new(MicroTari::from(100_000), PrivateKey::random(&mut OsRng), None); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari::from(177)) + .with_offset(PrivateKey::random(&mut OsRng)) + .with_private_nonce(PrivateKey::random(&mut OsRng)) + .with_amount(0, amount) + .with_message("Yo!".to_string()) + .with_input( + input.as_transaction_input(&factories.commitment, OutputFeatures::default()), + input.clone(), + ) + .with_change_secret(PrivateKey::random(&mut OsRng)); + + let mut stp = builder.build::(&factories).unwrap(); + let stp_msg = stp.build_single_round_message().unwrap(); + let tx_sender_msg = TransactionSenderMessage::Single(Box::new(stp_msg.clone())); + + let tx_id = stp.get_tx_id().unwrap(); + let mut outbound_tx = OutboundTransaction { + tx_id, + destination_public_key: PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + amount, + fee: stp.clone().get_fee_amount().unwrap(), + sender_protocol: stp.clone(), + status: TransactionStatus::Pending, + message: "Yo!".to_string(), + timestamp: Utc::now().naive_utc(), + cancelled: false, + direct_send_success: false, + send_count: 1, + last_send_timestamp: Some(Utc::now().naive_utc()), + }; + + let alice_backend = TransactionMemoryDatabase::new(); + alice_backend + .write(WriteOperation::Insert(DbKeyValuePair::PendingOutboundTransaction( + tx_id, + Box::new(outbound_tx.clone()), + ))) + .unwrap(); + + let (mut alice_ts, _, alice_outbound_service, _, _, _, _, _, _) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + alice_backend, + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + ..Default::default() + }), + ); + + // Need to set something for alices base node, doesn't matter what + runtime + .block_on(alice_ts.set_base_node_public_key(alice_node_identity.public_key().clone())) + .unwrap(); + // Check that if the cooldown is not done that a message will not be sent. + assert!(alice_outbound_service + .wait_call_count(1, Duration::from_secs(5)) + .is_err()); + drop(alice_ts); + drop(alice_outbound_service); + + // Now we do it again with the timestamp prior to the cooldown and see that a message is sent + outbound_tx.send_count = 1; + outbound_tx.last_send_timestamp = Utc::now().naive_utc().checked_sub_signed(ChronoDuration::seconds(20)); + + let alice_backend2 = TransactionMemoryDatabase::new(); + alice_backend2 + .write(WriteOperation::Insert(DbKeyValuePair::PendingOutboundTransaction( + tx_id, + Box::new(outbound_tx), + ))) + .unwrap(); + + let (mut alice_ts2, _, alice_outbound_service2, _, _, _, _, _, _) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + alice_backend2, + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + ..Default::default() + }), + ); + + // Need to set something for alices base node, doesn't matter what + runtime + .block_on(alice_ts2.set_base_node_public_key(alice_node_identity.public_key().clone())) + .unwrap(); + // Check for resend on startup + alice_outbound_service2 + .wait_call_count(1, Duration::from_secs(30)) + .expect("Carol call wait 1"); + + let call = alice_outbound_service2.pop_call().unwrap(); + + if let TransactionSenderMessage::Single(data) = try_decode_sender_message(call.1.to_vec().clone()).unwrap() { + assert_eq!(data.tx_id, tx_id); + } else { + assert!(false, "Should be a Single Transaction Sender Message") + } + + // Now we do this for the Transaction Reply + + let rtp = ReceiverTransactionProtocol::new( + tx_sender_msg, + PrivateKey::random(&mut OsRng), + PrivateKey::random(&mut OsRng), + OutputFeatures::default(), + &factories, + ); + + let mut inbound_tx = InboundTransaction { + tx_id, + source_public_key: PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + amount, + receiver_protocol: rtp, + status: TransactionStatus::Pending, + message: "Yo2".to_string(), + timestamp: Utc::now().naive_utc(), + cancelled: false, + direct_send_success: false, + send_count: 0, + last_send_timestamp: Some(Utc::now().naive_utc()), + }; + + let bob_backend = TransactionMemoryDatabase::new(); + bob_backend + .write(WriteOperation::Insert(DbKeyValuePair::PendingInboundTransaction( + tx_id, + Box::new(inbound_tx.clone()), + ))) + .unwrap(); + + let (mut bob_ts, _, bob_outbound_service, _, _, _, _, _, _) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + bob_backend, + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + ..Default::default() + }), + ); + + // Need to set something for bobs base node, doesn't matter what + runtime + .block_on(bob_ts.set_base_node_public_key(alice_node_identity.public_key().clone())) + .unwrap(); + // Check that if the cooldown is not done that a message will not be sent. + assert!(bob_outbound_service.wait_call_count(1, Duration::from_secs(5)).is_err()); + drop(bob_ts); + drop(bob_outbound_service); + + // Now we do it again with the timestamp prior to the cooldown and see that a message is sent + inbound_tx.send_count = 1; + inbound_tx.last_send_timestamp = Utc::now().naive_utc().checked_sub_signed(ChronoDuration::seconds(20)); + + let bob_backend2 = TransactionMemoryDatabase::new(); + bob_backend2 + .write(WriteOperation::Insert(DbKeyValuePair::PendingInboundTransaction( + tx_id, + Box::new(inbound_tx), + ))) + .unwrap(); + + let (mut bob_ts2, _, bob_outbound_service2, _, _, _, _, _, _) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + bob_backend2, + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + ..Default::default() + }), + ); + + // Need to set something for bobs base node, doesn't matter what + runtime + .block_on(bob_ts2.set_base_node_public_key(alice_node_identity.public_key().clone())) + .unwrap(); + // Check for resend on startup + + bob_outbound_service2 + .wait_call_count(1, Duration::from_secs(30)) + .expect("Dave call wait 1"); + + let call = bob_outbound_service2.pop_call().unwrap(); + + let reply = try_decode_transaction_reply_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(reply.tx_id, tx_id); +} + +#[test] +fn test_replying_to_cancelled_tx() { + let factories = CryptoFactories::default(); + let mut runtime = Runtime::new().unwrap(); + + let alice_node_identity = + NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); + let bob_node_identity = + NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); + // Testing if a Tx Reply is received for a Cancelled Outbound Tx that a Cancelled message is sent back: + let alice_db_name = format!("{}.sqlite3", random_string(8).as_str()); + let alice_temp_dir = tempdir().unwrap(); + let alice_db_folder = alice_temp_dir.path().to_str().unwrap().to_string(); + let alice_connection = + run_migration_and_create_sqlite_connection(&format!("{}/{}", alice_db_folder, alice_db_name)).unwrap(); + let ( + mut alice_ts, + mut alice_output_manager, + alice_outbound_service, + _alice_tx_sender, + mut alice_tx_reply_sender, + _, + _, + _, + _, + ) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + TransactionServiceSqliteDatabase::new(alice_connection, None), + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + pending_transaction_cancellation_timeout: Duration::from_secs(20), + ..Default::default() + }), + ); + + // Send a transaction to Bob + let alice_total_available = 250000 * uT; + let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); + runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + + let amount_sent = 10000 * uT; + + let tx_id = runtime + .block_on(alice_ts.send_transaction( + bob_node_identity.public_key().clone(), + amount_sent, + 100 * uT, + "Testing Message".to_string(), + )) + .unwrap(); + alice_outbound_service + .wait_call_count(1, Duration::from_secs(30)) + .expect("Alice call wait 1"); + + let call = alice_outbound_service.pop_call().unwrap(); + let alice_sender_message = try_decode_sender_message(call.1.to_vec().clone()).unwrap(); + if let TransactionSenderMessage::Single(data) = alice_sender_message.clone() { + assert_eq!(data.tx_id, tx_id); + } + // Need a moment for Alice's wallet to finish writing to its database before cancelling + runtime.block_on(async { delay_for(Duration::from_secs(5)).await }); + + runtime.block_on(alice_ts.cancel_transaction(tx_id)).unwrap(); + + // Setup Bob's wallet with no comms stack + let bob_db_name = format!("{}.sqlite3", random_string(8).as_str()); + let bob_temp_dir = tempdir().unwrap(); + let bob_db_folder = bob_temp_dir.path().to_str().unwrap().to_string(); + let bob_connection = + run_migration_and_create_sqlite_connection(&format!("{}/{}", bob_db_folder, bob_db_name)).unwrap(); + + let (_bob_ts, _bob_output_manager, bob_outbound_service, mut bob_tx_sender, mut _bob_tx_reply_sender, _, _, _, _) = + setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + TransactionServiceSqliteDatabase::new(bob_connection, None), + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + pending_transaction_cancellation_timeout: Duration::from_secs(15), + ..Default::default() + }), + ); + + // Pass sender message to Bob's wallet + runtime + .block_on(bob_tx_sender.send(create_dummy_message( + alice_sender_message.clone().into(), + alice_node_identity.public_key(), + ))) + .unwrap(); + bob_outbound_service + .wait_call_count(1, Duration::from_secs(30)) + .expect("Bob call wait 1"); + + let call = bob_outbound_service.pop_call().unwrap(); + let bob_reply_message = try_decode_transaction_reply_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(bob_reply_message.tx_id, tx_id); + + // Wait for cooldown to expire + runtime.block_on(async { delay_for(Duration::from_secs(5)).await }); + + let _ = alice_outbound_service.take_calls(); + + runtime + .block_on(alice_tx_reply_sender.send(create_dummy_message( + bob_reply_message.clone().into(), + bob_node_identity.public_key(), + ))) + .unwrap(); + + alice_outbound_service + .wait_call_count(1, Duration::from_secs(30)) + .expect("Alice call wait 2"); + + let call = alice_outbound_service.pop_call().unwrap(); + let alice_cancelled_message = try_decode_transaction_cancelled_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(alice_cancelled_message.tx_id, tx_id); +} + +#[test] +fn test_transaction_timeout_cancellation() { + let factories = CryptoFactories::default(); + let mut runtime = Runtime::new().unwrap(); + + let bob_node_identity = + NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE).unwrap(); + // Testing if a Tx Reply is received for a Cancelled Outbound Tx that a Cancelled message is sent back: + let alice_db_name = format!("{}.sqlite3", random_string(8).as_str()); + let alice_temp_dir = tempdir().unwrap(); + let alice_db_folder = alice_temp_dir.path().to_str().unwrap().to_string(); + let alice_connection = + run_migration_and_create_sqlite_connection(&format!("{}/{}", alice_db_folder, alice_db_name)).unwrap(); + let ( + mut alice_ts, + mut alice_output_manager, + alice_outbound_service, + _alice_tx_sender, + _alice_tx_reply_sender, + _, + _, + _, + _, + ) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + TransactionServiceSqliteDatabase::new(alice_connection, None), + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + pending_transaction_cancellation_timeout: Duration::from_secs(15), + ..Default::default() + }), + ); + + // Send a transaction to Bob + let alice_total_available = 250000 * uT; + let (_utxo, uo) = make_input(&mut OsRng, alice_total_available, &factories.commitment); + runtime.block_on(alice_output_manager.add_output(uo)).unwrap(); + + let amount_sent = 10000 * uT; + + let tx_id = runtime + .block_on(alice_ts.send_transaction( + bob_node_identity.public_key().clone(), + amount_sent, + 100 * uT, + "Testing Message".to_string(), + )) + .unwrap(); + + // For testing the resend period is set to 10 seconds and the timeout period is set to 15 seconds so we are going to + // wait for 3 messages The intial send, the resend and then the cancellation + alice_outbound_service + .wait_call_count(3, Duration::from_secs(60)) + .expect("Alice call wait 1"); + + let calls = alice_outbound_service.take_calls(); + assert_eq!(calls.len(), 3); + + // First call + + let sender_message = try_decode_sender_message(calls[0].1.to_vec().clone()).unwrap(); + if let TransactionSenderMessage::Single(data) = sender_message { + assert_eq!(data.tx_id, tx_id); + } else { + assert!(false, "Should be a Single Transaction Sender Message") + } + // Resend + let sender_message = try_decode_sender_message(calls[1].1.to_vec().clone()).unwrap(); + if let TransactionSenderMessage::Single(data) = sender_message { + assert_eq!(data.tx_id, tx_id); + } else { + assert!(false, "Should be a Single Transaction Sender Message") + } + + // Timeout Cancellation + let alice_cancelled_message = try_decode_transaction_cancelled_message(calls[2].1.to_vec().clone()).unwrap(); + assert_eq!(alice_cancelled_message.tx_id, tx_id); + + // Now to test if the timeout has elapsed during downtime and that it is honoured on startup + // First we will check the Send Transction message + let mut builder = SenderTransactionProtocol::builder(1); + let amount = MicroTari::from(10_000); + let input = UnblindedOutput::new(MicroTari::from(100_000), PrivateKey::random(&mut OsRng), None); + builder + .with_lock_height(0) + .with_fee_per_gram(MicroTari::from(177)) + .with_offset(PrivateKey::random(&mut OsRng)) + .with_private_nonce(PrivateKey::random(&mut OsRng)) + .with_amount(0, amount) + .with_message("Yo!".to_string()) + .with_input( + input.as_transaction_input(&factories.commitment, OutputFeatures::default()), + input.clone(), + ) + .with_change_secret(PrivateKey::random(&mut OsRng)); + + let mut stp = builder.build::(&factories).unwrap(); + let stp_msg = stp.build_single_round_message().unwrap(); + let tx_sender_msg = TransactionSenderMessage::Single(Box::new(stp_msg.clone())); + + let tx_id = stp.get_tx_id().unwrap(); + let outbound_tx = OutboundTransaction { + tx_id, + destination_public_key: PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + amount, + fee: stp.clone().get_fee_amount().unwrap(), + sender_protocol: stp.clone(), + status: TransactionStatus::Pending, + message: "Yo!".to_string(), + timestamp: Utc::now() + .naive_utc() + .checked_sub_signed(ChronoDuration::seconds(20)) + .unwrap(), + cancelled: false, + direct_send_success: false, + send_count: 1, + last_send_timestamp: Some(Utc::now().naive_utc()), + }; + + let bob_backend = TransactionMemoryDatabase::new(); + bob_backend + .write(WriteOperation::Insert(DbKeyValuePair::PendingOutboundTransaction( + tx_id, + Box::new(outbound_tx.clone()), + ))) + .unwrap(); + + let (mut bob_ts, _, bob_outbound_service, _, _, _, _, _, _) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + bob_backend, + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + pending_transaction_cancellation_timeout: Duration::from_secs(15), + ..Default::default() + }), + ); + + // Need to set something for bobs base node, doesn't matter what + runtime + .block_on(bob_ts.set_base_node_public_key(bob_node_identity.public_key().clone())) + .unwrap(); + + // Make sure we receive this before the timeout as it should be sent immideately on startup + bob_outbound_service + .wait_call_count(2, Duration::from_secs(14)) + .expect("Bob call wait 1"); + let call = bob_outbound_service.pop_call().unwrap(); + let bob_cancelled_message = try_decode_transaction_cancelled_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(bob_cancelled_message.tx_id, tx_id); + + let call = bob_outbound_service.pop_call().unwrap(); + let bob_cancelled_message = try_decode_transaction_cancelled_message(call.1.to_vec().clone()).unwrap(); + assert_eq!(bob_cancelled_message.tx_id, tx_id); + + // Now to do this for the Receiver + let (carol_ts, _, carol_outbound_service, mut carol_tx_sender, _, _, _, _, _) = setup_transaction_service_no_comms( + &mut runtime, + factories.clone(), + TransactionMemoryDatabase::new(), + Some(TransactionServiceConfig { + transaction_resend_period: Duration::from_secs(10), + resend_response_cooldown: Duration::from_secs(5), + pending_transaction_cancellation_timeout: Duration::from_secs(15), + ..Default::default() + }), + ); + let mut carol_event_stream = carol_ts.get_event_stream_fused(); + + runtime + .block_on(carol_tx_sender.send(create_dummy_message( + tx_sender_msg.clone().into(), + bob_node_identity.public_key(), + ))) + .unwrap(); + + // Then we should get 2 reply messages and 1 cancellation event + carol_outbound_service + .wait_call_count(2, Duration::from_secs(60)) + .expect("Carol call wait 1"); + + let calls = carol_outbound_service.take_calls(); + assert_eq!(calls.len(), 2); + + // Initial Reply + let carol_reply_message = try_decode_transaction_reply_message(calls[0].1.to_vec().clone()).unwrap(); + assert_eq!(carol_reply_message.tx_id, tx_id); + + // Resend + let carol_reply_message = try_decode_transaction_reply_message(calls[1].1.to_vec().clone()).unwrap(); + assert_eq!(carol_reply_message.tx_id, tx_id); + + runtime.block_on(async { + let mut delay = delay_for(Duration::from_secs(60)).fuse(); + let mut transaction_cancelled = false; + loop { + futures::select! { + event = carol_event_stream.select_next_some() => { + match &*event.unwrap() { + TransactionEvent::TransactionCancelled(t) => { + if t == &tx_id { + transaction_cancelled = true; + break; + } + + } + _ => (), + } + }, + () = delay => { + break; + }, + } + } + assert!(transaction_cancelled, "Transaction must be cancelled"); + }); +} diff --git a/base_layer/wallet/tests/transaction_service/storage.rs b/base_layer/wallet/tests/transaction_service/storage.rs index b83dec0934..e7e2e9688c 100644 --- a/base_layer/wallet/tests/transaction_service/storage.rs +++ b/base_layer/wallet/tests/transaction_service/storage.rs @@ -39,16 +39,15 @@ use tari_crypto::keys::{PublicKey as PublicKeyTrait, SecretKey as SecretKeyTrait use tari_wallet::{ storage::sqlite_utilities::run_migration_and_create_sqlite_connection, transaction_service::storage::{ - database::{ + database::{TransactionBackend, TransactionDatabase}, + memory_db::TransactionMemoryDatabase, + models::{ CompletedTransaction, InboundTransaction, OutboundTransaction, - TransactionBackend, - TransactionDatabase, TransactionDirection, TransactionStatus, }, - memory_db::TransactionMemoryDatabase, sqlite_db::TransactionServiceSqliteDatabase, }, }; @@ -94,6 +93,8 @@ pub fn test_db_backend(backend: T) { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }); assert!( !runtime.block_on(db.transaction_exists((i + 10) as u64)).unwrap(), @@ -116,6 +117,8 @@ pub fn test_db_backend(backend: T) { .block_on(db.get_pending_outbound_transaction(outbound_txs[i].tx_id)) .unwrap(); assert_eq!(retrieved_outbound_tx, outbound_txs[i]); + assert_eq!(retrieved_outbound_tx.send_count, 0); + assert!(retrieved_outbound_tx.last_send_timestamp.is_none()); assert_eq!( retrieved_outbound_txs.get(&outbound_txs[i].tx_id).unwrap(), @@ -123,6 +126,15 @@ pub fn test_db_backend(backend: T) { ); } + runtime + .block_on(db.increment_send_count(outbound_txs[0].tx_id)) + .unwrap(); + let retrieved_outbound_tx = runtime + .block_on(db.get_pending_outbound_transaction(outbound_txs[0].tx_id)) + .unwrap(); + assert_eq!(retrieved_outbound_tx.send_count, 1); + assert!(retrieved_outbound_tx.last_send_timestamp.is_some()); + let rtp = ReceiverTransactionProtocol::new( TransactionSenderMessage::Single(Box::new(stp.clone().build_single_round_message().unwrap())), PrivateKey::random(&mut OsRng), @@ -144,6 +156,8 @@ pub fn test_db_backend(backend: T) { timestamp: Utc::now().naive_utc(), cancelled: false, direct_send_success: false, + send_count: 0, + last_send_timestamp: None, }); assert!( !runtime.block_on(db.transaction_exists(i as u64)).unwrap(), @@ -161,12 +175,19 @@ pub fn test_db_backend(backend: T) { let retrieved_inbound_txs = runtime.block_on(db.get_pending_inbound_transactions()).unwrap(); assert_eq!(inbound_txs.len(), messages.len()); for i in 0..messages.len() { - assert_eq!( - retrieved_inbound_txs.get(&inbound_txs[i].tx_id).unwrap(), - &inbound_txs[i] - ); + let retrieved_tx = retrieved_inbound_txs.get(&inbound_txs[i].tx_id).unwrap(); + assert_eq!(retrieved_tx, &inbound_txs[i]); + assert_eq!(retrieved_tx.send_count, 0); + assert!(retrieved_tx.last_send_timestamp.is_none()); } + runtime.block_on(db.increment_send_count(inbound_txs[0].tx_id)).unwrap(); + let retrieved_inbound_tx = runtime + .block_on(db.get_pending_inbound_transaction(inbound_txs[0].tx_id)) + .unwrap(); + assert_eq!(retrieved_inbound_tx.send_count, 1); + assert!(retrieved_inbound_tx.last_send_timestamp.is_some()); + let inbound_pub_key = runtime .block_on(db.get_pending_transaction_counterparty_pub_key_by_tx_id(inbound_txs[0].tx_id)) .unwrap(); @@ -202,6 +223,8 @@ pub fn test_db_backend(backend: T) { cancelled: false, direction: TransactionDirection::Outbound, coinbase_block_height: None, + send_count: 0, + last_send_timestamp: None, }); runtime .block_on(db.complete_outbound_transaction(outbound_txs[i].tx_id, completed_txs[i].clone())) @@ -233,6 +256,18 @@ pub fn test_db_backend(backend: T) { ); } + runtime + .block_on(db.increment_send_count(completed_txs[0].tx_id)) + .unwrap(); + runtime + .block_on(db.increment_send_count(completed_txs[0].tx_id)) + .unwrap(); + let retrieved_completed_tx = runtime + .block_on(db.get_completed_transaction(completed_txs[0].tx_id)) + .unwrap(); + assert_eq!(retrieved_completed_tx.send_count, 2); + assert!(retrieved_completed_tx.last_send_timestamp.is_some()); + if cfg!(feature = "test_harness") { let retrieved_completed_txs = runtime.block_on(db.get_completed_transactions()).unwrap(); assert!(retrieved_completed_txs.contains_key(&completed_txs[0].tx_id)); diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs index 2fa7eafb7f..15c2f6706a 100644 --- a/base_layer/wallet/tests/wallet/mod.rs +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -62,6 +62,7 @@ use tari_wallet::{ sqlite_utilities::{partial_wallet_backup, run_migration_and_create_sqlite_connection}, }, transaction_service::{ + config::TransactionServiceConfig, handle::TransactionEvent, storage::{memory_db::TransactionMemoryDatabase, sqlite_db::TransactionServiceSqliteDatabase}, }, @@ -124,7 +125,17 @@ async fn create_wallet( let output_manager_backend = OutputManagerSqliteDatabase::new(connection.clone(), None); let contacts_backend = ContactsServiceSqliteDatabase::new(connection); - let config = WalletConfig::new(comms_config, factories, None, Network::Rincewind); + let transaction_service_config = TransactionServiceConfig { + resend_response_cooldown: Duration::from_secs(1), + ..Default::default() + }; + + let config = WalletConfig::new( + comms_config, + factories, + Some(transaction_service_config), + Network::Rincewind, + ); let wallet = Wallet::new( config, @@ -356,7 +367,6 @@ async fn test_store_and_forward_send_tx() { ) .await; let bob_wallet = create_wallet(bob_identity.clone(), &db_tempdir.path(), "bob_db", factories.clone()).await; - let mut alice_event_stream = alice_wallet.transaction_service.get_event_stream_fused(); alice_wallet .comms @@ -384,7 +394,7 @@ async fn test_store_and_forward_send_tx() { alice_wallet.output_manager_service.add_output(uo1).await.unwrap(); - alice_wallet + let tx_id = alice_wallet .transaction_service .send_transaction( carol_identity.public_key().clone(), @@ -398,6 +408,14 @@ async fn test_store_and_forward_send_tx() { // Waiting here for a while to make sure the discovery retry is over delay_for(Duration::from_secs(10)).await; + alice_wallet + .transaction_service + .cancel_transaction(tx_id) + .await + .unwrap(); + + delay_for(Duration::from_secs(10)).await; + let carol_wallet = create_wallet( carol_identity.clone(), &db_tempdir.path(), @@ -406,6 +424,7 @@ async fn test_store_and_forward_send_tx() { ) .await; + let mut carol_event_stream = carol_wallet.transaction_service.get_event_stream_fused(); carol_wallet .comms .peer_manager() @@ -418,15 +437,17 @@ async fn test_store_and_forward_send_tx() { carol_wallet.dht_service.dht_requester().send_join().await.unwrap(); let mut delay = delay_for(Duration::from_secs(60)).fuse(); - let mut tx_reply = 0; + let mut tx_recv = false; + let mut tx_cancelled = false; loop { futures::select! { - event = alice_event_stream.select_next_some() => { + event = carol_event_stream.select_next_some() => { match &*event.unwrap() { - TransactionEvent::ReceivedTransactionReply(_) => tx_reply+=1, + TransactionEvent::ReceivedTransaction(_) => tx_recv = true, + TransactionEvent::TransactionCancelled(_) => tx_cancelled = true, _ => (), } - if tx_reply == 1 { + if tx_recv && tx_cancelled { break; } }, @@ -435,7 +456,8 @@ async fn test_store_and_forward_send_tx() { }, } } - assert_eq!(tx_reply, 1, "Must have received a reply from Carol"); + assert!(tx_recv, "Must have received a tx from alice"); + assert!(tx_cancelled, "Must have received a cancel tx from alice"); alice_wallet.shutdown().await; bob_wallet.shutdown().await; diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index b4fd801f61..ab8438e3fe 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -60,7 +60,10 @@ use tari_wallet::{ }, transaction_service::{ handle::{TransactionEvent, TransactionEventReceiver}, - storage::database::{CompletedTransaction, InboundTransaction, TransactionBackend, TransactionDatabase}, + storage::{ + database::{TransactionBackend, TransactionDatabase}, + models::{CompletedTransaction, InboundTransaction}, + }, }, }; @@ -447,15 +450,15 @@ mod test { transaction_service::{ handle::TransactionEvent, storage::{ - database::{ + database::TransactionDatabase, + memory_db::TransactionMemoryDatabase, + models::{ CompletedTransaction, InboundTransaction, OutboundTransaction, - TransactionDatabase, TransactionDirection, TransactionStatus, }, - memory_db::TransactionMemoryDatabase, }, }, }; diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 14ca67fbc0..09d9539f08 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -184,11 +184,11 @@ use tari_wallet::{ config::TransactionServiceConfig, error::TransactionServiceError, storage::{ - database::{ + database::TransactionDatabase, + models::{ CompletedTransaction, InboundTransaction, OutboundTransaction, - TransactionDatabase, TransactionDirection, TransactionStatus, }, @@ -221,12 +221,12 @@ pub type TariCommsConfig = tari_p2p::initialization::CommsConfig; pub struct TariContacts(Vec); pub type TariContact = tari_wallet::contacts_service::storage::database::Contact; -pub type TariCompletedTransaction = tari_wallet::transaction_service::storage::database::CompletedTransaction; +pub type TariCompletedTransaction = tari_wallet::transaction_service::storage::models::CompletedTransaction; pub struct TariCompletedTransactions(Vec); -pub type TariPendingInboundTransaction = tari_wallet::transaction_service::storage::database::InboundTransaction; -pub type TariPendingOutboundTransaction = tari_wallet::transaction_service::storage::database::OutboundTransaction; +pub type TariPendingInboundTransaction = tari_wallet::transaction_service::storage::models::InboundTransaction; +pub type TariPendingOutboundTransaction = tari_wallet::transaction_service::storage::models::OutboundTransaction; pub struct TariPendingInboundTransactions(Vec); @@ -4729,7 +4729,7 @@ mod test { use tari_key_manager::mnemonic::Mnemonic; use tari_wallet::{ testnet_utils::random_string, - transaction_service::storage::database::TransactionStatus, + transaction_service::storage::models::TransactionStatus, util::emoji, }; use tempfile::tempdir; @@ -5351,7 +5351,7 @@ mod test { let inbound_transactions: std::collections::HashMap< u64, - tari_wallet::transaction_service::storage::database::InboundTransaction, + tari_wallet::transaction_service::storage::models::InboundTransaction, > = (*alice_wallet) .runtime .block_on( @@ -5372,7 +5372,7 @@ mod test { let inbound_transactions: std::collections::HashMap< u64, - tari_wallet::transaction_service::storage::database::InboundTransaction, + tari_wallet::transaction_service::storage::models::InboundTransaction, > = (*alice_wallet) .runtime .block_on( @@ -5417,7 +5417,7 @@ mod test { let completed_transactions: std::collections::HashMap< u64, - tari_wallet::transaction_service::storage::database::CompletedTransaction, + tari_wallet::transaction_service::storage::models::CompletedTransaction, > = (*alice_wallet) .runtime .block_on((*alice_wallet).wallet.transaction_service.get_completed_transactions()) @@ -5433,7 +5433,7 @@ mod test { let completed_transactions: std::collections::HashMap< u64, - tari_wallet::transaction_service::storage::database::CompletedTransaction, + tari_wallet::transaction_service::storage::models::CompletedTransaction, > = (*alice_wallet) .runtime .block_on((*alice_wallet).wallet.transaction_service.get_completed_transactions()) @@ -5486,7 +5486,7 @@ mod test { // TODO: Test transaction collection and transaction methods let completed_transactions: std::collections::HashMap< u64, - tari_wallet::transaction_service::storage::database::CompletedTransaction, + tari_wallet::transaction_service::storage::models::CompletedTransaction, > = (*alice_wallet) .runtime .block_on((*alice_wallet).wallet.transaction_service.get_completed_transactions())