From a75ffca2c37eca8b9829467c84f1a9939a840652 Mon Sep 17 00:00:00 2001 From: SW van Heerden Date: Thu, 21 Oct 2021 13:43:11 +0200 Subject: [PATCH] =?UTF-8?q?Mark=20all=20outputs=20and=20transactions=20to?= =?UTF-8?q?=20be=20revalidated.=C2=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Philip Robinson --- applications/tari_app_grpc/proto/wallet.proto | 8 +- .../src/grpc/wallet_grpc_server.rs | 19 +++ .../src/output_manager_service/handle.rs | 9 + .../src/output_manager_service/service.rs | 9 + .../storage/database.rs | 10 ++ .../storage/sqlite_db.rs | 25 +++ .../wallet/src/transaction_service/handle.rs | 13 ++ .../wallet/src/transaction_service/service.rs | 12 ++ .../transaction_service/storage/database.rs | 10 ++ .../transaction_service/storage/sqlite_db.rs | 22 +++ .../tests/output_manager_service/service.rs | 155 ++++++++++++++++++ .../transaction_protocols.rs | 123 ++++++++++++++ clients/wallet_grpc_client/index.js | 1 + 13 files changed, 415 insertions(+), 1 deletion(-) diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index aad57e28c4..301fe4b416 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -54,6 +54,8 @@ service Wallet { rpc ListConnectedPeers(Empty) returns (ListConnectedPeersResponse); // Cancel pending transaction rpc CancelTransaction (CancelTransactionRequest) returns (CancelTransactionResponse); + // Will triggger a complete revalidation of all wallet outputs. + rpc RevalidateAllTransactions (RevalidateRequest) returns (RevalidateResponse); } message GetVersionRequest { } @@ -196,4 +198,8 @@ message CancelTransactionRequest { message CancelTransactionResponse { bool is_success = 1; string failure_message = 2; -} \ No newline at end of file +} + +message RevalidateRequest{} + +message RevalidateResponse{} \ No newline at end of file diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index cf20599a9b..4ac00be1e1 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -23,6 +23,8 @@ use tari_app_grpc::{ GetVersionResponse, ImportUtxosRequest, ImportUtxosResponse, + RevalidateRequest, + RevalidateResponse, TransactionDirection, TransactionInfo, TransactionStatus, @@ -118,6 +120,23 @@ impl wallet_server::Wallet for WalletGrpcServer { })) } + async fn revalidate_all_transactions( + &self, + _request: Request, + ) -> Result, Status> { + let mut output_service = self.get_output_manager_service(); + output_service + .revalidate_all_outputs() + .await + .map_err(|e| Status::unknown(e.to_string()))?; + let mut tx_service = self.get_transaction_service(); + tx_service + .revalidate_all_transactions() + .await + .map_err(|e| Status::unknown(e.to_string()))?; + Ok(Response::new(RevalidateResponse {})) + } + async fn get_coinbase( &self, request: Request, diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 65976f983d..a669c0ff79 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -57,6 +57,7 @@ pub enum OutputManagerRequest { GetInvalidOutputs, GetSeedWords, ValidateUtxos, + RevalidateTxos, CreateCoinSplit((MicroTari, usize, MicroTari, Option)), ApplyEncryption(Box), RemoveEncryption, @@ -98,6 +99,7 @@ impl fmt::Display for OutputManagerRequest { GetInvalidOutputs => write!(f, "GetInvalidOutputs"), GetSeedWords => write!(f, "GetSeedWords"), ValidateUtxos => write!(f, "ValidateUtxos"), + RevalidateTxos => write!(f, "RevalidateTxos"), CreateCoinSplit(v) => write!(f, "CreateCoinSplit ({})", v.0), ApplyEncryption(_) => write!(f, "ApplyEncryption"), RemoveEncryption => write!(f, "RemoveEncryption"), @@ -241,6 +243,13 @@ impl OutputManagerHandle { } } + pub async fn revalidate_all_outputs(&mut self) -> Result { + match self.handle.call(OutputManagerRequest::RevalidateTxos).await?? { + OutputManagerResponse::TxoValidationStarted(request_key) => Ok(request_key), + _ => Err(OutputManagerError::UnexpectedApiResponse), + } + } + pub async fn get_recipient_transaction( &mut self, sender_message: TransactionSenderMessage, diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index c75899701d..3283f45587 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -274,6 +274,10 @@ where OutputManagerRequest::ValidateUtxos => { self.validate_outputs().map(OutputManagerResponse::TxoValidationStarted) }, + OutputManagerRequest::RevalidateTxos => self + .revalidate_outputs() + .await + .map(OutputManagerResponse::TxoValidationStarted), OutputManagerRequest::GetInvalidOutputs => { let outputs = self .fetch_invalid_outputs() @@ -385,6 +389,11 @@ where Ok(id) } + async fn revalidate_outputs(&mut self) -> Result { + self.resources.db.set_outputs_to_be_revalidated().await?; + self.validate_outputs() + } + /// Add an unblinded output to the unspent outputs list pub async fn add_output(&mut self, tx_id: Option, output: UnblindedOutput) -> Result<(), OutputManagerError> { debug!( diff --git a/base_layer/wallet/src/output_manager_service/storage/database.rs b/base_layer/wallet/src/output_manager_service/storage/database.rs index 0a232aec44..97d61ad374 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database.rs @@ -65,6 +65,8 @@ pub trait OutputManagerBackend: Send + Sync + Clone { fn set_output_to_unmined(&self, hash: Vec) -> Result<(), OutputManagerStorageError>; + fn set_outputs_to_be_revalidated(&self) -> Result<(), OutputManagerStorageError>; + fn mark_output_as_spent( &self, hash: Vec, @@ -562,6 +564,14 @@ where T: OutputManagerBackend + 'static Ok(()) } + pub async fn set_outputs_to_be_revalidated(&self) -> Result<(), OutputManagerStorageError> { + let db = self.db.clone(); + tokio::task::spawn_blocking(move || db.set_outputs_to_be_revalidated()) + .await + .map_err(|err| OutputManagerStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(()) + } + pub async fn mark_output_as_spent( &self, hash: HashOutput, diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs index b3b9c872a2..1de4dfcf02 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs @@ -491,6 +491,31 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { Ok(()) } + fn set_outputs_to_be_revalidated(&self) -> Result<(), OutputManagerStorageError> { + let start = Instant::now(); + let conn = self.database_connection.acquire_lock(); + let acquire_lock = start.elapsed(); + // Only update non-deleted utxos + let result = diesel::update(outputs::table.filter(outputs::marked_deleted_at_height.is_null())) + .set(( + outputs::mined_height.eq::>(None), + outputs::mined_in_block.eq::>>(None), + outputs::mined_mmr_position.eq::>(None), + )) + .execute(&(*conn))?; + + trace!(target: LOG_TARGET, "rows updated: {:?}", result); + trace!( + target: LOG_TARGET, + "sqlite profile - set_outputs_to_be_revalidated: lock {} + db_op {} = {} ms", + acquire_lock.as_millis(), + (start.elapsed() - acquire_lock).as_millis(), + start.elapsed().as_millis() + ); + + Ok(()) + } + fn mark_output_as_spent( &self, hash: Vec, diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 0d5b279094..c8ed08aebd 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -59,6 +59,7 @@ pub enum TransactionServiceRequest { GetNumConfirmationsRequired, SetNumConfirmationsRequired(u64), ValidateTransactions, + ReValidateTransactions, } impl fmt::Display for TransactionServiceRequest { @@ -99,6 +100,7 @@ impl fmt::Display for TransactionServiceRequest { Self::SetNumConfirmationsRequired(_) => f.write_str("SetNumConfirmationsRequired"), Self::GetAnyTransaction(t) => f.write_str(&format!("GetAnyTransaction({})", t)), TransactionServiceRequest::ValidateTransactions => f.write_str("ValidateTransactions"), + TransactionServiceRequest::ReValidateTransactions => f.write_str("ReValidateTransactions"), } } } @@ -396,6 +398,17 @@ impl TransactionServiceHandle { } } + pub async fn revalidate_all_transactions(&mut self) -> Result<(), TransactionServiceError> { + match self + .handle + .call(TransactionServiceRequest::ReValidateTransactions) + .await?? + { + TransactionServiceResponse::ValidationStarted(_) => Ok(()), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + pub async fn set_normal_power_mode(&mut self) -> Result<(), TransactionServiceError> { match self .handle diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index cca61d66e9..b77d9d5957 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -646,6 +646,10 @@ where .start_transaction_validation_protocol(transaction_validation_join_handles) .await .map(TransactionServiceResponse::ValidationStarted), + TransactionServiceRequest::ReValidateTransactions => self + .start_transaction_revalidation(transaction_validation_join_handles) + .await + .map(TransactionServiceResponse::ValidationStarted), }; // If the individual handlers did not already send the API response then do it here. @@ -1544,6 +1548,14 @@ where Ok(()) } + async fn start_transaction_revalidation( + &mut self, + join_handles: &mut FuturesUnordered>>, + ) -> Result { + self.resources.db.mark_all_transactions_as_unvalidated().await?; + self.start_transaction_validation_protocol(join_handles).await + } + async fn start_transaction_validation_protocol( &mut self, join_handles: &mut FuturesUnordered>>, diff --git a/base_layer/wallet/src/transaction_service/storage/database.rs b/base_layer/wallet/src/transaction_service/storage/database.rs index 52e2f51292..0ed8840538 100644 --- a/base_layer/wallet/src/transaction_service/storage/database.rs +++ b/base_layer/wallet/src/transaction_service/storage/database.rs @@ -124,6 +124,8 @@ pub trait TransactionBackend: Send + Sync + Clone { /// Clears the mined block and height of a transaction fn set_transaction_as_unmined(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; + + fn mark_all_transactions_as_unvalidated(&self) -> Result<(), TransactionStorageError>; } #[derive(Clone, PartialEq)] @@ -752,6 +754,14 @@ where T: TransactionBackend + 'static Ok(()) } + pub async fn mark_all_transactions_as_unvalidated(&self) -> Result<(), TransactionStorageError> { + let db_clone = self.db.clone(); + tokio::task::spawn_blocking(move || db_clone.mark_all_transactions_as_unvalidated()) + .await + .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(()) + } + pub async fn set_transaction_mined_height( &self, tx_id: TxId, diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index 0e539823f6..0def1078b8 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -1034,6 +1034,28 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Ok(result) } + fn mark_all_transactions_as_unvalidated(&self) -> Result<(), TransactionStorageError> { + let start = Instant::now(); + let conn = self.database_connection.acquire_lock(); + let acquire_lock = start.elapsed(); + let result = diesel::update(completed_transactions::table.filter(completed_transactions::cancelled.eq(0))) + .set(( + completed_transactions::mined_height.eq::>(None), + completed_transactions::mined_in_block.eq::>>(None), + )) + .execute(&(*conn))?; + + trace!(target: LOG_TARGET, "rows updated: {:?}", result); + trace!( + target: LOG_TARGET, + "sqlite profile - set_transactions_to_be_revalidated: lock {} + db_op {} = {} ms", + acquire_lock.as_millis(), + (start.elapsed() - acquire_lock).as_millis(), + start.elapsed().as_millis() + ); + Ok(()) + } + fn set_transaction_as_unmined(&self, tx_id: u64) -> Result<(), TransactionStorageError> { let start = Instant::now(); let conn = self.database_connection.acquire_lock(); diff --git a/base_layer/wallet/tests/output_manager_service/service.rs b/base_layer/wallet/tests/output_manager_service/service.rs index 0bd38fda78..d1a04f0298 100644 --- a/base_layer/wallet/tests/output_manager_service/service.rs +++ b/base_layer/wallet/tests/output_manager_service/service.rs @@ -1454,6 +1454,161 @@ async fn test_txo_validation() { assert_eq!(balance.available_balance, balance.time_locked_balance.unwrap()); } +#[tokio::test] +async fn test_txo_revalidation() { + let factories = CryptoFactories::default(); + + let (connection, _tempdir) = get_temp_sqlite_database_connection(); + let backend = OutputManagerSqliteDatabase::new(connection, None); + + let ( + mut oms, + wallet_connectivity, + _shutdown, + _ts, + mock_rpc_server, + server_node_identity, + rpc_service_state, + _base_node_service_event_publisher, + ) = setup_output_manager_service(backend, true).await; + + wallet_connectivity.notify_base_node_set(server_node_identity.to_peer()); + // Now we add the connection + let mut connection = mock_rpc_server + .create_connection(server_node_identity.to_peer(), "t/bnwallet/1".into()) + .await; + wallet_connectivity.set_base_node_wallet_rpc_client(connect_rpc_client(&mut connection).await); + + let output1_value = 1_000_000; + let output1 = create_unblinded_output( + script!(Nop), + OutputFeatures::default(), + TestParamsHelpers::new(), + MicroTari::from(output1_value), + ); + let output1_tx_output = output1.as_transaction_output(&factories).unwrap(); + oms.add_output_with_tx_id(1, output1.clone()).await.unwrap(); + + let output2_value = 2_000_000; + let output2 = create_unblinded_output( + script!(Nop), + OutputFeatures::default(), + TestParamsHelpers::new(), + MicroTari::from(output2_value), + ); + let output2_tx_output = output2.as_transaction_output(&factories).unwrap(); + + oms.add_output_with_tx_id(2, output2.clone()).await.unwrap(); + + let mut block1_header = BlockHeader::new(1); + block1_header.height = 1; + let mut block4_header = BlockHeader::new(1); + block4_header.height = 4; + + let mut block_headers = HashMap::new(); + block_headers.insert(1, block1_header.clone()); + block_headers.insert(4, block4_header.clone()); + rpc_service_state.set_blocks(block_headers.clone()); + + // These responses will mark outputs 1 and 2 and mined confirmed + let responses = vec![ + UtxoQueryResponse { + output: Some(output1_tx_output.clone().into()), + mmr_position: 1, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output1_tx_output.hash(), + }, + UtxoQueryResponse { + output: Some(output2_tx_output.clone().into()), + mmr_position: 2, + mined_height: 1, + mined_in_block: block1_header.hash(), + output_hash: output2_tx_output.hash(), + }, + ]; + + let utxo_query_responses = UtxoQueryResponses { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + responses, + }; + + rpc_service_state.set_utxo_query_response(utxo_query_responses.clone()); + + // This response sets output1 as spent + let query_deleted_response = QueryDeletedResponse { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + deleted_positions: vec![], + not_deleted_positions: vec![1, 2], + heights_deleted_at: vec![], + blocks_deleted_in: vec![], + }; + + rpc_service_state.set_query_deleted_response(query_deleted_response.clone()); + oms.validate_txos().await.unwrap(); + let _utxo_query_calls = rpc_service_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); + let _query_deleted_calls = rpc_service_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); + + let unspent_txos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(unspent_txos.len(), 2); + + // This response sets output1 as spent + let query_deleted_response = QueryDeletedResponse { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + deleted_positions: vec![1], + not_deleted_positions: vec![2], + heights_deleted_at: vec![4], + blocks_deleted_in: vec![block4_header.hash()], + }; + + rpc_service_state.set_query_deleted_response(query_deleted_response.clone()); + oms.revalidate_all_outputs().await.unwrap(); + let _utxo_query_calls = rpc_service_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); + let _query_deleted_calls = rpc_service_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); + + let unspent_txos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(unspent_txos.len(), 1); + + // This response sets output1 and 2 as spent + let query_deleted_response = QueryDeletedResponse { + best_block: block4_header.hash(), + height_of_longest_chain: 4, + deleted_positions: vec![1, 2], + not_deleted_positions: vec![], + heights_deleted_at: vec![4, 4], + blocks_deleted_in: vec![block4_header.hash(), block4_header.hash()], + }; + + rpc_service_state.set_query_deleted_response(query_deleted_response.clone()); + oms.revalidate_all_outputs().await.unwrap(); + let _utxo_query_calls = rpc_service_state + .wait_pop_utxo_query_calls(1, Duration::from_secs(60)) + .await + .unwrap(); + let _query_deleted_calls = rpc_service_state + .wait_pop_query_deleted(1, Duration::from_secs(60)) + .await + .unwrap(); + + let unspent_txos = oms.get_unspent_outputs().await.unwrap(); + assert_eq!(unspent_txos.len(), 0); +} + #[tokio::test] async fn test_oms_key_manager_discrepancy() { let shutdown = Shutdown::new(); diff --git a/base_layer/wallet/tests/transaction_service/transaction_protocols.rs b/base_layer/wallet/tests/transaction_service/transaction_protocols.rs index 8dc532ce81..ac3dd8eebd 100644 --- a/base_layer/wallet/tests/transaction_service/transaction_protocols.rs +++ b/base_layer/wallet/tests/transaction_service/transaction_protocols.rs @@ -862,6 +862,129 @@ async fn tx_validation_protocol_tx_becomes_mined_unconfirmed_then_confirmed() { assert_eq!(completed_txs.get(&2).unwrap().confirmations.unwrap(), 4); } +/// Test that revalidation clears the correct db fields and calls for validation of is said transactions +#[tokio::test] +#[allow(clippy::identity_op)] +async fn tx_revalidation() { + let ( + resources, + _outbound_mock_state, + mock_rpc_server, + server_node_identity, + rpc_service_state, + _shutdown, + _temp_dir, + _transaction_event_receiver, + wallet_connectivity, + ) = setup(TxProtocolTestConfig::WithConnection).await; + // Now we add the connection + let mut connection = mock_rpc_server + .create_connection(server_node_identity.to_peer(), "t/bnwallet/1".into()) + .await; + wallet_connectivity.set_base_node_wallet_rpc_client(connect_rpc_client(&mut connection).await); + add_transaction_to_database( + 1, + 1 * T, + true, + Some(TransactionStatus::Completed), + None, + resources.db.clone(), + ) + .await; + add_transaction_to_database( + 2, + 2 * T, + true, + Some(TransactionStatus::Completed), + None, + resources.db.clone(), + ) + .await; + + let tx2 = resources.db.get_completed_transaction(2).await.unwrap(); + + // set tx2 as fully mined + let transaction_query_batch_responses = vec![TxQueryBatchResponseProto { + signature: Some(SignatureProto::from( + tx2.transaction.first_kernel_excess_sig().unwrap().clone(), + )), + location: TxLocationProto::from(TxLocation::Mined) as i32, + block_hash: Some([5u8; 16].to_vec()), + confirmations: 4, + block_height: 5, + }]; + + let batch_query_response = TxQueryBatchResponsesProto { + responses: transaction_query_batch_responses.clone(), + is_synced: true, + tip_hash: Some([5u8; 16].to_vec()), + height_of_longest_chain: 5, + }; + + rpc_service_state.set_transaction_query_batch_responses(batch_query_response.clone()); + + let protocol = TransactionValidationProtocol::new( + 4, + resources.db.clone(), + wallet_connectivity.clone(), + resources.config.clone(), + resources.event_publisher.clone(), + resources.output_manager_service.clone(), + ); + + let join_handle = task::spawn(protocol.execute()); + let result = join_handle.await.unwrap(); + assert!(result.is_ok()); + + let completed_txs = resources.db.get_completed_transactions().await.unwrap(); + + assert_eq!(completed_txs.get(&2).unwrap().status, TransactionStatus::MinedConfirmed); + assert_eq!(completed_txs.get(&2).unwrap().confirmations.unwrap(), 4); + + let transaction_query_batch_responses = vec![TxQueryBatchResponseProto { + signature: Some(SignatureProto::from( + tx2.transaction.first_kernel_excess_sig().unwrap().clone(), + )), + location: TxLocationProto::from(TxLocation::Mined) as i32, + block_hash: Some([5u8; 16].to_vec()), + confirmations: 8, + block_height: 10, + }]; + + let batch_query_response = TxQueryBatchResponsesProto { + responses: transaction_query_batch_responses.clone(), + is_synced: true, + tip_hash: Some([5u8; 16].to_vec()), + height_of_longest_chain: 10, + }; + + rpc_service_state.set_transaction_query_batch_responses(batch_query_response.clone()); + // revalidate sets all to unvalidated, so lets check that thay are + resources.db.mark_all_transactions_as_unvalidated().await.unwrap(); + let completed_txs = resources.db.get_completed_transactions().await.unwrap(); + assert_eq!(completed_txs.get(&2).unwrap().status, TransactionStatus::MinedConfirmed); + assert_eq!(completed_txs.get(&2).unwrap().mined_height, None); + assert_eq!(completed_txs.get(&2).unwrap().mined_in_block, None); + + let protocol = TransactionValidationProtocol::new( + 5, + resources.db.clone(), + wallet_connectivity.clone(), + resources.config.clone(), + resources.event_publisher.clone(), + resources.output_manager_service.clone(), + ); + + let join_handle = task::spawn(protocol.execute()); + let result = join_handle.await.unwrap(); + assert!(result.is_ok()); + + let completed_txs = resources.db.get_completed_transactions().await.unwrap(); + // data should now be updated and changed + assert_eq!(completed_txs.get(&2).unwrap().status, TransactionStatus::MinedConfirmed); + assert_eq!(completed_txs.get(&2).unwrap().confirmations.unwrap(), 8); +} + /// Test that validation detects transactions becoming mined unconfirmed and then confirmed with some going back to /// completed #[tokio::test] diff --git a/clients/wallet_grpc_client/index.js b/clients/wallet_grpc_client/index.js index 1770f1d06b..41065483d7 100644 --- a/clients/wallet_grpc_client/index.js +++ b/clients/wallet_grpc_client/index.js @@ -41,6 +41,7 @@ function Client(address) { "getNetworkStatus", "cancelTransaction", "checkForUpdates", + "revalidateAllTransactions", ]; this.waitForReady = (...args) => {