From 40bedde2053a8e98a2c1a97fc5223be105d0e60e Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Fri, 1 Apr 2022 09:37:01 +0200 Subject: [PATCH] feat!: add less aggressive txn cancelling (#3904) Description --- - Added less aggressive transaction canceling, i.e. removed automatic transaction cancelling, when: - a wallet has difficulty discovering the receiving party (for direct P2P comms); - a wallet has difficulty discovering base node peers that could be used to send the transaction via store and forward; - tor is not available. - Updated the wallet ffi with the new callbacks. - Added a cucumber test to test sending transactions while offline. Users will now manually cancel transactions stuck in pending due to the wallet continuing to be being offline. _**Note:** Wallet ffi interface changed._ Motivation and Context --- During a recent semi-stress test [[report here](https://github.com/tari-project/tari-data-analysis/blob/master/reports/stress_tests/20211221-make-it-rain/Stress%20test%20of%2020211221%20-%20analysis.md)] adverse connectivity and/or discovery symptoms caused ~55% of transactions to be abandoned. Attempted transactions should not be canceled if they failed to be sent both directly and via store and forward; a peer-to-peer connection may fail for valid reasons but failing to submit a transaction to a base node for store and forward delivery does not warrant the transaction to be canceled. How Has This Been Tested? --- System-level testing Unit tests Cucumber test: `Scenario: Wallet send transactions while offline` --- applications/ffi_client/index.js | 20 +- applications/ffi_client/lib/index.js | 1 - applications/ffi_client/recovery.js | 19 +- applications/tari_app_grpc/proto/wallet.proto | 2 + .../src/conversions/transaction.rs | 1 + .../src/automation/commands.rs | 23 +- .../tari_console_wallet/src/notifier/mod.rs | 18 +- .../src/ui/components/send_tab.rs | 25 ++ .../src/ui/components/transactions_tab.rs | 3 + .../src/ui/state/app_state.rs | 23 +- .../tari_console_wallet/src/ui/state/tasks.rs | 30 +- .../src/ui/state/wallet_event_monitor.rs | 341 ++++++++++-------- base_layer/common_types/src/transaction.rs | 4 + .../transaction_protocol/sender.rs | 11 + .../src/output_manager_service/service.rs | 3 - .../storage/database/backend.rs | 2 +- .../storage/database/mod.rs | 2 +- .../storage/sqlite_db/mod.rs | 10 +- .../wallet/src/transaction_service/handle.rs | 34 +- .../protocols/transaction_send_protocol.rs | 267 ++++++++++---- .../wallet/src/transaction_service/service.rs | 125 +++++-- .../output_manager_service_tests/service.rs | 19 +- .../transaction_service_tests/service.rs | 90 +++-- base_layer/wallet_ffi/src/callback_handler.rs | 42 +-- .../wallet_ffi/src/callback_handler_tests.rs | 65 ++-- base_layer/wallet_ffi/src/lib.rs | 246 ++++++++++--- base_layer/wallet_ffi/wallet.h | 18 +- integration_tests/README.md | 6 +- .../features/WalletTransactions.feature | 25 ++ integration_tests/helpers/ffi/ffiInterface.js | 30 +- .../ffi/{liveness_data.js => livenessData.js} | 0 .../helpers/ffi/transactionSendStatus.js | 34 ++ integration_tests/helpers/ffi/wallet.js | 33 +- integration_tests/package-lock.json | 341 +++++++++++++----- integration_tests/package.json | 14 +- 35 files changed, 1288 insertions(+), 639 deletions(-) rename integration_tests/helpers/ffi/{liveness_data.js => livenessData.js} (100%) create mode 100644 integration_tests/helpers/ffi/transactionSendStatus.js diff --git a/applications/ffi_client/index.js b/applications/ffi_client/index.js index c148743d04..85be5adb50 100644 --- a/applications/ffi_client/index.js +++ b/applications/ffi_client/index.js @@ -84,13 +84,9 @@ try { console.log("txFauxUnconfirmed: ", ptr, confirmations); } ); - // callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), - const directSendResult = ffi.Callback("void", [u64, bool], function (i, j) { - console.log("directSendResult: ", i, j); - }); - // callback_store_and_forward_send_result: unsafe extern "C" fn(c_ulonglong, bool), - const safResult = ffi.Callback("void", [u64, bool], function (i, j) { - console.log("safResult: ", i, j); + // callback_transaction_send_result: unsafe extern "C" fn(c_ulonglong, *mut TariTransactionSendStatus), + const transactionSendResult = ffi.Callback("void", [u64, ["pointer"]], function (i, ptr) { + console.log("transactionSendResult: ", i, ptr); }); // callback_transaction_cancellation: unsafe extern "C" fn(*mut TariCompletedTransaction), const txCancelled = ffi.Callback("void", ["pointer"], function (ptr) { @@ -100,7 +96,7 @@ try { const txoValidation = ffi.Callback("void", [u64, u8], function (i, j) { console.log("txoValidation: ", i, j); }); - // callback_contacts_liveness_data_updated: unsafe extern "C" fn(*mut ContactsLivenessData), + // callback_contacts_liveness_data_updated: unsafe extern "C" fn(*mut TariContactsLivenessData), const contactsLivenessDataUpdated = ffi.Callback("void", ["pointer"], function (ptr) { console.log("contactsLivenessDataUpdated: ", ptr); }); @@ -116,6 +112,10 @@ try { const safsReceived = ffi.Callback("void", [], function () { console.log("safsReceived"); }); + // callback_connectivity_status: unsafe extern "C" fn(), + const connectivityStatus = ffi.Callback("void", [u64], function () { + console.log("connectivityStatus"); + }); console.log("Create Wallet..."); let wallet = lib.wallet_create( @@ -133,14 +133,14 @@ try { txMinedUnconfirmed, txFauxConfirmed, txFauxUnconfirmed, - directSendResult, - safResult, + transactionSendResult, txCancelled, txoValidation, contactsLivenessDataUpdated, balanceUpdated, txValidation, safsReceived, + connectivityStatus, recoveryInProgress, err ); diff --git a/applications/ffi_client/lib/index.js b/applications/ffi_client/lib/index.js index bfe444954d..8750bbbe70 100644 --- a/applications/ffi_client/lib/index.js +++ b/applications/ffi_client/lib/index.js @@ -71,7 +71,6 @@ const libWallet = ffi.Library("./libtari_wallet_ffi.dylib", { fn, fn, fn, - fn, bool, errPtr, ], diff --git a/applications/ffi_client/recovery.js b/applications/ffi_client/recovery.js index 2c3abe42bf..3a53a97046 100644 --- a/applications/ffi_client/recovery.js +++ b/applications/ffi_client/recovery.js @@ -97,14 +97,9 @@ try { console.log("txFauxUnconfirmed: ", ptr, confirmations); } ); - // callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), - const directSendResult = ffi.Callback("void", [u64, bool], function (i, j) { - console.log("directSendResult: ", i, j); - }); - // callback_store_and_forward_send_result: unsafe extern "C" fn(c_ulonglong, bool), - const safResult = ffi.Callback("void", [u64, bool], function (i, j) { - console.log("safResult: ", i, j); - }); + // callback_transaction_send_result: unsafe extern "C" fn(c_ulonglong, *mut TariTransactionSendStatus), + const transactionSendResult = ffi.Callback("void", [u64, ["pointer"]], function (i, ptr) { + console.log("transactionSendResult: ", i, ptr); // callback_transaction_cancellation: unsafe extern "C" fn(*mut TariCompletedTransaction), const txCancelled = ffi.Callback("void", ["pointer"], function (ptr) { console.log("txCancelled: ", ptr); @@ -129,6 +124,10 @@ try { const safsReceived = ffi.Callback("void", [], function () { console.log("safsReceived"); }); + // callback_connectivity_status: unsafe extern "C" fn(), + const connectivityStatus = ffi.Callback("void", [u64], function () { + console.log("connectivityStatus"); + }); const recovery = ffi.Callback("void", [u64, u64], function (current, total) { console.log("recovery scanning UTXOs: ", { current }, { total }); @@ -163,14 +162,14 @@ try { txMinedUnconfirmed, txFauxConfirmed, txFauxUnconfirmed, - directSendResult, - safResult, + transactionSendResult, txCancelled, txoValidation, contactsLivenessDataUpdated, balanceUpdated, txValidation, safsReceived, + connectivityStatus, recoveryInProgress, err ); diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index 2acb7be966..75301feaaa 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -200,6 +200,8 @@ enum TransactionStatus { TRANSACTION_STATUS_FAUX_UNCONFIRMED = 9; // All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed TRANSACTION_STATUS_FAUX_CONFIRMED = 10; + // This transaction is still being queued for sending + TRANSACTION_STATUS_QUEUED = 11; } message GetCompletedTransactionsRequest { } diff --git a/applications/tari_app_grpc/src/conversions/transaction.rs b/applications/tari_app_grpc/src/conversions/transaction.rs index 2757e22188..fd44c03825 100644 --- a/applications/tari_app_grpc/src/conversions/transaction.rs +++ b/applications/tari_app_grpc/src/conversions/transaction.rs @@ -100,6 +100,7 @@ impl From for grpc::TransactionStatus { Rejected => grpc::TransactionStatus::Rejected, FauxUnconfirmed => grpc::TransactionStatus::FauxUnconfirmed, FauxConfirmed => grpc::TransactionStatus::FauxConfirmed, + Queued => grpc::TransactionStatus::Queued, } } } diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 1c4261594e..5ac16f1dcf 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -549,24 +549,11 @@ pub async fn monitor_transactions( loop { match event_stream.recv().await { Ok(event) => match &*event { - TransactionEvent::TransactionDirectSendResult(id, success) if tx_ids.contains(id) => { - debug!( - target: LOG_TARGET, - "tx direct send event for tx_id: {}, success: {}", *id, success - ); - if wait_stage == TransactionStage::DirectSendOrSaf { - results.push(SentTransaction {}); - if results.len() == tx_ids.len() { - break; - } - } - }, - TransactionEvent::TransactionStoreForwardSendResult(id, success) if tx_ids.contains(id) => { - debug!( - target: LOG_TARGET, - "tx store and forward event for tx_id: {}, success: {}", *id, success - ); - if wait_stage == TransactionStage::DirectSendOrSaf { + TransactionEvent::TransactionSendResult(id, status) if tx_ids.contains(id) => { + debug!(target: LOG_TARGET, "tx send event for tx_id: {}, {}", *id, status); + if wait_stage == TransactionStage::DirectSendOrSaf && + (status.direct_send_result || status.store_and_forward_send_result) + { results.push(SentTransaction {}); if results.len() == tx_ids.len() { break; diff --git a/applications/tari_console_wallet/src/notifier/mod.rs b/applications/tari_console_wallet/src/notifier/mod.rs index bc57527584..5f4ce8162b 100644 --- a/applications/tari_console_wallet/src/notifier/mod.rs +++ b/applications/tari_console_wallet/src/notifier/mod.rs @@ -43,6 +43,7 @@ use tokio::runtime::Handle; pub const LOG_TARGET: &str = "wallet::notifier"; const RECEIVED: &str = "received"; const SENT: &str = "sent"; +const QUEUED: &str = "queued"; const CONFIRMATION: &str = "confirmation"; const MINED: &str = "mined"; const CANCELLED: &str = "cancelled"; @@ -132,9 +133,18 @@ impl Notifier { } } - /// Trigger a notification that a pending transaction was sent. - pub fn transaction_sent(&self, tx_id: TxId) { - debug!(target: LOG_TARGET, "transaction_sent tx_id: {}", tx_id); + /// Trigger a notification that a pending transaction was sent or queued. + pub fn transaction_sent_or_queued(&self, tx_id: TxId, is_sent: bool) { + let event = if is_sent { + debug!(target: LOG_TARGET, "Transaction sent tx_id: {}", tx_id); + SENT + } else { + debug!( + target: LOG_TARGET, + "Transaction queued for further retry sending tx_id: {}", tx_id + ); + QUEUED + }; if let Some(program) = self.path.clone() { let mut transaction_service = self.wallet.transaction_service.clone(); @@ -143,7 +153,7 @@ impl Notifier { match transaction_service.get_pending_outbound_transactions().await { Ok(txs) => { if let Some(tx) = txs.get(&tx_id) { - let args = args_from_outbound(tx, SENT); + let args = args_from_outbound(tx, event); let result = Command::new(program).args(&args).output(); log(result); } else { diff --git a/applications/tari_console_wallet/src/ui/components/send_tab.rs b/applications/tari_console_wallet/src/ui/components/send_tab.rs index c55f33deac..2c9ade498f 100644 --- a/applications/tari_console_wallet/src/ui/components/send_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/send_tab.rs @@ -1,6 +1,7 @@ // Copyright 2022 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use log::*; use tari_core::transactions::tari_amount::MicroTari; use tari_utilities::hex::Hex; use tari_wallet::tokens::Token; @@ -21,6 +22,8 @@ use crate::ui::{ widgets::{draw_dialog, WindowedListState}, }; +const LOG_TARGET: &str = "wallet::console_wallet::send_tab "; + pub struct SendTab { balance: Balance, send_input_mode: SendInputMode, @@ -31,6 +34,7 @@ pub struct SendTab { message_field: String, error_message: Option, success_message: Option, + offline_message: Option, contacts_list_state: WindowedListState, send_result_watch: Option>, confirmation_dialog: Option, @@ -50,6 +54,7 @@ impl SendTab { message_field: String::new(), error_message: None, success_message: None, + offline_message: None, contacts_list_state: WindowedListState::new(), send_result_watch: None, confirmation_dialog: None, @@ -451,6 +456,7 @@ impl Component for SendTab { let rx_option = self.send_result_watch.take(); if let Some(rx) = rx_option { + trace!(target: LOG_TARGET, "{:?}", (*rx.borrow()).clone()); let status = match (*rx.borrow()).clone() { UiTransactionSendStatus::Initiated => "Initiated", UiTransactionSendStatus::DiscoveryInProgress => "Discovery In Progress", @@ -463,6 +469,14 @@ impl Component for SendTab { Some("Transaction successfully sent!\nPlease press Enter to continue".to_string()); return; }, + UiTransactionSendStatus::Queued => { + self.offline_message = Some( + "This wallet appears to be offline; transaction queued for further retry sending.\n Please \ + press Enter to continue" + .to_string(), + ); + return; + }, UiTransactionSendStatus::TransactionComplete => { self.success_message = Some("Transaction completed successfully!\nPlease press Enter to continue".to_string()); @@ -485,6 +499,10 @@ impl Component for SendTab { draw_dialog(f, area, "Success!".to_string(), msg, Color::Green, 120, 9); } + if let Some(msg) = self.offline_message.clone() { + draw_dialog(f, area, "Offline!".to_string(), msg, Color::Green, 120, 9); + } + if let Some(msg) = self.error_message.clone() { draw_dialog(f, area, "Error!".to_string(), msg, Color::Red, 120, 9); } @@ -531,6 +549,13 @@ impl Component for SendTab { return; } + if self.offline_message.is_some() { + if '\n' == c { + self.offline_message = None; + } + return; + } + if self.send_result_watch.is_some() { return; } 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 705ec26980..f1aac116f1 100644 --- a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs @@ -551,6 +551,9 @@ impl Component for TransactionsTab { match c { 'p' => { + if let Err(e) = Handle::current().block_on(app_state.restart_transaction_protocols()) { + error!(target: LOG_TARGET, "Error rebroadcasting transactions: {}", e); + } self.completed_list_state.select(None); self.selected_tx_list = SelectedTransactionList::PendingTxs; self.pending_list_state.set_num_items(app_state.get_pending_txs().len()); 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 3a61b3d892..18c2f692b2 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -356,6 +356,13 @@ impl AppState { Ok(()) } + pub async fn restart_transaction_protocols(&mut self) -> Result<(), UiError> { + let inner = self.inner.write().await; + let mut tx_service = inner.wallet.transaction_service.clone(); + tx_service.restart_transaction_protocols().await?; + Ok(()) + } + pub fn get_identity(&self) -> &MyIdentity { &self.cached_data.my_identity } @@ -857,6 +864,7 @@ impl AppStateInner { ) .await?; + self.spawn_restart_transaction_protocols_task(); self.spawn_transaction_revalidation_task(); self.data.base_node_previous = self.data.base_node_selected.clone(); @@ -881,6 +889,7 @@ impl AppStateInner { ) .await?; + self.spawn_restart_transaction_protocols_task(); self.spawn_transaction_revalidation_task(); self.data.base_node_previous = self.data.base_node_selected.clone(); @@ -922,6 +931,7 @@ impl AppStateInner { ) .await?; + self.spawn_restart_transaction_protocols_task(); self.spawn_transaction_revalidation_task(); self.data.base_node_peer_custom = None; @@ -956,6 +966,16 @@ impl AppStateInner { }); } + pub fn spawn_restart_transaction_protocols_task(&mut self) { + let mut txn_service = self.wallet.transaction_service.clone(); + + task::spawn(async move { + if let Err(e) = txn_service.restart_transaction_protocols().await { + error!(target: LOG_TARGET, "Problem restarting transaction protocols: {}", e); + } + }); + } + pub fn add_notification(&mut self, notification: String) { self.data.notifications.push((Local::now(), notification)); self.data.new_notification_count += 1; @@ -1173,9 +1193,10 @@ pub struct MyIdentity { pub node_id: String, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum UiTransactionSendStatus { Initiated, + Queued, SentDirect, TransactionComplete, DiscoveryInProgress, diff --git a/applications/tari_console_wallet/src/ui/state/tasks.rs b/applications/tari_console_wallet/src/ui/state/tasks.rs index 3d18cbea42..2b581db0e1 100644 --- a/applications/tari_console_wallet/src/ui/state/tasks.rs +++ b/applications/tari_console_wallet/src/ui/state/tasks.rs @@ -23,7 +23,7 @@ use tari_common_types::types::PublicKey; use tari_comms::types::CommsPublicKey; use tari_core::transactions::tari_amount::MicroTari; -use tari_wallet::transaction_service::handle::{TransactionEvent, TransactionServiceHandle}; +use tari_wallet::transaction_service::handle::{TransactionEvent, TransactionSendStatus, TransactionServiceHandle}; use tokio::sync::{broadcast, watch}; use crate::ui::{state::UiTransactionSendStatus, UiError}; @@ -42,8 +42,7 @@ pub async fn send_transaction_task( ) { let _ = result_tx.send(UiTransactionSendStatus::Initiated); let mut event_stream = transaction_service_handle.get_event_stream(); - let mut send_direct_received_result = (false, false); - let mut send_saf_received_result = (false, false); + let mut send_status = TransactionSendStatus::default(); match transaction_service_handle .send_transaction_or_token(public_key, amount, unique_id, parent_public_key, fee_per_gram, message) .await @@ -53,27 +52,18 @@ pub async fn send_transaction_task( }, Ok(our_tx_id) => { loop { - match event_stream.recv().await { + let next_event = event_stream.recv().await; + match next_event { Ok(event) => match &*event { TransactionEvent::TransactionDiscoveryInProgress(tx_id) => { if our_tx_id == *tx_id { let _ = result_tx.send(UiTransactionSendStatus::DiscoveryInProgress); } }, - TransactionEvent::TransactionDirectSendResult(tx_id, result) => { - if our_tx_id == *tx_id { - send_direct_received_result = (true, *result); - if send_saf_received_result.0 { - break; - } - } - }, - TransactionEvent::TransactionStoreForwardSendResult(tx_id, result) => { + TransactionEvent::TransactionSendResult(tx_id, status) => { if our_tx_id == *tx_id { - send_saf_received_result = (true, *result); - if send_direct_received_result.0 { - break; - } + send_status = status.clone(); + break; } }, TransactionEvent::TransactionCompletedImmediately(tx_id) => { @@ -94,10 +84,12 @@ pub async fn send_transaction_task( } } - if send_direct_received_result.1 { + if send_status.direct_send_result { let _ = result_tx.send(UiTransactionSendStatus::SentDirect); - } else if send_saf_received_result.1 { + } else if send_status.store_and_forward_send_result { let _ = result_tx.send(UiTransactionSendStatus::SentViaSaf); + } else if send_status.queued_for_retry { + let _ = result_tx.send(UiTransactionSendStatus::Queued); } else { let _ = result_tx.send(UiTransactionSendStatus::Error( "Transaction could not be sent".to_string(), diff --git a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs index bd5f036e63..c89e68d0bb 100644 --- a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs +++ b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs @@ -86,175 +86,204 @@ impl WalletEventMonitor { info!(target: LOG_TARGET, "Wallet Event Monitor starting"); loop { tokio::select! { - result = transaction_service_events.recv() => { - match result { - Ok(msg) => { - trace!(target: LOG_TARGET, "Wallet Event Monitor received wallet transaction service event {:?}", msg); - self.app_state_inner.write().await.add_event(EventListItem{event_type: "TransactionEvent".to_string(), desc: (&*msg).to_string() }); - match (*msg).clone() { - TransactionEvent::ReceivedFinalizedTransaction(tx_id) => { - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - notifier.transaction_received(tx_id); - self.add_notification(format!("Finalized Transaction Received - TxId: {}", tx_id)).await; - }, - TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations, is_valid: _} | - TransactionEvent::FauxTransactionUnconfirmed{tx_id, num_confirmations, is_valid: _}=> { - self.trigger_confirmations_refresh(tx_id, num_confirmations).await; - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - notifier.transaction_mined_unconfirmed(tx_id, num_confirmations); - self.add_notification(format!("Transaction Mined Unconfirmed with {} confirmations - TxId: {}", num_confirmations, tx_id)).await; - }, - TransactionEvent::TransactionMined{tx_id, is_valid: _} | - TransactionEvent::FauxTransactionConfirmed{tx_id, is_valid: _}=> { - self.trigger_confirmations_cleanup(tx_id).await; - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - notifier.transaction_mined(tx_id); - self.add_notification(format!("Transaction Confirmed - TxId: {}", tx_id)).await; - }, - TransactionEvent::TransactionCancelled(tx_id, _) => { - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - notifier.transaction_cancelled(tx_id); - }, - TransactionEvent::ReceivedTransaction(tx_id) => { - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - self.add_notification(format!("Transaction Received - TxId: {}", tx_id)).await; - }, - TransactionEvent::ReceivedTransactionReply(tx_id) => { - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - self.add_notification(format!("Transaction Reply Received - TxId: {}", tx_id)).await; - }, - TransactionEvent::TransactionBroadcast(tx_id) => { - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - self.add_notification(format!("Transaction Broadcast to Mempool - TxId: {}", tx_id)).await; - }, - TransactionEvent::TransactionMinedRequestTimedOut(tx_id) | - TransactionEvent::TransactionImported(tx_id) => { - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - }, - TransactionEvent::TransactionDirectSendResult(tx_id, true) | - TransactionEvent::TransactionStoreForwardSendResult(tx_id, true) | - TransactionEvent::TransactionCompletedImmediately(tx_id) => { - self.trigger_tx_state_refresh(tx_id).await; - self.trigger_balance_refresh(); - notifier.transaction_sent(tx_id); - }, - TransactionEvent::TransactionValidationStateChanged(_) => { - self.trigger_full_tx_state_refresh().await; - self.trigger_balance_refresh(); - }, - // Only the above variants trigger state refresh - _ => (), - } - }, - Err(broadcast::error::RecvError::Lagged(n)) => { - warn!(target: LOG_TARGET, "Missed {} from Transaction events", n); + result = transaction_service_events.recv() => { + match result { + Ok(msg) => { + trace!( + target: LOG_TARGET, + "Wallet Event Monitor received wallet transaction service event {:?}", + msg + ); + self.app_state_inner.write().await.add_event(EventListItem{ + event_type: "TransactionEvent".to_string(), + desc: (&*msg).to_string() + }); + match (*msg).clone() { + TransactionEvent::ReceivedFinalizedTransaction(tx_id) => { + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + notifier.transaction_received(tx_id); + self.add_notification( + format!("Finalized Transaction Received - TxId: {}", tx_id) + ).await; + }, + TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations, is_valid: _} | + TransactionEvent::FauxTransactionUnconfirmed{tx_id, num_confirmations, is_valid: _}=> { + self.trigger_confirmations_refresh(tx_id, num_confirmations).await; + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + notifier.transaction_mined_unconfirmed(tx_id, num_confirmations); + self.add_notification( + format!( + "Transaction Mined Unconfirmed with {} confirmations - TxId: {}", + num_confirmations, + tx_id + ) + ).await; + }, + TransactionEvent::TransactionMined{tx_id, is_valid: _} | + TransactionEvent::FauxTransactionConfirmed{tx_id, is_valid: _}=> { + self.trigger_confirmations_cleanup(tx_id).await; + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + notifier.transaction_mined(tx_id); + self.add_notification(format!("Transaction Confirmed - TxId: {}", tx_id)).await; + }, + TransactionEvent::TransactionCancelled(tx_id, _) => { + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + notifier.transaction_cancelled(tx_id); + }, + TransactionEvent::ReceivedTransaction(tx_id) => { + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + self.add_notification(format!("Transaction Received - TxId: {}", tx_id)).await; + }, + TransactionEvent::ReceivedTransactionReply(tx_id) => { + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + self.add_notification( + format!("Transaction Reply Received - TxId: {}", tx_id) + ).await; + }, + TransactionEvent::TransactionBroadcast(tx_id) => { + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + self.add_notification( + format!("Transaction Broadcast to Mempool - TxId: {}", tx_id) + ).await; + }, + TransactionEvent::TransactionMinedRequestTimedOut(tx_id) | + TransactionEvent::TransactionImported(tx_id) => { + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + }, + TransactionEvent::TransactionCompletedImmediately(tx_id) => { + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + notifier.transaction_sent_or_queued(tx_id, true); + }, + TransactionEvent::TransactionSendResult(tx_id, status) => { + self.trigger_tx_state_refresh(tx_id).await; + self.trigger_balance_refresh(); + notifier.transaction_sent_or_queued(tx_id, status.direct_send_result || status.store_and_forward_send_result); + }, + TransactionEvent::TransactionValidationStateChanged(_) => { + self.trigger_full_tx_state_refresh().await; + self.trigger_balance_refresh(); + }, + // Only the above variants trigger state refresh + _ => (), } - Err(broadcast::error::RecvError::Closed) => {} - } - }, - Ok(_) = connectivity_status.changed() => { - trace!(target: LOG_TARGET, "Wallet Event Monitor received wallet connectivity status changed"); - self.trigger_peer_state_refresh().await; - }, - Ok(_) = software_update_notif.changed() => { - trace!(target: LOG_TARGET, "Wallet Event Monitor received wallet auto update status changed"); - let update = software_update_notif.borrow().as_ref().cloned(); - if let Some(update) = update { - self.add_notification(format!( - "Version {} of the {} is available: {} (sha: {})", - update.version(), - update.app(), - update.download_url(), - update.to_hash_hex() - )).await; + }, + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!(target: LOG_TARGET, "Missed {} from Transaction events", n); } - }, - result = connectivity_events.recv() => { - match result { - Ok(msg) => { - trace!(target: LOG_TARGET, "Wallet Event Monitor received wallet connectivity event {:?}", msg); - match msg { - ConnectivityEvent::PeerConnected(_) | - ConnectivityEvent::PeerDisconnected(_) => { - self.trigger_peer_state_refresh().await; - }, - // Only the above variants trigger state refresh - _ => (), - } - }, - Err(broadcast::error::RecvError::Lagged(n)) => { - warn!(target: LOG_TARGET, "Missed {} from Connectivity events", n); + Err(broadcast::error::RecvError::Closed) => {} + } + }, + Ok(_) = connectivity_status.changed() => { + trace!(target: LOG_TARGET, "Wallet Event Monitor received wallet connectivity status changed"); + self.trigger_peer_state_refresh().await; + }, + Ok(_) = software_update_notif.changed() => { + trace!(target: LOG_TARGET, "Wallet Event Monitor received wallet auto update status changed"); + let update = software_update_notif.borrow().as_ref().cloned(); + if let Some(update) = update { + self.add_notification(format!( + "Version {} of the {} is available: {} (sha: {})", + update.version(), + update.app(), + update.download_url(), + update.to_hash_hex() + )).await; + } + }, + result = connectivity_events.recv() => { + match result { + Ok(msg) => { + trace!( + target: LOG_TARGET, + "Wallet Event Monitor received wallet connectivity event {:?}", + msg + ); + match msg { + ConnectivityEvent::PeerConnected(_) | + ConnectivityEvent::PeerDisconnected(_) => { + self.trigger_peer_state_refresh().await; + }, + // Only the above variants trigger state refresh + _ => (), } - Err(broadcast::error::RecvError::Closed) => {} - } - }, - _ = base_node_changed.changed() => { - let peer = base_node_changed.borrow().as_ref().cloned(); - if let Some(peer) = peer { - self.trigger_base_node_peer_refresh(peer).await; - self.trigger_balance_refresh(); + }, + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!(target: LOG_TARGET, "Missed {} from Connectivity events", n); } + Err(broadcast::error::RecvError::Closed) => {} } - result = base_node_events.recv() => { - match result { - Ok(msg) => { - trace!(target: LOG_TARGET, "Wallet Event Monitor received base node event {:?}", msg); - if let BaseNodeEvent::BaseNodeStateChanged(state) = (*msg).clone() { - self.trigger_base_node_state_refresh(state).await; - } - }, - Err(broadcast::error::RecvError::Lagged(n)) => { - warn!(target: LOG_TARGET, "Missed {} from Base node Service events", n); + }, + _ = base_node_changed.changed() => { + let peer = base_node_changed.borrow().as_ref().cloned(); + if let Some(peer) = peer { + self.trigger_base_node_peer_refresh(peer).await; + self.trigger_balance_refresh(); + } + } + result = base_node_events.recv() => { + match result { + Ok(msg) => { + trace!(target: LOG_TARGET, "Wallet Event Monitor received base node event {:?}", msg); + if let BaseNodeEvent::BaseNodeStateChanged(state) = (*msg).clone() { + self.trigger_base_node_state_refresh(state).await; } - Err(broadcast::error::RecvError::Closed) => {} + }, + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!(target: LOG_TARGET, "Missed {} from Base node Service events", n); } - }, - result = output_manager_service_events.recv() => { - match result { - Ok(msg) => { - trace!(target: LOG_TARGET, "Output Manager Service Callback Handler event {:?}", msg); - if let OutputManagerEvent::TxoValidationSuccess(_) = &*msg { - self.trigger_balance_refresh(); - } - }, - Err(broadcast::error::RecvError::Lagged(n)) => { - warn!(target: LOG_TARGET, "Missed {} from Output Manager Service events", n); + Err(broadcast::error::RecvError::Closed) => {} + } + }, + result = output_manager_service_events.recv() => { + match result { + Ok(msg) => { + trace!(target: LOG_TARGET, "Output Manager Service Callback Handler event {:?}", msg); + if let OutputManagerEvent::TxoValidationSuccess(_) = &*msg { + self.trigger_balance_refresh(); } - Err(broadcast::error::RecvError::Closed) => {} + }, + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!(target: LOG_TARGET, "Missed {} from Output Manager Service events", n); } - }, - event = contacts_liveness_events.recv() => { - match event { - Ok(liveness_event) => { - match liveness_event.deref() { - ContactsLivenessEvent::StatusUpdated(data) => { - trace!(target: LOG_TARGET, - "Contacts Liveness Service event 'StatusUpdated': {}", - data.clone(), - ); - self.trigger_contacts_refresh().await; - } - ContactsLivenessEvent::NetworkSilence => {} + Err(broadcast::error::RecvError::Closed) => {} + } + }, + event = contacts_liveness_events.recv() => { + match event { + Ok(liveness_event) => { + match liveness_event.deref() { + ContactsLivenessEvent::StatusUpdated(data) => { + trace!(target: LOG_TARGET, + "Contacts Liveness Service event 'StatusUpdated': {}", + data.clone(), + ); + self.trigger_contacts_refresh().await; } + ContactsLivenessEvent::NetworkSilence => {} } - Err(broadcast::error::RecvError::Lagged(n)) => { - warn!(target: LOG_TARGET, "Missed {} from Output Manager Service events", n); - } - Err(broadcast::error::RecvError::Closed) => {} } + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!(target: LOG_TARGET, "Missed {} from Output Manager Service events", n); + } + Err(broadcast::error::RecvError::Closed) => {} } - _ = shutdown_signal.wait() => { - info!(target: LOG_TARGET, "Wallet Event Monitor shutting down because the shutdown signal was received"); - break; - }, + } + _ = shutdown_signal.wait() => { + info!( + target: LOG_TARGET, + "Wallet Event Monitor shutting down because the shutdown signal was received" + ); + break; + }, } } } diff --git a/base_layer/common_types/src/transaction.rs b/base_layer/common_types/src/transaction.rs index 62bff4037b..32a693f61a 100644 --- a/base_layer/common_types/src/transaction.rs +++ b/base_layer/common_types/src/transaction.rs @@ -34,6 +34,8 @@ pub enum TransactionStatus { FauxUnconfirmed, /// All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed FauxConfirmed, + /// This transaction is still being queued for initial sending + Queued, } impl TransactionStatus { @@ -66,6 +68,7 @@ impl TryFrom for TransactionStatus { 7 => Ok(TransactionStatus::Rejected), 8 => Ok(TransactionStatus::FauxUnconfirmed), 9 => Ok(TransactionStatus::FauxConfirmed), + 10 => Ok(TransactionStatus::Queued), code => Err(TransactionConversionError { code }), } } @@ -91,6 +94,7 @@ impl Display for TransactionStatus { TransactionStatus::Rejected => write!(f, "Rejected"), TransactionStatus::FauxUnconfirmed => write!(f, "FauxUnconfirmed"), TransactionStatus::FauxConfirmed => write!(f, "FauxConfirmed"), + TransactionStatus::Queued => write!(f, "Queued"), } } } diff --git a/base_layer/core/src/transactions/transaction_protocol/sender.rs b/base_layer/core/src/transactions/transaction_protocol/sender.rs index 6bf47bb19a..d0f78335b5 100644 --- a/base_layer/core/src/transactions/transaction_protocol/sender.rs +++ b/base_layer/core/src/transactions/transaction_protocol/sender.rs @@ -357,6 +357,17 @@ impl SenderTransactionProtocol { } } + /// Revert the sender state back to 'SingleRoundMessageReady', used if transactions gets queued + pub fn revert_sender_state_to_single_round_message_ready(&mut self) -> Result<(), TPE> { + match &self.state { + SenderState::CollectingSingleSignature(info) => { + self.state = SenderState::SingleRoundMessageReady(info.clone()); + Ok(()) + }, + _ => Err(TPE::InvalidStateError), + } + } + /// Return the single round sender message pub fn get_single_round_message(&self) -> Result { match &self.state { diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 37ea5d1475..08a35083de 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -129,9 +129,6 @@ where node_identity: Arc, key_manager: TKeyManagerInterface, ) -> Result { - // Clear any encumberances for transactions that were being negotiated but did not complete to become official - // Pending Transactions. - db.clear_short_term_encumberances()?; Self::initialise_key_manager(&key_manager).await?; let rewind_key = key_manager .get_key_at_index(OutputManagerKeyManagerBranch::RecoveryViewOnly.get_branch_key(), 0) diff --git a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs index ab174704e8..bd1bf4c178 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs @@ -34,7 +34,7 @@ pub trait OutputManagerBackend: Send + Sync + Clone { /// Retrieve outputs that have been mined but not spent yet (have not been deleted) fn fetch_mined_unspent_outputs(&self) -> Result, OutputManagerStorageError>; /// Retrieve outputs that have not been found or confirmed in the block chain yet - fn fetch_unconfirmed_outputs(&self) -> Result, OutputManagerStorageError>; + fn fetch_unspent_mined_unconfirmed_outputs(&self) -> Result, OutputManagerStorageError>; /// Modify the state the of the backend with a write operation fn write(&self, op: WriteOperation) -> Result, OutputManagerStorageError>; fn fetch_pending_incoming_outputs(&self) -> Result, OutputManagerStorageError>; diff --git a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs index 5676c6e7ce..fc621c1cc7 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs @@ -245,7 +245,7 @@ where T: OutputManagerBackend + 'static } pub fn fetch_unconfirmed_outputs(&self) -> Result, OutputManagerStorageError> { - let utxos = self.db.fetch_unconfirmed_outputs()?; + let utxos = self.db.fetch_unspent_mined_unconfirmed_outputs()?; Ok(utxos) } diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs index 9698abfdb5..3aca42e8ff 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs @@ -346,7 +346,7 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { .collect::, _>>() } - fn fetch_unconfirmed_outputs(&self) -> Result, OutputManagerStorageError> { + fn fetch_unspent_mined_unconfirmed_outputs(&self) -> Result, OutputManagerStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); @@ -357,7 +357,7 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { if start.elapsed().as_millis() > 0 { trace!( target: LOG_TARGET, - "sqlite profile - fetch_unconfirmed_outputs: lock {} + db_op {} = {} ms", + "sqlite profile - fetch_unspent_mined_unconfirmed_outputs: lock {} + db_op {} = {} ms", acquire_lock.as_millis(), (start.elapsed() - acquire_lock).as_millis(), start.elapsed().as_millis() @@ -678,7 +678,8 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { if start.elapsed().as_millis() > 0 { trace!( target: LOG_TARGET, - "sqlite profile - short_term_encumber_outputs: lock {} + db_op {} = {} ms", + "sqlite profile - short_term_encumber_outputs (TxId: {}): lock {} + db_op {} = {} ms", + tx_id, acquire_lock.as_millis(), (start.elapsed() - acquire_lock).as_millis(), start.elapsed().as_millis() @@ -719,7 +720,8 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { if start.elapsed().as_millis() > 0 { trace!( target: LOG_TARGET, - "sqlite profile - confirm_encumbered_outputs: lock {} + db_op {} = {} ms", + "sqlite profile - confirm_encumbered_outputs (TxId: {}): lock {} + db_op {} = {} ms", + tx_id, acquire_lock.as_millis(), (start.elapsed() - acquire_lock).as_millis(), start.elapsed().as_millis() diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 28c5396547..0083f8e446 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -20,7 +20,12 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{collections::HashMap, fmt, fmt::Formatter, sync::Arc}; +use std::{ + collections::HashMap, + fmt, + fmt::{Display, Formatter}, + sync::Arc, +}; use aes_gcm::Aes256Gcm; use tari_common_types::{ @@ -203,6 +208,23 @@ pub enum TransactionServiceResponse { ShaAtomicSwapTransactionSent(Box<(TxId, PublicKey, TransactionOutput)>), } +#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)] +pub struct TransactionSendStatus { + pub direct_send_result: bool, + pub store_and_forward_send_result: bool, + pub queued_for_retry: bool, +} + +impl Display for TransactionSendStatus { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + fmt, + "direct('{}')' saf('{}') queued('{}')", + self.direct_send_result, self.store_and_forward_send_result, self.queued_for_retry, + ) + } +} + /// Events that can be published on the Text Message Service Event Stream #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum TransactionEvent { @@ -211,9 +233,8 @@ pub enum TransactionEvent { ReceivedTransactionReply(TxId), ReceivedFinalizedTransaction(TxId), TransactionDiscoveryInProgress(TxId), - TransactionDirectSendResult(TxId, bool), + TransactionSendResult(TxId, TransactionSendStatus), TransactionCompletedImmediately(TxId), - TransactionStoreForwardSendResult(TxId, bool), TransactionCancelled(TxId, TxCancellationReason), TransactionBroadcast(TxId), TransactionImported(TxId), @@ -260,15 +281,12 @@ impl fmt::Display for TransactionEvent { TransactionEvent::TransactionDiscoveryInProgress(tx) => { write!(f, "TransactionDiscoveryInProgress for {}", tx) }, - TransactionEvent::TransactionDirectSendResult(tx, success) => { - write!(f, "TransactionDirectSendResult for {}: {}", tx, success) + TransactionEvent::TransactionSendResult(tx, status) => { + write!(f, "TransactionSendResult for {}: {}", tx, status) }, TransactionEvent::TransactionCompletedImmediately(tx) => { write!(f, "TransactionCompletedImmediately for {}", tx) }, - TransactionEvent::TransactionStoreForwardSendResult(tx, success) => { - write!(f, "TransactionStoreForwardSendResult for {}:{}", tx, success) - }, TransactionEvent::TransactionCancelled(tx, rejection) => { write!(f, "TransactionCancelled for {}:{:?}", tx, rejection) }, 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 f2fe074aa9..3390cfd46b 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 @@ -59,8 +59,8 @@ use crate::{ transaction_service::{ config::TransactionRoutingMechanism, error::{TransactionServiceError, TransactionServiceProtocolError}, - handle::{TransactionEvent, TransactionServiceResponse}, - service::TransactionServiceResources, + handle::{TransactionEvent, TransactionSendStatus, TransactionServiceResponse}, + service::{TransactionSendResult, TransactionServiceResources}, storage::{ database::TransactionBackend, models::{CompletedTransaction, OutboundTransaction, TxCancellationReason}, @@ -79,6 +79,7 @@ const LOG_TARGET: &str = "wallet::transaction_service::protocols::send_protocol" #[derive(Debug, PartialEq)] pub enum TransactionSendProtocolStage { Initial, + Queued, WaitForReply, } @@ -97,6 +98,7 @@ pub struct TransactionSendProtocol { cancellation_receiver: Option>, prev_header: Option, height: Option, + sender_protocol: Option, } impl TransactionSendProtocol @@ -121,6 +123,7 @@ where stage: TransactionSendProtocolStage, prev_header: Option, height: Option, + sender_protocol: Option, ) -> Self { Self { id, @@ -137,30 +140,66 @@ where stage, prev_header, height, + sender_protocol, } } /// Execute the Transaction Send Protocol as an async task. - pub async fn execute(mut self) -> Result> { + pub async fn execute( + mut self, + ) -> Result> { info!( target: LOG_TARGET, "Starting Transaction Send protocol for TxId: {} at Stage {:?}", self.id, self.stage ); - match self.stage { + let transaction_status = match self.stage { TransactionSendProtocolStage::Initial => { let sender_protocol = self.prepare_transaction().await?; - self.initial_send_transaction(sender_protocol).await?; - self.wait_for_reply().await?; + let status = self.initial_send_transaction(sender_protocol).await?; + if status == TransactionStatus::Pending { + self.wait_for_reply().await?; + } + status + }, + TransactionSendProtocolStage::Queued => { + if let Some(mut sender_protocol) = self.sender_protocol.clone() { + if sender_protocol.is_collecting_single_signature() { + let _ = sender_protocol + .revert_sender_state_to_single_round_message_ready() + .map_err(|e| { + TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)) + })?; + } + let status = self.initial_send_transaction(sender_protocol).await?; + if status == TransactionStatus::Pending { + self.wait_for_reply().await?; + } + status + } else { + error!( + target: LOG_TARGET, + "{:?} not valid; Sender Transaction Protocol does not exist", self.stage + ); + return Err(TransactionServiceProtocolError::new( + self.id, + TransactionServiceError::InvalidStateError, + )); + } }, TransactionSendProtocolStage::WaitForReply => { self.wait_for_reply().await?; + TransactionStatus::Pending }, - } + }; - Ok(self.id) + Ok(TransactionSendResult { + tx_id: self.id, + transaction_status, + }) } + // Prepare transaction to send and encumber the unspent outputs to use as inputs async fn prepare_transaction( &mut self, ) -> Result> { @@ -222,7 +261,7 @@ where async fn initial_send_transaction( &mut self, mut sender_protocol: SenderTransactionProtocol, - ) -> Result<(), TransactionServiceProtocolError> { + ) -> Result> { if !sender_protocol.is_single_round_message_ready() { error!(target: LOG_TARGET, "Sender Transaction Protocol is in an invalid state"); return Err(TransactionServiceProtocolError::new( @@ -231,11 +270,11 @@ where )); } + // Build single round message and advance sender state let msg = sender_protocol .build_single_round_message() .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; let tx_id = msg.tx_id; - if tx_id != self.id { return Err(TransactionServiceProtocolError::new( self.id, @@ -243,18 +282,43 @@ where )); } + // Attempt to send the initial transaction let SendResult { direct_send_result, store_and_forward_send_result, - } = self.send_transaction(msg).await?; + transaction_status, + } = match self.send_transaction(msg).await { + Ok(val) => val, + Err(e) => { + warn!( + target: LOG_TARGET, + "Problem sending Outbound Transaction TxId: {:?}: {:?}", self.id, e + ); + SendResult { + direct_send_result: false, + store_and_forward_send_result: false, + transaction_status: TransactionStatus::Queued, + } + }, + }; - if direct_send_result || store_and_forward_send_result { + // Confirm pending transaction (confirm encumbered outputs) + if transaction_status == TransactionStatus::Pending { self.resources .output_manager_service .confirm_pending_transaction(self.id) .await .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + } + // Add pending outbound transaction if it does not exist + if !self + .resources + .db + .transaction_exists(tx_id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))? + { let fee = sender_protocol .get_fee_amount() .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; @@ -264,21 +328,18 @@ where self.amount, fee, sender_protocol.clone(), - TransactionStatus::Pending, + transaction_status.clone(), self.message.clone(), Utc::now().naive_utc(), direct_send_result, ); - info!( - target: LOG_TARGET, - "Pending Outbound Transaction TxId: {:?} added. Waiting for Reply or Cancellation", self.id, - ); self.resources .db .add_pending_outbound_transaction(outbound_tx.tx_id, outbound_tx) .await .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; - + } + if transaction_status == TransactionStatus::Pending { self.resources .db .increment_send_count(self.id) @@ -286,41 +347,31 @@ where .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; } + // Notify subscribers let _ = self .resources .event_publisher - .send(Arc::new(TransactionEvent::TransactionDirectSendResult( + .send(Arc::new(TransactionEvent::TransactionSendResult( self.id, - direct_send_result, - ))); - let _ = self - .resources - .event_publisher - .send(Arc::new(TransactionEvent::TransactionStoreForwardSendResult( - self.id, - store_and_forward_send_result, + TransactionSendStatus { + direct_send_result, + store_and_forward_send_result, + queued_for_retry: transaction_status == TransactionStatus::Queued, + }, ))); - if !direct_send_result && !store_and_forward_send_result { - error!( + if transaction_status == TransactionStatus::Pending { + info!( target: LOG_TARGET, - "Failed to Send Transaction (TxId: {}) both Directly or via Store and Forward. Pending Transaction \ - will be cancelled", - self.id + "Pending Outbound Transaction TxId: {:?} added. Waiting for Reply or Cancellation", self.id, + ); + } else { + info!( + target: LOG_TARGET, + "Pending Outbound Transaction TxId: {:?} queued. Waiting for wallet to come online", self.id, ); - if let Err(e) = self.resources.output_manager_service.cancel_transaction(self.id).await { - warn!( - target: LOG_TARGET, - "Failed to Cancel TX_ID: {} after failed sending attempt with error {:?}", self.id, e - ); - }; - return Err(TransactionServiceProtocolError::new( - self.id, - TransactionServiceError::OutboundSendFailure, - )); } - - Ok(()) + Ok(transaction_status) } async fn wait_for_reply(&mut self) -> Result<(), TransactionServiceProtocolError> { @@ -345,7 +396,10 @@ where .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; if !outbound_tx.sender_protocol.is_collecting_single_signature() { - error!(target: LOG_TARGET, "Pending Transaction not in correct state"); + error!( + target: LOG_TARGET, + "Pending Transaction (TxId: {}) not in correct state", self.id + ); return Err(TransactionServiceProtocolError::new( self.id, TransactionServiceError::InvalidStateError, @@ -382,7 +436,7 @@ where }; if resend { - if let Err(e) = self + match self .send_transaction( outbound_tx .sender_protocol @@ -391,16 +445,20 @@ where ) .await { - warn!( + Ok(val) => { + if val.transaction_status == TransactionStatus::Pending { + self.resources + .db + .increment_send_count(self.id) + .await + .map_err(|e| TransactionServiceProtocolError::new(self.id, e.into()))? + } + }, + Err(e) => 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, e.into()))?; + ), + }; } let mut shutdown = self.resources.shutdown_signal.clone(); @@ -427,7 +485,10 @@ where result = &mut cancellation_receiver => { if result.is_ok() { 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| { + 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 @@ -437,7 +498,9 @@ where .db .increment_send_count(self.id) .await - .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; + .map_err(|e| TransactionServiceProtocolError::new( + self.id, TransactionServiceError::from(e)) + )?; return Err(TransactionServiceProtocolError::new( self.id, TransactionServiceError::TransactionCancelled, @@ -445,24 +508,37 @@ where } }, () = 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!( + match self.send_transaction( + outbound_tx + .sender_protocol + .get_single_round_message() + .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))? + ).await + { + Ok(val) => if val.transaction_status == TransactionStatus::Pending { + 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 (TxId: {}): {:?}", self.id, e - ); - } else { - self.resources - .db - .increment_send_count(self.id) - .await - .map_err(|e| TransactionServiceProtocolError::new(self.id, TransactionServiceError::from(e)))?; - } + ), + }; }, () = &mut timeout_delay => { return self.timeout_transaction().await; } _ = shutdown.wait() => { - info!(target: LOG_TARGET, "Transaction Send Protocol (id: {}) shutting down because it received the shutdown signal", self.id); + info!( + target: LOG_TARGET, + "Transaction Send Protocol (id: {}) shutting down because it received the shutdown signal", + self.id + ); return Err(TransactionServiceProtocolError::new(self.id, TransactionServiceError::Shutdown)) } } @@ -567,6 +643,7 @@ where let mut result = SendResult { direct_send_result: false, store_and_forward_send_result: false, + transaction_status: TransactionStatus::Queued, }; match self.resources.config.transaction_routing_mechanism { @@ -575,6 +652,9 @@ where }, TransactionRoutingMechanism::StoreAndForwardOnly => { result.store_and_forward_send_result = self.send_transaction_store_and_forward(msg.clone()).await?; + if result.store_and_forward_send_result { + result.transaction_status = TransactionStatus::Pending; + } }, }; @@ -592,6 +672,7 @@ where let proto_message = proto::TransactionSenderMessage::single(msg.clone().into()); let mut store_and_forward_send_result = false; let mut direct_send_result = false; + let mut transaction_status = TransactionStatus::Queued; info!( target: LOG_TARGET, @@ -619,6 +700,7 @@ where .await { direct_send_result = true; + transaction_status = TransactionStatus::Pending; } // Send a Store and Forward (SAF) regardless. Empirical testing determined // that in some cases a direct send would be reported as true, even though the wallet @@ -631,21 +713,54 @@ where self.id, self.dest_pubkey, ); - store_and_forward_send_result = self.send_transaction_store_and_forward(msg.clone()).await?; + match self.send_transaction_store_and_forward(msg.clone()).await { + Ok(res) => { + store_and_forward_send_result = res; + if store_and_forward_send_result { + transaction_status = TransactionStatus::Pending + }; + }, + // Sending SAF is the secondary concern here, so do not propagate the error + Err(e) => warn!(target: LOG_TARGET, "Sending SAF for TxId {} failed ({:?})", self.id, e), + } }, SendMessageResponse::Failed(err) => { warn!( target: LOG_TARGET, "Transaction Send Direct for TxID {} failed: {}", self.id, err ); - store_and_forward_send_result = self.send_transaction_store_and_forward(msg.clone()).await?; + match self.send_transaction_store_and_forward(msg.clone()).await { + Ok(res) => { + store_and_forward_send_result = res; + if store_and_forward_send_result { + transaction_status = TransactionStatus::Pending + }; + }, + // Sending SAF is the secondary concern here, so do not propagate the error + Err(e) => warn!(target: LOG_TARGET, "Sending SAF for TxId {} failed ({:?})", self.id, e), + } }, SendMessageResponse::PendingDiscovery(rx) => { let _ = self .resources .event_publisher .send(Arc::new(TransactionEvent::TransactionDiscoveryInProgress(self.id))); - store_and_forward_send_result = self.send_transaction_store_and_forward(msg.clone()).await?; + match self.send_transaction_store_and_forward(msg.clone()).await { + Ok(res) => { + store_and_forward_send_result = res; + if store_and_forward_send_result { + transaction_status = TransactionStatus::Pending + }; + }, + // Sending SAF is the secondary concern here, so do not propagate the error + Err(e) => warn!( + target: LOG_TARGET, + "Sending SAF for TxId {} failed; we will still wait for discovery of {} to complete ({:?})", + self.id, + self.dest_pubkey, + e + ), + } // now wait for discovery to complete match rx.await { Ok(SendMessageResponse::Queued(send_states)) => { @@ -661,6 +776,9 @@ where self.resources.config.direct_send_timeout, ) .await; + if direct_send_result { + transaction_status = TransactionStatus::Pending + }; }, Ok(SendMessageResponse::Failed(e)) => warn!( target: LOG_TARGET, @@ -670,20 +788,24 @@ where Err(e) => { warn!( target: LOG_TARGET, - "Error waiting for Discovery while sending message to TxId: {} {:?}", self.id, e + "Error waiting for Discovery while sending message (TxId: {}) {:?}", self.id, e ); }, } }, }, Err(e) => { - warn!(target: LOG_TARGET, "Direct Transaction Send failed: {:?}", e); + warn!( + target: LOG_TARGET, + "Direct Transaction Send (TxId: {}) failed: {:?}", self.id, e + ); }, } Ok(SendResult { direct_send_result, store_and_forward_send_result, + transaction_status, }) } @@ -835,4 +957,5 @@ where struct SendResult { direct_send_result: bool, store_and_forward_send_result: bool, + transaction_status: TransactionStatus, } diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 5e85c2d62b..5202220dbc 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -296,7 +296,7 @@ where let mut shutdown = self.resources.shutdown_signal.clone(); let mut send_transaction_protocol_handles: FuturesUnordered< - JoinHandle>>, + JoinHandle>>, > = FuturesUnordered::new(); let mut receive_transaction_protocol_handles: FuturesUnordered< @@ -512,7 +512,10 @@ where Some(join_result) = transaction_validation_protocol_handles.next() => { trace!(target: LOG_TARGET, "Transaction Validation protocol has ended with result {:?}", join_result); match join_result { - Ok(join_result_inner) => self.complete_transaction_validation_protocol(join_result_inner, &mut transaction_broadcast_protocol_handles,).await, + Ok(join_result_inner) => self.complete_transaction_validation_protocol( + join_result_inner, + &mut transaction_broadcast_protocol_handles, + ).await, Err(e) => error!(target: LOG_TARGET, "Error resolving Transaction Validation protocol: {:?}", e), }; } @@ -531,7 +534,7 @@ where &mut self, request: TransactionServiceRequest, send_transaction_join_handles: &mut FuturesUnordered< - JoinHandle>>, + JoinHandle>>, >, receive_transaction_join_handles: &mut FuturesUnordered< JoinHandle>>, @@ -794,7 +797,9 @@ where parent_public_key: Option, fee_per_gram: MicroTari, message: String, - join_handles: &mut FuturesUnordered>>>, + join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, transaction_broadcast_join_handles: &mut FuturesUnordered< JoinHandle>>, >, @@ -877,6 +882,7 @@ where TransactionSendProtocolStage::Initial, None, self.last_seen_tip_height, + None, ); let join_handle = tokio::spawn(protocol.execute()); @@ -1339,40 +1345,44 @@ where /// Handle the final clean up after a Send Transaction protocol completes async fn complete_send_transaction_protocol( &mut self, - join_result: Result>, + join_result: Result>, transaction_broadcast_join_handles: &mut FuturesUnordered< JoinHandle>>, >, ) { match join_result { - Ok(id) => { - let _ = self.pending_transaction_reply_senders.remove(&id); - let _ = self.send_transaction_cancellation_senders.remove(&id); - let completed_tx = match self.db.get_completed_transaction(id).await { - Ok(v) => v, - Err(e) => { - error!( - target: LOG_TARGET, - "Error starting Broadcast Protocol after completed Send Transaction Protocol : {:?}", e - ); - return; - }, - }; - let _ = self - .broadcast_completed_transaction(completed_tx, transaction_broadcast_join_handles) - .await - .map_err(|resp| { - error!( - target: LOG_TARGET, - "Error starting Broadcast Protocol after completed Send Transaction Protocol : {:?}", resp - ); - resp - }); - trace!( - target: LOG_TARGET, - "Send Transaction Protocol for TxId: {} completed successfully", - id - ); + Ok(val) => { + if val.transaction_status != TransactionStatus::Queued { + let _ = self.pending_transaction_reply_senders.remove(&val.tx_id); + let _ = self.send_transaction_cancellation_senders.remove(&val.tx_id); + let completed_tx = match self.db.get_completed_transaction(val.tx_id).await { + Ok(v) => v, + Err(e) => { + error!( + target: LOG_TARGET, + "Error starting Broadcast Protocol after completed Send Transaction Protocol : {:?}", e + ); + return; + }, + }; + let _ = self + .broadcast_completed_transaction(completed_tx, transaction_broadcast_join_handles) + .await + .map_err(|resp| { + error!( + target: LOG_TARGET, + "Error starting Broadcast Protocol after completed Send Transaction Protocol : {:?}", + resp + ); + resp + }); + } else if val.transaction_status == TransactionStatus::Queued { + trace!( + target: LOG_TARGET, + "Send Transaction Protocol for TxId: {} not completed successfully, transaction Queued", + val.tx_id + ); + } }, Err(TransactionServiceProtocolError { id, error }) => { let _ = self.pending_transaction_reply_senders.remove(&id); @@ -1461,20 +1471,43 @@ where #[allow(clippy::map_entry)] async fn restart_all_send_transaction_protocols( &mut self, - join_handles: &mut FuturesUnordered>>>, + join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, ) -> Result<(), TransactionServiceError> { let outbound_txs = self.db.get_pending_outbound_transactions().await?; for (tx_id, tx) in outbound_txs { - if !self.pending_transaction_reply_senders.contains_key(&tx_id) { + let (sender_protocol, stage) = if tx.send_count > 0 { + (None, TransactionSendProtocolStage::WaitForReply) + } else { + (Some(tx.sender_protocol), TransactionSendProtocolStage::Queued) + }; + let (not_yet_pending, queued) = ( + !self.pending_transaction_reply_senders.contains_key(&tx_id), + stage == TransactionSendProtocolStage::Queued, + ); + + if not_yet_pending { debug!( target: LOG_TARGET, "Restarting listening for Reply for Pending Outbound Transaction TxId: {}", tx_id ); + } else if queued { + debug!( + target: LOG_TARGET, + "Retry sending queued Pending Outbound Transaction TxId: {}", tx_id + ); + let _ = self.pending_transaction_reply_senders.remove(&tx_id); + let _ = self.send_transaction_cancellation_senders.remove(&tx_id); + } + + if not_yet_pending || queued { let (tx_reply_sender, tx_reply_receiver) = mpsc::channel(100); let (cancellation_sender, cancellation_receiver) = oneshot::channel(); self.pending_transaction_reply_senders.insert(tx_id, tx_reply_sender); self.send_transaction_cancellation_senders .insert(tx_id, cancellation_sender); + let protocol = TransactionSendProtocol::new( tx_id, self.resources.clone(), @@ -1487,9 +1520,10 @@ where tx.fee, tx.message, None, - TransactionSendProtocolStage::WaitForReply, + stage, None, self.last_seen_tip_height, + sender_protocol, ); let join_handle = tokio::spawn(protocol.execute()); @@ -1810,7 +1844,7 @@ where async fn restart_transaction_negotiation_protocols( &mut self, send_transaction_join_handles: &mut FuturesUnordered< - JoinHandle>>, + JoinHandle>>, >, receive_transaction_join_handles: &mut FuturesUnordered< JoinHandle>>, @@ -2126,8 +2160,14 @@ where "Launch the transaction broadcast protocol for submitted transaction ({}).", tx_id ); - self.complete_send_transaction_protocol(Ok(tx_id), transaction_broadcast_join_handles) - .await; + self.complete_send_transaction_protocol( + Ok(TransactionSendResult { + tx_id, + transaction_status: TransactionStatus::Completed, + }), + transaction_broadcast_join_handles, + ) + .await; Ok(()) } @@ -2299,3 +2339,10 @@ pub struct PendingCoinbaseSpendingKey { fn hash_secret_key(key: &PrivateKey) -> Vec { HashDigest::new().chain(key.as_bytes()).finalize().to_vec() } + +/// Contains the generated TxId and TransactionStatus transaction send result +#[derive(Debug)] +pub struct TransactionSendResult { + pub tx_id: TxId, + pub transaction_status: TransactionStatus, +} diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index 14b6bee1d8..12ca01a069 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -1051,7 +1051,7 @@ async fn test_get_balance() { } #[tokio::test] -async fn sending_transaction_with_short_term_clear() { +async fn sending_transaction_persisted_while_offline() { let factories = CryptoFactories::default(); let (connection, _tempdir) = get_temp_sqlite_database_connection(); @@ -1060,15 +1060,18 @@ async fn sending_transaction_with_short_term_clear() { let mut oms = setup_output_manager_service(backend.clone(), ks_backend.clone(), true).await; - let available_balance = 10_000 * uT; - let (_ti, uo) = make_input(&mut OsRng.clone(), available_balance, &factories.commitment, None).await; + let available_balance = 20_000 * uT; + let (_ti, uo) = make_input(&mut OsRng.clone(), available_balance / 2, &factories.commitment, None).await; + oms.output_manager_handle.add_output(uo, None).await.unwrap(); + let (_ti, uo) = make_input(&mut OsRng.clone(), available_balance / 2, &factories.commitment, None).await; oms.output_manager_handle.add_output(uo, None).await.unwrap(); let balance = oms.output_manager_handle.get_balance().await.unwrap(); assert_eq!(balance.available_balance, available_balance); assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); + assert_eq!(balance.pending_outgoing_balance, MicroTari::from(0)); - // Check that funds are encumbered and then unencumbered if the pending tx is not confirmed before restart + // Check that funds are encumbered and stay encumbered if the pending tx is not confirmed before restart let _stp = oms .output_manager_handle .prepare_transaction_to_send( @@ -1086,16 +1089,18 @@ async fn sending_transaction_with_short_term_clear() { .unwrap(); let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!(balance.available_balance, MicroTari::from(0)); + assert_eq!(balance.available_balance, available_balance / 2); assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); - assert_eq!(balance.pending_outgoing_balance, available_balance); + assert_eq!(balance.pending_outgoing_balance, available_balance / 2); + // This simulates an offline wallet with a queued transaction that has not been sent to the receiving wallet yet drop(oms.output_manager_handle); let mut oms = setup_output_manager_service(backend.clone(), ks_backend.clone(), true).await; let balance = oms.output_manager_handle.get_balance().await.unwrap(); - assert_eq!(balance.available_balance, available_balance); + assert_eq!(balance.available_balance, available_balance / 2); assert_eq!(balance.time_locked_balance.unwrap(), MicroTari::from(0)); + assert_eq!(balance.pending_outgoing_balance, available_balance / 2); // Check that is the pending tx is confirmed that the encumberance persists after restart let stp = oms diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index db25d0f9b1..96a726f491 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -129,7 +129,7 @@ use tari_wallet::{ transaction_service::{ config::TransactionServiceConfig, error::TransactionServiceError, - handle::{TransactionEvent, TransactionServiceHandle}, + handle::{TransactionEvent, TransactionSendStatus, TransactionServiceHandle}, service::TransactionService, storage::{ database::{DbKeyValuePair, TransactionBackend, TransactionDatabase, WriteOperation}, @@ -1856,9 +1856,9 @@ fn discovery_async_return_test() { loop { tokio::select! { event = alice_event_stream.recv() => { - if let TransactionEvent::TransactionDirectSendResult(tx_id, result) = (*event.unwrap()).clone() { + if let TransactionEvent::TransactionSendResult(tx_id, status) = (*event.unwrap()).clone() { txid = tx_id; - is_success = result; + is_success = status.direct_send_result; break; } }, @@ -1889,8 +1889,8 @@ fn discovery_async_return_test() { loop { tokio::select! { event = alice_event_stream.recv() => { - if let TransactionEvent::TransactionDirectSendResult(tx_id, success) = &*event.unwrap() { - success_result = *success; + if let TransactionEvent::TransactionSendResult(tx_id, status) = &*event.unwrap() { + success_result = status.direct_send_result; success_tx_id = *tx_id; break; } @@ -2162,8 +2162,8 @@ fn test_transaction_cancellation() { loop { tokio::select! { event = alice_event_stream.recv() => { - if let TransactionEvent::TransactionStoreForwardSendResult(_,_) = &*event.unwrap() { - break; + if let TransactionEvent::TransactionSendResult(_,_) = &*event.unwrap() { + break; } }, () = &mut delay => { @@ -2804,22 +2804,16 @@ fn test_tx_direct_send_behaviour() { "Testing Message1".to_string(), )) .unwrap(); + let mut transaction_send_status = TransactionSendStatus::default(); runtime.block_on(async { let delay = sleep(Duration::from_secs(60)); tokio::pin!(delay); - let mut direct_count = 0; - let mut saf_count = 0; loop { tokio::select! { event = alice_event_stream.recv() => { - match &*event.unwrap() { - TransactionEvent::TransactionDirectSendResult(_, result) => if !result { direct_count+=1 }, - TransactionEvent::TransactionStoreForwardSendResult(_, result) => if !result { saf_count+=1}, - _ => (), - } - - if direct_count == 1 && saf_count == 1 { + if let TransactionEvent::TransactionSendResult(_, status) = &*event.unwrap() { + transaction_send_status = status.clone(); break; } }, @@ -2828,8 +2822,12 @@ fn test_tx_direct_send_behaviour() { }, } } - assert_eq!(direct_count, 1, "Should be 1 failed direct"); - assert_eq!(saf_count, 1, "Should be 1 failed saf"); + assert!(!transaction_send_status.direct_send_result, "Should be 1 failed direct"); + assert!( + !transaction_send_status.store_and_forward_send_result, + "Should be 1 failed saf" + ); + assert!(transaction_send_status.queued_for_retry, "Should be 1 queued"); }); alice_ts_interface @@ -2856,18 +2854,11 @@ fn test_tx_direct_send_behaviour() { runtime.block_on(async { let delay = sleep(Duration::from_secs(60)); tokio::pin!(delay); - let mut direct_count = 0; - let mut saf_count = 0; loop { tokio::select! { event = alice_event_stream.recv() => { - match &*event.unwrap() { - TransactionEvent::TransactionDirectSendResult(_, result) => if !result { direct_count+=1 }, - TransactionEvent::TransactionStoreForwardSendResult(_, result) => if *result { saf_count+=1 }, - _ => (), - } - - if direct_count == 1 && saf_count == 1 { + if let TransactionEvent::TransactionSendResult(_, status) = &*event.unwrap() { + transaction_send_status = status.clone(); break; } }, @@ -2876,15 +2867,19 @@ fn test_tx_direct_send_behaviour() { }, } } - assert_eq!(direct_count, 1, "Should be 1 failed direct"); - assert_eq!(saf_count, 1, "Should be 1 succeeded saf"); + assert!(!transaction_send_status.direct_send_result, "Should be 1 failed direct"); + assert!( + transaction_send_status.store_and_forward_send_result, + "Should be 1 succeed saf" + ); + assert!(!transaction_send_status.queued_for_retry, "Should be 0 queued"); }); alice_ts_interface .outbound_service_mock_state .set_behaviour(MockBehaviour { direct: ResponseType::QueuedSuccessDelay(Duration::from_secs(1)), - broadcast: ResponseType::Queued, + broadcast: ResponseType::QueuedFail, }); let _tx_id = runtime @@ -2904,17 +2899,11 @@ fn test_tx_direct_send_behaviour() { runtime.block_on(async { let delay = sleep(Duration::from_secs(60)); tokio::pin!(delay); - let mut direct_count = 0; loop { tokio::select! { event = alice_event_stream.recv() => { - match &*event.unwrap() { - TransactionEvent::TransactionDirectSendResult(_, result) => if *result { direct_count+=1 }, - TransactionEvent::TransactionStoreForwardSendResult(_, _) => panic!("Should be no SAF messages"), - _ => (), - } - - if direct_count >= 1 { + if let TransactionEvent::TransactionSendResult(_, status) = &*event.unwrap() { + transaction_send_status = status.clone(); break; } }, @@ -2923,7 +2912,12 @@ fn test_tx_direct_send_behaviour() { }, } } - assert_eq!(direct_count, 1, "Should be 1 succeeded direct"); + assert!(transaction_send_status.direct_send_result, "Should be 1 succeed direct"); + assert!( + !transaction_send_status.store_and_forward_send_result, + "Should be 1 failed saf" + ); + assert!(!transaction_send_status.queued_for_retry, "Should be 0 queued"); }); alice_ts_interface @@ -2949,17 +2943,12 @@ fn test_tx_direct_send_behaviour() { runtime.block_on(async { let delay = sleep(Duration::from_secs(60)); -tokio::pin!(delay); - let mut saf_count = 0; + tokio::pin!(delay); loop { tokio::select! { event = alice_event_stream.recv() => { - match &*event.unwrap() { - TransactionEvent::TransactionStoreForwardSendResult(_, result) => if *result { saf_count+=1}, - TransactionEvent::TransactionDirectSendResult(_, result) => if *result { panic!("Should be no direct messages") }, _ => (), - } - - if saf_count >= 1 { + if let TransactionEvent::TransactionSendResult(_, status) = &*event.unwrap() { + transaction_send_status = status.clone(); break; } }, @@ -2968,7 +2957,12 @@ tokio::pin!(delay); }, } } - assert_eq!(saf_count, 1, "Should be 1 succeeded saf"); + assert!(!transaction_send_status.direct_send_result, "Should be 1 failed direct"); + assert!( + transaction_send_status.store_and_forward_send_result, + "Should be 1 succeed saf" + ); + assert!(!transaction_send_status.queued_for_retry, "Should be 0 queued"); }); } diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index 9c83275906..604b5abb60 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -69,7 +69,7 @@ use tari_wallet::{ service::Balance, }, transaction_service::{ - handle::{TransactionEvent, TransactionEventReceiver}, + handle::{TransactionEvent, TransactionEventReceiver, TransactionSendStatus}, storage::{ database::{TransactionBackend, TransactionDatabase}, models::{CompletedTransaction, InboundTransaction}, @@ -91,8 +91,7 @@ where TBackend: TransactionBackend + 'static callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut CompletedTransaction), callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), - callback_direct_send_result: unsafe extern "C" fn(u64, bool), - callback_store_and_forward_send_result: unsafe extern "C" fn(u64, bool), + callback_transaction_send_result: unsafe extern "C" fn(u64, *mut TransactionSendStatus), callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_txo_validation_complete: unsafe extern "C" fn(u64, bool), callback_contacts_liveness_data_updated: unsafe extern "C" fn(*mut ContactsLivenessData), @@ -134,8 +133,7 @@ where TBackend: TransactionBackend + 'static callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut CompletedTransaction), callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), - callback_direct_send_result: unsafe extern "C" fn(u64, bool), - callback_store_and_forward_send_result: unsafe extern "C" fn(u64, bool), + callback_transaction_send_result: unsafe extern "C" fn(u64, *mut TransactionSendStatus), callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_txo_validation_complete: unsafe extern "C" fn(u64, bool), callback_contacts_liveness_data_updated: unsafe extern "C" fn(*mut ContactsLivenessData), @@ -178,11 +176,7 @@ where TBackend: TransactionBackend + 'static ); info!( target: LOG_TARGET, - "DirectSendResultCallback -> Assigning Fn: {:?}", callback_direct_send_result - ); - info!( - target: LOG_TARGET, - "StoreAndForwardSendResultCallback -> Assigning Fn: {:?}", callback_store_and_forward_send_result + "TransactionSendResultCallback -> Assigning Fn: {:?}", callback_transaction_send_result ); info!( target: LOG_TARGET, @@ -222,8 +216,7 @@ where TBackend: TransactionBackend + 'static callback_transaction_mined_unconfirmed, callback_faux_transaction_confirmed, callback_faux_transaction_unconfirmed, - callback_direct_send_result, - callback_store_and_forward_send_result, + callback_transaction_send_result, callback_transaction_cancellation, callback_txo_validation_complete, callback_contacts_liveness_data_updated, @@ -271,12 +264,8 @@ where TBackend: TransactionBackend + 'static self.receive_finalized_transaction_event(tx_id).await; self.trigger_balance_refresh().await; }, - TransactionEvent::TransactionDirectSendResult(tx_id, result) => { - self.receive_direct_send_result(tx_id, result); - self.trigger_balance_refresh().await; - }, - TransactionEvent::TransactionStoreForwardSendResult(tx_id, result) => { - self.receive_store_and_forward_send_result(tx_id, result); + TransactionEvent::TransactionSendResult(tx_id, status) => { + self.receive_transaction_send_result(tx_id, status); self.trigger_balance_refresh().await; }, TransactionEvent::TransactionCancelled(tx_id, reason) => { @@ -476,23 +465,14 @@ where TBackend: TransactionBackend + 'static } } - fn receive_direct_send_result(&mut self, tx_id: TxId, result: bool) { - debug!( - target: LOG_TARGET, - "Calling Direct Send Result callback function for TxId: {} with result {}", tx_id, result - ); - unsafe { - (self.callback_direct_send_result)(tx_id.as_u64(), result); - } - } - - fn receive_store_and_forward_send_result(&mut self, tx_id: TxId, result: bool) { + fn receive_transaction_send_result(&mut self, tx_id: TxId, status: TransactionSendStatus) { debug!( target: LOG_TARGET, - "Calling Store and Forward Send Result callback function for TxId: {} with result {}", tx_id, result + "Calling Transaction Send Result callback function for TxId: {} with result {}", tx_id, status ); + let boxing = Box::into_raw(Box::new(status)); unsafe { - (self.callback_store_and_forward_send_result)(tx_id.as_u64(), result); + (self.callback_transaction_send_result)(tx_id.as_u64(), boxing); } } diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index f19c09bd88..47a0dfd4c6 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -57,7 +57,7 @@ mod test { }, test_utils::make_wallet_database_connection, transaction_service::{ - handle::TransactionEvent, + handle::{TransactionEvent, TransactionSendStatus}, storage::{ database::TransactionDatabase, models::{CompletedTransaction, InboundTransaction, OutboundTransaction, TxCancellationReason}, @@ -83,8 +83,9 @@ mod test { pub mined_tx_unconfirmed_callback_called: u64, pub faux_tx_confirmed_callback_called: bool, pub faux_tx_unconfirmed_callback_called: u64, - pub direct_send_callback_called: bool, - pub store_and_forward_send_callback_called: bool, + pub direct_send_callback_called: u32, + pub store_and_forward_send_callback_called: u32, + pub transaction_queued_for_retry_callback_called: u32, pub tx_cancellation_callback_called_completed: bool, pub tx_cancellation_callback_called_inbound: bool, pub tx_cancellation_callback_called_outbound: bool, @@ -107,8 +108,9 @@ mod test { mined_tx_unconfirmed_callback_called: 0, faux_tx_confirmed_callback_called: false, faux_tx_unconfirmed_callback_called: 0, - direct_send_callback_called: false, - store_and_forward_send_callback_called: false, + direct_send_callback_called: 0, + store_and_forward_send_callback_called: 0, + transaction_queued_for_retry_callback_called: 0, callback_txo_validation_complete: 0, callback_contacts_liveness_data_updated: 0, callback_balance_updated: 0, @@ -182,15 +184,17 @@ mod test { Box::from_raw(tx); } - unsafe extern "C" fn direct_send_callback(_tx_id: u64, _result: bool) { + unsafe extern "C" fn transaction_send_result_callback(_tx_id: u64, status: *mut TransactionSendStatus) { let mut lock = CALLBACK_STATE.lock().unwrap(); - lock.direct_send_callback_called = true; - drop(lock); - } - - unsafe extern "C" fn store_and_forward_send_callback(_tx_id: u64, _result: bool) { - let mut lock = CALLBACK_STATE.lock().unwrap(); - lock.store_and_forward_send_callback_called = true; + if (*status).direct_send_result { + lock.direct_send_callback_called += 1; + }; + if (*status).store_and_forward_send_result { + lock.store_and_forward_send_callback_called += 1; + }; + if (*status).queued_for_retry { + lock.transaction_queued_for_retry_callback_called += 1; + }; drop(lock); } @@ -419,8 +423,7 @@ mod test { mined_unconfirmed_callback, faux_confirmed_callback, faux_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -510,16 +513,35 @@ mod test { .unwrap(); transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionDirectSendResult( + .send(Arc::new(TransactionEvent::TransactionSendResult( + 2u64.into(), + TransactionSendStatus { + direct_send_result: true, + store_and_forward_send_result: true, + queued_for_retry: false, + }, + ))) + .unwrap(); + + transaction_event_sender + .send(Arc::new(TransactionEvent::TransactionSendResult( 2u64.into(), - true, + TransactionSendStatus { + direct_send_result: false, + store_and_forward_send_result: false, + queued_for_retry: true, + }, ))) .unwrap(); transaction_event_sender - .send(Arc::new(TransactionEvent::TransactionStoreForwardSendResult( + .send(Arc::new(TransactionEvent::TransactionSendResult( 2u64.into(), - true, + TransactionSendStatus { + direct_send_result: false, + store_and_forward_send_result: true, + queued_for_retry: false, + }, ))) .unwrap(); @@ -707,8 +729,9 @@ mod test { assert_eq!(lock.mined_tx_unconfirmed_callback_called, 22u64); assert!(lock.faux_tx_confirmed_callback_called); assert_eq!(lock.faux_tx_unconfirmed_callback_called, 2u64); - assert!(lock.direct_send_callback_called); - assert!(lock.store_and_forward_send_callback_called); + assert_eq!(lock.direct_send_callback_called, 1); + assert_eq!(lock.store_and_forward_send_callback_called, 2); + assert_eq!(lock.transaction_queued_for_retry_callback_called, 1); assert!(lock.tx_cancellation_callback_called_inbound); assert!(lock.tx_cancellation_callback_called_completed); assert!(lock.tx_cancellation_callback_called_outbound); diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 7e2a712f5c..0b3d004360 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -192,6 +192,7 @@ pub struct TariContacts(Vec); pub type TariContact = tari_wallet::contacts_service::storage::database::Contact; pub type TariCompletedTransaction = tari_wallet::transaction_service::storage::models::CompletedTransaction; +pub type TariTransactionSendStatus = tari_wallet::transaction_service::handle::TransactionSendStatus; pub type TariContactsLivenessData = tari_wallet::contacts_service::handle::ContactsLivenessData; pub type TariBalance = tari_wallet::output_manager_service::service::Balance; pub type TariMnemonicLanguage = tari_key_manager::mnemonic::MnemonicLanguage; @@ -2833,6 +2834,79 @@ pub unsafe extern "C" fn pending_inbound_transaction_destroy(transaction: *mut T /// -------------------------------------------------------------------------------------------- /// +/// ----------------------------------- Transport Send Status -----------------------------------/// + +/// Encode the transaction send status of a TariTransactionSendStatus +/// +/// ## Arguments +/// `status` - The pointer to a TariTransactionSendStatus +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `c_uint` - Returns +/// !direct_send & !saf_send & queued = 0 +/// direct_send & saf_send & !queued = 1 +/// direct_send & !saf_send & !queued = 2 +/// !direct_send & saf_send & !queued = 3 +/// any other combination (is not valid) = 4 +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn transaction_send_status_decode( + status: *const TariTransactionSendStatus, + error_out: *mut c_int, +) -> c_uint { + let mut error = 0; + ptr::swap(error_out, &mut error as *mut c_int); + let mut send_status = 4; + if status.is_null() { + error = LibWalletError::from(InterfaceError::NullError("transaction send status".to_string())).code; + ptr::swap(error_out, &mut error as *mut c_int); + } else if ((*status).direct_send_result || (*status).store_and_forward_send_result) && (*status).queued_for_retry { + error = LibWalletError::from(InterfaceError::NullError( + "transaction send status - not valid".to_string(), + )) + .code; + ptr::swap(error_out, &mut error as *mut c_int); + } else if (*status).queued_for_retry { + send_status = 0; + } else if (*status).direct_send_result && (*status).store_and_forward_send_result { + send_status = 1; + } else if (*status).direct_send_result && !(*status).store_and_forward_send_result { + send_status = 2; + } else if !(*status).direct_send_result && (*status).store_and_forward_send_result { + send_status = 3; + } else { + error = LibWalletError::from(InterfaceError::NullError( + "transaction send status - not valid".to_string(), + )) + .code; + ptr::swap(error_out, &mut error as *mut c_int); + } + send_status +} + +/// Frees memory for a TariTransactionSendStatus +/// +/// ## Arguments +/// `status` - The pointer to a TariPendingInboundTransaction +/// +/// ## Returns +/// `()` - Does not return a value, equivalent to void in C +/// +/// # Safety +/// None +#[no_mangle] +pub unsafe extern "C" fn transaction_send_status_destroy(status: *mut TariTransactionSendStatus) { + if !status.is_null() { + Box::from_raw(status); + } +} + +/// -------------------------------------------------------------------------------------------- /// + /// ----------------------------------- Transport Types -----------------------------------------/// /// Creates a memory transport type @@ -3435,9 +3509,11 @@ unsafe fn init_logging( /// `seed_words` - An optional instance of TariSeedWords, used to create a wallet for recovery purposes. /// If this is null, then a new master key is created for the wallet. /// `callback_received_transaction` - The callback function pointer matching the function signature. This will be -/// called when an inbound transaction is received. `callback_received_transaction_reply` - The callback function +/// called when an inbound transaction is received. +/// `callback_received_transaction_reply` - The callback function /// pointer matching the function signature. This will be called when a reply is received for a pending outbound -/// transaction `callback_received_finalized_transaction` - The callback function pointer matching the function +/// transaction +/// `callback_received_finalized_transaction` - The callback function pointer matching the function /// signature. This will be called when a Finalized version on an Inbound transaction is received /// `callback_transaction_broadcast` - The callback function pointer matching the function signature. This will be /// called when a Finalized transaction is detected a Broadcast to a base node mempool. @@ -3449,12 +3525,15 @@ unsafe fn init_logging( /// called when a one-sided transaction is detected as mined AND confirmed. /// `callback_faux_transaction_unconfirmed` - The callback function pointer matching the function signature. This /// will be called when a one-sided transaction is detected as mined but not yet confirmed. -/// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called -/// when a direct send is completed. The first parameter is the transaction id and the second is whether if was -/// successful or not. -/// `callback_store_and_forward_send_result` - The callback function pointer matching the function -/// signature. This is called when a direct send is completed. The first parameter is the transaction id and the second -/// is whether if was successful or not. +/// `callback_transaction_send_result` - The callback function pointer matching the function signature. This is called +/// when a transaction send is completed. The first parameter is the transaction id and the second contains the +/// transaction send status, weather it was send direct and/or send via saf on the one hand or queued for further retry +/// sending on the other hand. +/// !direct_send & !saf_send & queued = 0 +/// direct_send & saf_send & !queued = 1 +/// direct_send & !saf_send & !queued = 2 +/// !direct_send & saf_send & !queued = 3 +/// any other combination (is not valid) = 4 /// `callback_transaction_cancellation` - The callback function pointer matching /// the function signature. This is called when a transaction is cancelled. The first parameter is a pointer to the /// cancelled transaction, the second is a reason as to why said transaction failed that is mapped to the @@ -3514,8 +3593,7 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), - callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), - callback_store_and_forward_send_result: unsafe extern "C" fn(c_ulonglong, bool), + callback_transaction_send_result: unsafe extern "C" fn(c_ulonglong, *mut TariTransactionSendStatus), callback_transaction_cancellation: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), callback_txo_validation_complete: unsafe extern "C" fn(u64, bool), callback_contacts_liveness_data_updated: unsafe extern "C" fn(*mut TariContactsLivenessData), @@ -3727,8 +3805,7 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_mined_unconfirmed, callback_faux_transaction_confirmed, callback_faux_transaction_unconfirmed, - callback_direct_send_result, - callback_store_and_forward_send_result, + callback_transaction_send_result, callback_transaction_cancellation, callback_txo_validation_complete, callback_contacts_liveness_data_updated, @@ -6351,7 +6428,10 @@ mod test { use tari_core::transactions::test_helpers::{create_unblinded_output, TestParams}; use tari_key_manager::{mnemonic::MnemonicLanguage, mnemonic_wordlists}; use tari_test_utils::random; - use tari_wallet::storage::sqlite_utilities::run_migration_and_create_sqlite_connection; + use tari_wallet::{ + storage::sqlite_utilities::run_migration_and_create_sqlite_connection, + transaction_service::handle::TransactionSendStatus, + }; use tempfile::tempdir; use crate::*; @@ -6371,8 +6451,7 @@ mod test { pub mined_tx_unconfirmed_callback_called: bool, pub scanned_tx_callback_called: bool, pub scanned_tx_unconfirmed_callback_called: bool, - pub direct_send_callback_called: bool, - pub store_and_forward_send_callback_called: bool, + pub transaction_send_result_callback: bool, pub tx_cancellation_callback_called: bool, pub callback_txo_validation_complete: bool, pub callback_contacts_liveness_data_updated: bool, @@ -6391,8 +6470,7 @@ mod test { mined_tx_unconfirmed_callback_called: false, scanned_tx_callback_called: false, scanned_tx_unconfirmed_callback_called: false, - direct_send_callback_called: false, - store_and_forward_send_callback_called: false, + transaction_send_result_callback: false, tx_cancellation_callback_called: false, callback_txo_validation_complete: false, callback_contacts_liveness_data_updated: false, @@ -6541,12 +6619,13 @@ mod test { completed_transaction_destroy(tx); } - unsafe extern "C" fn direct_send_callback(_tx_id: c_ulonglong, _result: bool) { - // assert!(true); //optimized out by compiler - } - - unsafe extern "C" fn store_and_forward_send_callback(_tx_id: c_ulonglong, _result: bool) { - // assert!(true); //optimized out by compiler + unsafe extern "C" fn transaction_send_result_callback(_tx_id: c_ulonglong, status: *mut TransactionSendStatus) { + assert!(!status.is_null()); + assert_eq!( + type_of((*status).clone()), + std::any::type_name::() + ); + transaction_send_status_destroy(status); } unsafe extern "C" fn tx_cancellation_callback(tx: *mut TariCompletedTransaction, _reason: u64) { @@ -6662,6 +6741,94 @@ mod test { } } + #[test] + fn test_transaction_send_status() { + unsafe { + let mut error = 0; + let error_ptr = &mut error as *mut c_int; + + let status = Box::into_raw(Box::new(TariTransactionSendStatus { + direct_send_result: false, + store_and_forward_send_result: false, + queued_for_retry: true, + })); + let transaction_status = transaction_send_status_decode(status, error_ptr); + transaction_send_status_destroy(status); + assert_eq!(error, 0); + assert_eq!(transaction_status, 0); + + let status = Box::into_raw(Box::new(TariTransactionSendStatus { + direct_send_result: true, + store_and_forward_send_result: true, + queued_for_retry: false, + })); + let transaction_status = transaction_send_status_decode(status, error_ptr); + transaction_send_status_destroy(status); + assert_eq!(error, 0); + assert_eq!(transaction_status, 1); + + let status = Box::into_raw(Box::new(TariTransactionSendStatus { + direct_send_result: true, + store_and_forward_send_result: false, + queued_for_retry: false, + })); + let transaction_status = transaction_send_status_decode(status, error_ptr); + transaction_send_status_destroy(status); + assert_eq!(error, 0); + assert_eq!(transaction_status, 2); + + let status = Box::into_raw(Box::new(TariTransactionSendStatus { + direct_send_result: false, + store_and_forward_send_result: true, + queued_for_retry: false, + })); + let transaction_status = transaction_send_status_decode(status, error_ptr); + transaction_send_status_destroy(status); + assert_eq!(error, 0); + assert_eq!(transaction_status, 3); + + let status = Box::into_raw(Box::new(TariTransactionSendStatus { + direct_send_result: false, + store_and_forward_send_result: false, + queued_for_retry: false, + })); + let transaction_status = transaction_send_status_decode(status, error_ptr); + transaction_send_status_destroy(status); + assert_eq!(error, 1); + assert_eq!(transaction_status, 4); + + let status = Box::into_raw(Box::new(TariTransactionSendStatus { + direct_send_result: true, + store_and_forward_send_result: true, + queued_for_retry: true, + })); + let transaction_status = transaction_send_status_decode(status, error_ptr); + transaction_send_status_destroy(status); + assert_eq!(error, 1); + assert_eq!(transaction_status, 4); + + let status = Box::into_raw(Box::new(TariTransactionSendStatus { + direct_send_result: true, + store_and_forward_send_result: false, + queued_for_retry: true, + })); + let transaction_status = transaction_send_status_decode(status, error_ptr); + transaction_send_status_destroy(status); + assert_eq!(error, 1); + assert_eq!(transaction_status, 4); + + let status = Box::into_raw(Box::new(TariTransactionSendStatus { + direct_send_result: false, + store_and_forward_send_result: true, + queued_for_retry: true, + })); + let transaction_status = transaction_send_status_decode(status, error_ptr); + transaction_send_status_destroy(status); + assert_eq!(error, 1); + assert_eq!(transaction_status, 4); + } + } + #[test] fn test_transport_type_tcp() { unsafe { @@ -6949,8 +7116,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -6989,8 +7155,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7095,8 +7260,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7146,8 +7310,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7180,8 +7343,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7209,8 +7371,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7259,8 +7420,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7338,8 +7498,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7520,8 +7679,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7751,8 +7909,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, @@ -7809,8 +7966,7 @@ mod test { mined_unconfirmed_callback, scanned_callback, scanned_unconfirmed_callback, - direct_send_callback, - store_and_forward_send_callback, + transaction_send_result_callback, tx_cancellation_callback, txo_validation_complete_callback, contacts_liveness_data_updated_callback, diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index be2ce6e65f..3af84f5cd1 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -66,6 +66,8 @@ struct TariCompletedTransactions; struct TariBalance; +struct TariTransactionSendStatus; + struct TariCompletedTransaction; struct TariPendingOutboundTransactions; @@ -490,10 +492,15 @@ struct TariPublicKeys *comms_list_connected_public_keys(struct TariWallet *walle /// when a one-sided transaction is detected as mined AND confirmed. /// `callback_faux_transaction_unconfirmed` - The callback function pointer matching the function signature. This will /// be called when a one-sided transaction is detected as mined but not yet confirmed. -/// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called -/// when a direct send is completed. The first parameter is the transaction id and the second is whether if was successful or not. -/// `callback_store_and_forward_send_result` - The callback function pointer matching the function signature. This is called -/// when a direct send is completed. The first parameter is the transaction id and the second is whether if was successful or not. +/// `callback_transaction_send_result` - The callback function pointer matching the function signature. This is called +/// when a transaction send is completed. The first parameter is the transaction id and the second contains the +/// transaction send status, weather it was send direct and/or send via saf on the one hand or queued for further retry +/// sending on the other hand. +/// !direct_send & !saf_send & queued = 0 +/// direct_send & saf_send & !queued = 1 +/// direct_send & !saf_send & !queued = 2 +/// !direct_send & saf_send & !queued = 3 +/// any other combination (is not valid) = 4 /// `callback_transaction_cancellation` - The callback function pointer matching the function signature. This is called /// when a transaction is cancelled. The first parameter is a pointer to the cancelled transaction, the second is a reason as to /// why said transaction failed that is mapped to the `TxCancellationReason` enum: @@ -558,8 +565,7 @@ struct TariWallet *wallet_create(struct TariCommsConfig *config, void (*callback_transaction_mined_unconfirmed)(struct TariCompletedTransaction *, unsigned long long), void (*callback_faux_transaction_confirmed)(struct TariCompletedTransaction *), void (*callback_faux_transaction_unconfirmed)(struct TariCompletedTransaction *, unsigned long long), - void (*callback_direct_send_result)(unsigned long long, bool), - void (*callback_store_and_forward_send_result)(unsigned long long, bool), + void (*callback_transaction_send_result)(unsigned long long, struct TariTransactionSendStatus *), void (*callback_transaction_cancellation)(struct TariCompletedTransaction *, unsigned long long), void (*callback_txo_validation_complete)(unsigned long long, bool), void (*callback_contacts_liveness_data_updated)(struct TariContactsLivenessData *), diff --git a/integration_tests/README.md b/integration_tests/README.md index 61f30f1794..0c00a531c1 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -53,14 +53,14 @@ npm test -- --tags "@critical and not @long-running and not @broken" ``` -- To run the wallet FFI tests, add `--profile ci` or `--profile none` to the command line (_take note of the node +- To run the wallet FFI tests, add `--profile "ci"` or `--profile "none"` to the command line (_take note of the node version requirements_) and take note of the profile definition in `cucumber.js`. ```shell # Runs a specific FFI test - npm test -- --profile ci --name "My wallet FFI test scenario name" + npm test -- --profile "ci" --name "My wallet FFI test scenario name" # Runs a complete set of FFI tests - npm test -- --profile none --tags "@wallet-ffi" + npm test -- --profile "none" --tags "@wallet-ffi" ``` - Runs all @critical tests, but not @long-running diff --git a/integration_tests/features/WalletTransactions.feature b/integration_tests/features/WalletTransactions.feature index 906dc95aa9..59b6f81fa9 100644 --- a/integration_tests/features/WalletTransactions.feature +++ b/integration_tests/features/WalletTransactions.feature @@ -219,6 +219,31 @@ Feature: Wallet Transactions When I mine 6 blocks on NODE_C Then all nodes are at height 16 + Scenario: Wallet send transactions while offline + Given I have a seed node SEED + And I have wallet WALLET_A connected to seed node SEED + And I have wallet WALLET_B connected to seed node SEED + And I have mining node MINER_A connected to base node SEED and wallet WALLET_A + When mining node MINER_A mines 1 blocks with min difficulty 1 and max difficulty 100000 + When I mine 4 blocks on SEED + Then I wait for wallet WALLET_A to have at least 1000000000 uT + When I stop wallet WALLET_B + When I stop node SEED + Then I wait 10 seconds + Then I send 100000000 uT without waiting for broadcast from wallet WALLET_A to wallet WALLET_B at fee 20 + Then I wait 10 seconds + And I start base node SEED + And I have a base node NODE_A connected to seed SEED + And I have a base node NODE_B connected to seed SEED + And I stop wallet WALLET_A + And I start wallet WALLET_A + And I start wallet WALLET_B + Then all nodes are at height 5 + When I mine 1 blocks on SEED + Then all nodes are at height 6 + Then wallet WALLET_B detects all transactions are at least Pending + Then I wait 1 seconds + Scenario: Short wallet clearing out invalid transactions after a reorg # # Chain 1: diff --git a/integration_tests/helpers/ffi/ffiInterface.js b/integration_tests/helpers/ffi/ffiInterface.js index 7f62a6fc94..3c37b7bb33 100644 --- a/integration_tests/helpers/ffi/ffiInterface.js +++ b/integration_tests/helpers/ffi/ffiInterface.js @@ -260,6 +260,8 @@ class InterfaceFFI { [this.ptr, this.uint, this.intPtr], ], pending_inbound_transactions_destroy: [this.void, [this.ptr]], + transaction_send_status_decode: [this.uint, [this.ptr, this.intPtr]], + transaction_send_status_destroy: [this.void, [this.ptr]], comms_config_create: [ this.ptr, [ @@ -300,7 +302,6 @@ class InterfaceFFI { this.ptr, this.ptr, this.ptr, - this.ptr, this.boolPtr, this.intPtr, ], @@ -1127,7 +1128,18 @@ class InterfaceFFI { } //endregion - //region Wallet + //region TransactionSendStatus + static transactionSendStatusDecode(ptr) { + let error = this.initError(); + let result = this.fn.transaction_send_status_decode(ptr, error); + this.checkErrorResult(error, `transactionSendStatusDecode`); + return result; + } + + static transactionSendStatusDestroy(ptr) { + this.fn.transaction_send_status_destroy(ptr); + } + //endregion //region Callbacks static createCallbackReceivedTransaction(fn) { @@ -1162,12 +1174,8 @@ class InterfaceFFI { return ffi.Callback(this.void, [this.ptr, this.ulonglong], fn); } - static createCallbackDirectSendResult(fn) { - return ffi.Callback(this.void, [this.ulonglong, this.bool], fn); - } - - static createCallbackStoreAndForwardSendResult(fn) { - return ffi.Callback(this.void, [this.ulonglong, this.bool], fn); + static createCallbackTransactionSendResult(fn) { + return ffi.Callback(this.void, [this.ulonglong, this.ptr], fn); } static createCallbackTransactionCancellation(fn) { @@ -1215,8 +1223,7 @@ class InterfaceFFI { callback_transaction_mined_unconfirmed, callback_faux_transaction_confirmed, callback_faux_transaction_unconfirmed, - callback_direct_send_result, - callback_store_and_forward_send_result, + callback_transaction_send_result, callback_transaction_cancellation, callback_txo_validation_complete, callback_contacts_liveness_data_updated, @@ -1243,8 +1250,7 @@ class InterfaceFFI { callback_transaction_mined_unconfirmed, callback_faux_transaction_confirmed, callback_faux_transaction_unconfirmed, - callback_direct_send_result, - callback_store_and_forward_send_result, + callback_transaction_send_result, callback_transaction_cancellation, callback_txo_validation_complete, callback_contacts_liveness_data_updated, diff --git a/integration_tests/helpers/ffi/liveness_data.js b/integration_tests/helpers/ffi/livenessData.js similarity index 100% rename from integration_tests/helpers/ffi/liveness_data.js rename to integration_tests/helpers/ffi/livenessData.js diff --git a/integration_tests/helpers/ffi/transactionSendStatus.js b/integration_tests/helpers/ffi/transactionSendStatus.js new file mode 100644 index 0000000000..44bb9b58eb --- /dev/null +++ b/integration_tests/helpers/ffi/transactionSendStatus.js @@ -0,0 +1,34 @@ +// Copyright 2022 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +const InterfaceFFI = require("./ffiInterface"); + +class TransactionSendStatus { + ptr; + + pointerAssign(ptr) { + if (this.ptr) { + this.destroy(); + this.ptr = ptr; + } else { + this.ptr = ptr; + } + } + + getPtr() { + return this.ptr; + } + + getSendStatus() { + return InterfaceFFI.transactionSendStatusDecode(this.ptr); + } + + destroy() { + if (this.ptr) { + InterfaceFFI.transactionSendStatusDestroy(this.ptr); + this.ptr = undefined; //prevent double free segfault + } + } +} + +module.exports = TransactionSendStatus; diff --git a/integration_tests/helpers/ffi/wallet.js b/integration_tests/helpers/ffi/wallet.js index 84fb01382a..253483c92a 100644 --- a/integration_tests/helpers/ffi/wallet.js +++ b/integration_tests/helpers/ffi/wallet.js @@ -13,7 +13,8 @@ const Contacts = require("./contacts"); const Balance = require("./balance"); const utf8 = require("utf8"); -const LivenessData = require("./liveness_data"); +const LivenessData = require("./livenessData"); +const TransactionSendStatus = require("./transactionSendStatus"); class WalletBalance { available = 0; @@ -50,8 +51,7 @@ class Wallet { callback_transaction_mined_unconfirmed; callback_faux_transaction_confirmed; callback_faux_transaction_unconfirmed; - callback_direct_send_result; - callback_store_and_forward_send_result; + callback_transaction_send_result; callback_transaction_cancellation; callback_contacts_liveness_data_updated; callback_balance_updated; @@ -148,11 +148,9 @@ class Wallet { InterfaceFFI.createCallbackFauxTransactionUnconfirmed( this.onFauxTransactionUnconfirmed ); - this.callback_direct_send_result = - InterfaceFFI.createCallbackDirectSendResult(this.onDirectSendResult); - this.callback_store_and_forward_send_result = - InterfaceFFI.createCallbackStoreAndForwardSendResult( - this.onStoreAndForwardSendResult + this.callback_transaction_send_result = + InterfaceFFI.createCallbackTransactionSendResult( + this.onTransactionSendResult ); this.callback_transaction_cancellation = InterfaceFFI.createCallbackTransactionCancellation( @@ -220,8 +218,7 @@ class Wallet { this.callback_transaction_mined_unconfirmed, this.callback_faux_transaction_confirmed, this.callback_faux_transaction_unconfirmed, - this.callback_direct_send_result, - this.callback_store_and_forward_send_result, + this.callback_transaction_send_result, this.callback_transaction_cancellation, this.callback_txo_validation_complete, this.callback_contacts_liveness_data_updated, @@ -323,16 +320,13 @@ class Wallet { this.transactionCancelled += 1; }; - onDirectSendResult = (id, success) => { + onTransactionSendResult = (id, ptr) => { + let status = new TransactionSendStatus(ptr); + status.pointerAssign(ptr); console.log( - `${new Date().toISOString()} callbackDirectSendResult(${id},${success})` - ); - }; - - onStoreAndForwardSendResult = (id, success) => { - console.log( - `${new Date().toISOString()} callbackStoreAndForwardSendResult(${id},${success})` + `${new Date().toISOString()} callbackTransactionSendResult(${id}: (${status.getSendStatus()}))` ); + status.destroy(); }; onTxoValidationComplete = (request_key, validation_results) => { @@ -563,8 +557,7 @@ class Wallet { this.callback_transaction_mined_unconfirmed = this.callback_faux_transaction_confirmed = this.callback_faux_transaction_unconfirmed = - this.callback_direct_send_result = - this.callback_store_and_forward_send_result = + this.callback_transaction_send_result = this.callback_transaction_cancellation = this.callback_txo_validation_complete = this.callback_contacts_liveness_data_updated = diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index ec5fa33699..88ec6df778 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -42,24 +42,24 @@ } }, "@babel/compat-data": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.0.tgz", - "integrity": "sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", + "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", "dev": true }, "@babel/core": { - "version": "7.17.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.5.tgz", - "integrity": "sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA==", + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz", + "integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==", "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-compilation-targets": "^7.16.7", - "@babel/helper-module-transforms": "^7.16.7", - "@babel/helpers": "^7.17.2", - "@babel/parser": "^7.17.3", + "@babel/generator": "^7.17.7", + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helpers": "^7.17.8", + "@babel/parser": "^7.17.8", "@babel/template": "^7.16.7", "@babel/traverse": "^7.17.3", "@babel/types": "^7.17.0", @@ -82,18 +82,18 @@ } }, "@babel/eslint-plugin": { - "version": "7.16.5", - "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.16.5.tgz", - "integrity": "sha512-R1p6RMyU1Xl1U/NNr+D4+HjkQzN5dQOX0MpjW9WLWhHDjhzN9gso96MxxOFvPh0fKF/mMH8TGW2kuqQ2eK2s9A==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-plugin/-/eslint-plugin-7.17.7.tgz", + "integrity": "sha512-JATUoJJXSgwI0T8juxWYtK1JSgoLpIGUsCHIv+NMXcUDA2vIe6nvAHR9vnuJgs/P1hOFw7vPwibixzfqBBLIVw==", "dev": true, "requires": { "eslint-rule-composer": "^0.3.0" } }, "@babel/generator": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", - "integrity": "sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", "dev": true, "requires": { "@babel/types": "^7.17.0", @@ -102,12 +102,12 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", - "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz", + "integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==", "dev": true, "requires": { - "@babel/compat-data": "^7.16.4", + "@babel/compat-data": "^7.17.7", "@babel/helper-validator-option": "^7.16.7", "browserslist": "^4.17.5", "semver": "^6.3.0" @@ -161,14 +161,14 @@ } }, "@babel/helper-module-transforms": { - "version": "7.17.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.6.tgz", - "integrity": "sha512-2ULmRdqoOMpdvkbT8jONrZML/XALfzxlb052bldftkicAUy8AxSCkD5trDPQcwHNmolcl7wP6ehNqMlyUw6AaA==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", "@babel/helper-split-export-declaration": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7", "@babel/template": "^7.16.7", @@ -185,12 +185,12 @@ } }, "@babel/helper-simple-access": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", - "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", "dev": true, "requires": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.17.0" } }, "@babel/helper-split-export-declaration": { @@ -215,13 +215,13 @@ "dev": true }, "@babel/helpers": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.2.tgz", - "integrity": "sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ==", + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz", + "integrity": "sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==", "dev": true, "requires": { "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.0", + "@babel/traverse": "^7.17.3", "@babel/types": "^7.17.0" } }, @@ -237,11 +237,20 @@ } }, "@babel/parser": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.3.tgz", - "integrity": "sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA==", + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz", + "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==", "dev": true }, + "@babel/runtime": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.8.tgz", + "integrity": "sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, "@babel/template": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", @@ -305,9 +314,9 @@ } }, "@cucumber/ci-environment": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-8.0.1.tgz", - "integrity": "sha512-oQ6nifJ5MRyHFyCsBQU+D0CERSPbxezOxlVpJXcSrcOdKbdqGojZcu17Ww13dyHUGN8c417pWUifIlOrxrsZTQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-9.0.0.tgz", + "integrity": "sha512-5obgX4V7/kvBUzUNWeSRG+9gGdZlNBOvzuwS9QtcNy4KkVIDBPiUN/L6tqc8TGA6tKTI1ULGmkkO9XJbWzF4dw==", "dev": true }, "@cucumber/create-meta": { @@ -342,24 +351,25 @@ } }, "@cucumber/cucumber": { - "version": "8.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-8.0.0-rc.2.tgz", - "integrity": "sha512-cYcZEIkYl2BmdTa0VS62JUty2Vg/N+EC81xujNajaQQfXiKamwbunGFQhaqxrXIFmlFpuSASnEm4UudELbOfJw==", + "version": "8.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-8.0.0-rc.3.tgz", + "integrity": "sha512-1hauVrwUMKnXk6q/bjV9PF0qij0maUw+5TmerKPQYKgyBJ+z/7yIF58zLOrcsnF9WGgjX+YuusPoCU/wGhyYkQ==", "dev": true, "requires": { "@cspotcode/source-map-support": "^0.7.0", - "@cucumber/ci-environment": "8.0.1", - "@cucumber/cucumber-expressions": "14.0.0", + "@cucumber/ci-environment": "9.0.0", + "@cucumber/cucumber-expressions": "15.0.2", "@cucumber/gherkin": "22.0.0", "@cucumber/gherkin-streams": "4.0.0", - "@cucumber/html-formatter": "17.0.0", + "@cucumber/gherkin-utils": "^7.0.0", + "@cucumber/html-formatter": "18.0.0", "@cucumber/messages": "17.1.1", "@cucumber/tag-expressions": "4.1.0", "assertion-error-formatter": "^3.0.0", "capital-case": "^1.0.4", + "chalk": "^4.1.2", "cli-table3": "0.6.1", - "colors": "1.4.0", - "commander": "^8.0.0", + "commander": "^9.0.0", "duration": "^0.2.2", "durations": "^3.4.2", "figures": "^3.2.0", @@ -367,33 +377,85 @@ "indent-string": "^4.0.0", "is-stream": "^2.0.0", "knuth-shuffle-seeded": "^1.0.6", + "lodash.merge": "^4.6.2", + "lodash.mergewith": "^4.6.2", "mz": "^2.7.0", "progress": "^2.0.3", "resolve": "^1.19.0", "resolve-pkg": "^2.0.0", + "semver": "7.3.5", "stack-chain": "^2.0.0", "string-argv": "^0.3.1", "tmp": "^0.2.1", "util-arity": "^1.1.0", - "verror": "^1.10.0" + "verror": "^1.10.0", + "yup": "^0.32.11" }, "dependencies": { - "cli-table3": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", - "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "colors": "1.4.0", - "string-width": "^4.2.0" + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } }, "@cucumber/cucumber-expressions": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-14.0.0.tgz", - "integrity": "sha512-QiuFBrj4dZRc1Igvp2/nOjUNFyDtO7uHTrzgY9DbwzebYAYOvM6CKGOSxSuPUzxowuc1nuRkzJfFUI1kHaZgPQ==", + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-15.0.2.tgz", + "integrity": "sha512-ppN9JL1C5lw3InvM7WnoKZV9La5PW5Vr8c/8J0ZGzdlC8Y+PB7kskGzx7/tzl9kGjq7USQ7MZfwFz5el2sB/GA==", "dev": true, "requires": { "regexp-match-indices": "1.0.2" @@ -440,15 +502,15 @@ } } }, - "@cucumber/html-formatter": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-17.0.0.tgz", - "integrity": "sha512-yegA8LY1HYUONyMtTvAYj+aG4zc/6WRtKQxqJahjcdmjgXWcL1BTe8y0lw4BFVqFjaZNI9onOM5KDnMHDm3J/w==", + "@cucumber/gherkin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-7.0.0.tgz", + "integrity": "sha512-tDkSRITTPA6Df501doqeRH3+1jAM4ls6+tlFEVvkvuzTH3C8DXwQ5xBPWmUNmDhR/gJeZ+yj7gDRbDWr7Qc6Zw==", "dev": true, "requires": { "@cucumber/messages": "^17.1.0", - "commander": "8.1.0", - "source-map-support": "0.5.19" + "@teppeis/multimaps": "2.0.0", + "commander": "8.1.0" }, "dependencies": { "commander": { @@ -459,6 +521,41 @@ } } }, + "@cucumber/html-formatter": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-18.0.0.tgz", + "integrity": "sha512-GM9Nlob24JlUu5Nv6LZjOBbvptH4JatfXbime9T1Q4TIW2X3PxHtYC3NhoavCFJaenpjhTJsydua/dnbNFPtqw==", + "dev": true, + "requires": { + "@cucumber/messages": "^17.1.1", + "commander": "8.3.0", + "source-map-support": "0.5.21" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, "@cucumber/message-streams": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-3.0.0.tgz", @@ -529,9 +626,9 @@ } }, "@grpc/grpc-js": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.7.tgz", - "integrity": "sha512-RAlSbZ9LXo0wNoHKeUlwP9dtGgVBDUbnBKFpfAv5iSqMG4qWz9um2yLH215+Wow1I48etIa1QMS+WAGmsE/7HQ==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.10.tgz", + "integrity": "sha512-++oAubX/7rJzlqH0ShyzDENNNDHYrlttdc3NM40KlaVQDcgGqQknuPoavmyTC+oNUDyxPCX5dHceKhfcgN3tiw==", "dev": true, "requires": { "@grpc/proto-loader": "^0.6.4", @@ -666,6 +763,18 @@ "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", "dev": true }, + "@teppeis/multimaps": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-2.0.0.tgz", + "integrity": "sha512-TL1adzq1HdxUf9WYduLcQ/DNGYiz71U31QRgbnr0Ef1cPyOUOsBojxHVWpFeOSUucB6Lrs0LxFRA14ntgtkc9w==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.181", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", + "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==", + "dev": true + }, "@types/long": { "version": "4.0.1", "resolved": false, @@ -882,9 +991,9 @@ } }, "blakejs": { - "version": "1.1.1", - "resolved": false, - "integrity": "sha512-bLG6PHOCZJKNshTjGRBvET0vTciwQE6zFKOKKXPDJfwFBd4Ac0yBfPZqcGvGJap50l7ktvlpFqc2jGVaUgbJgg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", "dev": true }, "bluebird": { @@ -902,13 +1011,13 @@ } }, "browserslist": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.3.tgz", - "integrity": "sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==", + "version": "4.20.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", + "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001312", - "electron-to-chromium": "^1.4.71", + "caniuse-lite": "^1.0.30001317", + "electron-to-chromium": "^1.4.84", "escalade": "^3.1.1", "node-releases": "^2.0.2", "picocolors": "^1.0.0" @@ -940,9 +1049,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001312", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz", - "integrity": "sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==", + "version": "1.0.30001323", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz", + "integrity": "sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA==", "dev": true }, "capital-case": { @@ -1052,9 +1161,9 @@ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.1.0.tgz", + "integrity": "sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==", "dev": true }, "compress-commons": { @@ -1354,9 +1463,9 @@ "integrity": "sha512-V/lf7y33dGaypZZetVI1eu7BmvkbC4dItq12OElLRpKuaU5JxQstV2zHwLv8P7cNbQ+KL1WD80zMCTx5dNC4dg==" }, "electron-to-chromium": { - "version": "1.4.74", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.74.tgz", - "integrity": "sha512-DvQ20M0I4dIH8KcAo7n7E4OEeNafZ1N8z6g6ck+ALCM0ZoV6mpjaX6ekjs31zKlqPzacU3lmjG9PZEa1mQhEpQ==", + "version": "1.4.103", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz", + "integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg==", "dev": true }, "emoji-regex": { @@ -2091,12 +2200,9 @@ "dev": true }, "json5": { - "version": "2.2.0", - "resolved": false, - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" }, "jsonfile": { "version": "5.0.0", @@ -2168,6 +2274,12 @@ "resolved": false, "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, "lodash.camelcase": { "version": "4.3.0", "resolved": false, @@ -2206,6 +2318,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, "lodash.truncate": { "version": "4.4.2", "resolved": false, @@ -2341,6 +2459,12 @@ "thenify-all": "^1.0.0" } }, + "nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": false, @@ -2480,9 +2604,9 @@ "dev": true }, "prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.1.tgz", + "integrity": "sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A==", "dev": true }, "prettier-linter-helpers": { @@ -2509,6 +2633,12 @@ "resolved": false, "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" }, + "property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==", + "dev": true + }, "protobufjs": { "version": "6.11.2", "resolved": false, @@ -2599,6 +2729,12 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, "regexp-match-indices": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", @@ -2993,6 +3129,12 @@ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=", + "dev": true + }, "traverse-chain": { "version": "0.1.0", "resolved": false, @@ -3305,6 +3447,21 @@ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true }, + "yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + } + }, "zip-stream": { "version": "4.1.0", "resolved": false, diff --git a/integration_tests/package.json b/integration_tests/package.json index 74d4cd2b44..2ccf64075d 100644 --- a/integration_tests/package.json +++ b/integration_tests/package.json @@ -14,13 +14,13 @@ "author": "The Tari Project", "license": "ISC", "devDependencies": { - "@babel/core": "^7.16.0", + "@babel/core": "^7.17.8", "@babel/eslint-parser": "^7.16.3", - "@babel/eslint-plugin": "^7.14.5", - "@cucumber/cucumber": "^8.0.0-rc.1", - "@grpc/grpc-js": "^1.4.4", + "@babel/eslint-plugin": "^7.17.7", + "@cucumber/cucumber": "^8.0.0-rc.3", + "@grpc/grpc-js": "^1.5.10", "@grpc/proto-loader": "^0.5.5", - "blakejs": "^1.1.0", + "blakejs": "^1.2.1", "chai": "^4.3.6", "cucumber-html-reporter": "^5.5.0", "cucumber-pretty": "^6.0.1", @@ -30,7 +30,7 @@ "ffi-napi": "^4.0.3", "grpc-promise": "^1.4.0", "husky": "^6.0.0", - "prettier": "^2.5.1", + "prettier": "^2.6.1", "ref-napi": "^3.0.3" }, "dependencies": { @@ -41,7 +41,7 @@ "cucumber": "^7.0.0-rc.0", "dateformat": "^3.0.3", "glob": "^7.2.0", - "json5": "^2.2.0", + "json5": "^2.2.1", "sha3": "^2.1.3", "tari_crypto": "^0.9.1", "utf8": "^3.0.0",