diff --git a/packages/rs-dpp/src/bls/native_bls.rs b/packages/rs-dpp/src/bls/native_bls.rs index 492f563511..3e1341bf9a 100644 --- a/packages/rs-dpp/src/bls/native_bls.rs +++ b/packages/rs-dpp/src/bls/native_bls.rs @@ -2,7 +2,6 @@ use crate::bls_signatures::{ Bls12381G2Impl, Pairing, PublicKey, SecretKey, Signature, SignatureSchemes, }; use crate::{BlsModule, ProtocolError, PublicKeyValidationError}; -use std::array::TryFromSliceError; #[derive(Default)] pub struct NativeBlsModule; diff --git a/packages/rs-dpp/src/identity/identity_public_key/methods/hash/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/methods/hash/mod.rs index 5cc4828dd7..a7b4803e2d 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/methods/hash/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/methods/hash/mod.rs @@ -2,7 +2,7 @@ mod v0; use crate::identity::IdentityPublicKey; use crate::ProtocolError; -use dashcore::Network; +use dashcore::{Address, Network}; pub use v0::*; impl IdentityPublicKeyHashMethodsV0 for IdentityPublicKey { @@ -12,6 +12,12 @@ impl IdentityPublicKeyHashMethodsV0 for IdentityPublicKey { } } + fn address(&self, network: Network) -> Result { + match self { + IdentityPublicKey::V0(v0) => v0.address(network), + } + } + fn validate_private_key_bytes( &self, private_key_bytes: &[u8; 32], diff --git a/packages/rs-dpp/src/identity/identity_public_key/methods/hash/v0/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/methods/hash/v0/mod.rs index dd2b975e88..504d5187e7 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/methods/hash/v0/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/methods/hash/v0/mod.rs @@ -1,10 +1,13 @@ use crate::ProtocolError; -use dashcore::Network; +use dashcore::{Address, Network}; pub trait IdentityPublicKeyHashMethodsV0 { /// Get the original public key hash fn public_key_hash(&self) -> Result<[u8; 20], ProtocolError>; + /// Get the address + fn address(&self, network: Network) -> Result; + /// Verifies that the private key bytes match this identity public key fn validate_private_key_bytes( &self, diff --git a/packages/rs-dpp/src/identity/identity_public_key/v0/methods/mod.rs b/packages/rs-dpp/src/identity/identity_public_key/v0/methods/mod.rs index ea064e33b6..25989a6a04 100644 --- a/packages/rs-dpp/src/identity/identity_public_key/v0/methods/mod.rs +++ b/packages/rs-dpp/src/identity/identity_public_key/v0/methods/mod.rs @@ -4,12 +4,13 @@ use crate::identity::KeyType; use crate::util::hash::ripemd160_sha256; use crate::ProtocolError; use anyhow::anyhow; +use dashcore::address::Payload; #[cfg(feature = "ed25519-dalek")] use dashcore::ed25519_dalek; use dashcore::hashes::Hash; use dashcore::key::Secp256k1; use dashcore::secp256k1::SecretKey; -use dashcore::{Network, PublicKey as ECDSAPublicKey}; +use dashcore::{Address, Network, PubkeyHash, PublicKey as ECDSAPublicKey}; use platform_value::Bytes20; #[cfg(feature = "bls-signatures")] use {crate::bls_signatures, dashcore::blsful::Bls12381G2Impl}; @@ -51,6 +52,21 @@ impl IdentityPublicKeyHashMethodsV0 for IdentityPublicKeyV0 { } } + fn address(&self, network: Network) -> Result { + match self.key_type { + KeyType::BIP13_SCRIPT_HASH => Err(ProtocolError::NotSupported( + "Can not get an address from a single script hash key".to_string(), + )), + _ => { + let public_key_hash = self.public_key_hash()?; + Ok(Address::new( + network, + Payload::PubkeyHash(PubkeyHash::from_byte_array(public_key_hash)), + )) + } + } + } + fn validate_private_key_bytes( &self, private_key_bytes: &[u8; 32], diff --git a/packages/rs-drive-abci/src/query/document_query/v0/mod.rs b/packages/rs-drive-abci/src/query/document_query/v0/mod.rs index d4177878bc..b725855ec1 100644 --- a/packages/rs-drive-abci/src/query/document_query/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/document_query/v0/mod.rs @@ -131,6 +131,8 @@ impl Platform { &self.config.drive, )); + println!("{:?}", drive_query); + let response = if prove { let proof = match drive_query.execute_with_proof(&self.drive, None, None, platform_version) { diff --git a/packages/rs-drive-proof-verifier/src/error.rs b/packages/rs-drive-proof-verifier/src/error.rs index 3fb5825a8c..3da25aacc7 100644 --- a/packages/rs-drive-proof-verifier/src/error.rs +++ b/packages/rs-drive-proof-verifier/src/error.rs @@ -119,6 +119,10 @@ pub enum ContextProviderError { /// Async error, eg. when tokio runtime fails #[error("async error: {0}")] AsyncError(String), + + /// Dash Core error + #[error("Dash Core error: {0}")] + DashCoreError(String), } impl From for Error { diff --git a/packages/rs-drive/src/query/mod.rs b/packages/rs-drive/src/query/mod.rs index f6aa81deb2..d8f7181cc2 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -2143,7 +2143,6 @@ impl<'a> DriveDocumentQuery<'a> { drive_operations, platform_version, )?; - let query_result = drive.grove_get_path_query_serialized_results( &path_query, transaction, diff --git a/packages/rs-drive/tests/query_tests.rs b/packages/rs-drive/tests/query_tests.rs index 6bad5144f9..d8c3a84dfb 100644 --- a/packages/rs-drive/tests/query_tests.rs +++ b/packages/rs-drive/tests/query_tests.rs @@ -4662,6 +4662,8 @@ fn test_dpns_query_start_after_with_null_id() { ) .expect("query should be built"); + println!("{:?}", query); + // We are commenting this out on purpose to make it easier to find // let mut query_operations: Vec = vec![]; // let path_query = query diff --git a/packages/rs-drive/tests/supporting_files/contract/withdrawals/withdrawals.json b/packages/rs-drive/tests/supporting_files/contract/withdrawals/withdrawals.json new file mode 100644 index 0000000000..fbadb59824 --- /dev/null +++ b/packages/rs-drive/tests/supporting_files/contract/withdrawals/withdrawals.json @@ -0,0 +1,141 @@ +{ + "$format_version": "0", + "id": "BnqN3oupH6uCogzgZMvSjjpKxmcdNXAShnNY4Kor33aL", + "ownerId": "BnqN3oupH6uCogzgZMvSjjpKxmcdNXAShnNY4Kor33aL", + "version": 1, + "documentSchemas": { + "withdrawal": { + "description": "Withdrawal document to track underlying withdrawal transactions. Withdrawals should be created with IdentityWithdrawalTransition", + "creationRestrictionMode": 2, + "type": "object", + "indices": [ + { + "name": "identityStatus", + "properties": [ + { + "$ownerId": "asc" + }, + { + "status": "asc" + }, + { + "$createdAt": "asc" + } + ], + "unique": false + }, + { + "name": "identityRecent", + "properties": [ + { + "$ownerId": "asc" + }, + { + "$updatedAt": "asc" + }, + { + "status": "asc" + } + ], + "unique": false + }, + { + "name": "pooling", + "properties": [ + { + "status": "asc" + }, + { + "pooling": "asc" + }, + { + "coreFeePerByte": "asc" + }, + { + "$updatedAt": "asc" + } + ], + "unique": false + }, + { + "name": "transaction", + "properties": [ + { + "status": "asc" + }, + { + "transactionIndex": "asc" + } + ], + "unique": false + } + ], + "properties": { + "transactionIndex": { + "type": "integer", + "description": "Sequential index of asset unlock (withdrawal) transaction. Populated when a withdrawal pooled into withdrawal transaction", + "minimum": 1, + "position": 0 + }, + "transactionSignHeight": { + "type": "integer", + "description": "The Core height on which transaction was signed", + "minimum": 1, + "position": 1 + }, + "amount": { + "type": "integer", + "description": "The amount to be withdrawn", + "minimum": 1000, + "position": 2 + }, + "coreFeePerByte": { + "type": "integer", + "description": "This is the fee that you are willing to spend for this transaction in Duffs/Byte", + "minimum": 1, + "maximum": 4294967295, + "position": 3 + }, + "pooling": { + "type": "integer", + "description": "This indicated the level at which Platform should try to pool this transaction", + "enum": [ + 0, + 1, + 2 + ], + "position": 4 + }, + "outputScript": { + "type": "array", + "byteArray": true, + "minItems": 23, + "maxItems": 25, + "position": 5 + }, + "status": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "description": "0 - Pending, 1 - Signed, 2 - Broadcasted, 3 - Complete, 4 - Expired", + "position": 6 + } + }, + "additionalProperties": false, + "required": [ + "$createdAt", + "$updatedAt", + "amount", + "coreFeePerByte", + "pooling", + "outputScript", + "status" + ] + } + } +} diff --git a/packages/rs-sdk/src/core/dash_core_client.rs b/packages/rs-sdk/src/core/dash_core_client.rs index d59af4207c..73216f294a 100644 --- a/packages/rs-sdk/src/core/dash_core_client.rs +++ b/packages/rs-sdk/src/core/dash_core_client.rs @@ -12,10 +12,12 @@ use dashcore_rpc::{ }; use dpp::dashcore::ProTxHash; use dpp::prelude::CoreBlockHeight; -use drive_proof_verifier::error::ContextProviderError; +use std::time::Duration; use std::{fmt::Debug, sync::Mutex}; use zeroize::Zeroizing; +use super::DashCoreError; + /// Core RPC client that can be used to retrieve quorum keys from core. /// /// TODO: This is a temporary implementation, effective until we integrate SPV. @@ -27,6 +29,50 @@ pub struct LowLevelDashCoreClient { core_port: u16, } +macro_rules! retry { + ($action:expr) => {{ + /// Maximum number of retry attempts + const MAX_RETRIES: u32 = 4; + /// // Multiplier for Fibonacci sequence + const FIB_MULTIPLIER: u64 = 1; + + const BASE_TIME_MS: u64 = 40; + + fn fibonacci(n: u32) -> u64 { + match n { + 0 => 0, + 1 => 1, + _ => fibonacci(n - 1) + fibonacci(n - 2), + } + } + + let mut final_result = None; + for i in 0..MAX_RETRIES { + match $action { + Ok(result) => { + final_result = Some(Ok(result)); + break; + } + Err(e) => { + use rs_dapi_client::CanRetry; + + let err: DashCoreError = e.into(); + if err.can_retry() { + if i == MAX_RETRIES - 1 { + final_result = Some(Err(err)); + } + let delay = fibonacci(i + 2) * FIB_MULTIPLIER; + std::thread::sleep(Duration::from_millis(delay * BASE_TIME_MS)); + } else { + return Err(err); + } + } + } + } + final_result.expect("expected a final result") + }}; +} + impl Clone for LowLevelDashCoreClient { // As Client does not implement Clone, we just create a new instance of CoreClient here. fn clone(&self) -> Self { @@ -69,8 +115,7 @@ impl LowLevelDashCoreClient { let core = Client::new( &addr, Auth::UserPass(core_user.to_string(), core_password.to_string()), - ) - .map_err(Error::CoreClientError)?; + )?; Ok(Self { core: Mutex::new(core), @@ -98,28 +143,23 @@ impl LowLevelDashCoreClient { pub fn list_unspent( &self, minimum_sum_satoshi: Option, - ) -> Result, Error> { + ) -> Result, DashCoreError> { let options = json::ListUnspentQueryOptions { minimum_sum_amount: minimum_sum_satoshi.map(Amount::from_sat), ..Default::default() }; - self.core - .lock() - .expect("Core lock poisoned") - .list_unspent(None, None, None, None, Some(options)) - .map_err(Error::CoreClientError) + let core = self.core.lock().expect("Core lock poisoned"); + + retry!(core.list_unspent(None, None, None, None, Some(options.clone()))) } /// Return address to which change of transaction can be sent. #[allow(dead_code)] #[deprecated(note = "This function is marked as unused.")] - pub fn get_balance(&self) -> Result { - self.core - .lock() - .expect("Core lock poisoned") - .get_balance(None, None) - .map_err(Error::CoreClientError) + pub fn get_balance(&self) -> Result { + let core = self.core.lock().expect("Core lock poisoned"); + retry!(core.get_balance(None, None)) } /// Retrieve quorum public key from core. @@ -127,40 +167,42 @@ impl LowLevelDashCoreClient { &self, quorum_type: u32, quorum_hash: [u8; 32], - ) -> Result<[u8; 48], ContextProviderError> { + ) -> Result<[u8; 48], DashCoreError> { let quorum_hash = QuorumHash::from_slice(&quorum_hash) - .map_err(|e| ContextProviderError::InvalidQuorum(e.to_string()))?; + .map_err(|e| DashCoreError::InvalidQuorum(format!("invalid quorum hash: {}", e)))?; let core = self.core.lock().expect("Core lock poisoned"); - let quorum_info = core - .get_quorum_info(json::QuorumType::from(quorum_type), &quorum_hash, None) - .map_err(|e: dashcore_rpc::Error| ContextProviderError::Generic(e.to_string()))?; + + // Retrieve the quorum info + let quorum_info: json::QuorumInfoResult = + retry!(core.get_quorum_info(json::QuorumType::from(quorum_type), &quorum_hash, None))?; + + // Extract the quorum public key and attempt to convert it let key = quorum_info.quorum_public_key; - let pubkey = as TryInto<[u8; 48]>>::try_into(key).map_err(|_e| { - ContextProviderError::InvalidQuorum( - "quorum public key is not 48 bytes long".to_string(), - ) + let pubkey = as TryInto<[u8; 48]>>::try_into(key).map_err(|_| { + DashCoreError::InvalidQuorum("quorum public key is not 48 bytes long".to_string()) })?; + Ok(pubkey) } /// Retrieve platform activation height from core. - pub fn get_platform_activation_height(&self) -> Result { + pub fn get_platform_activation_height(&self) -> Result { let core = self.core.lock().expect("Core lock poisoned"); - let fork_info = core - .get_blockchain_info() - .map(|blockchain_info| blockchain_info.softforks.get("mn_rr").cloned()) - .map_err(|e: dashcore_rpc::Error| ContextProviderError::Generic(e.to_string()))? - .ok_or(ContextProviderError::ActivationForkError( - "no fork info for mn_rr".to_string(), - ))?; - - fork_info - .height - .ok_or(ContextProviderError::ActivationForkError( - "unknown fork height".to_string(), - )) + let blockchain_info = retry!(core.get_blockchain_info())?; + + let fork_info = + blockchain_info + .softforks + .get("mn_rr") + .ok_or(DashCoreError::ActivationForkError( + "no fork info for mn_rr".to_string(), + ))?; + + fork_info.height.ok_or(DashCoreError::ActivationForkError( + "unknown fork height".to_string(), + )) } /// Require list of validators from Core. @@ -171,15 +213,14 @@ impl LowLevelDashCoreClient { &self, height: Option, protx_type: Option, - ) -> Result, Error> { + ) -> Result, DashCoreError> { let core = self.core.lock().expect("Core lock poisoned"); - let pro_tx_hashes = - core.get_protx_list(protx_type, Some(false), height) - .map(|x| match x { - ProTxList::Hex(hex) => hex, - ProTxList::Info(info) => info.into_iter().map(|v| v.pro_tx_hash).collect(), - })?; + let pro_tx_list = retry!(core.get_protx_list(protx_type.clone(), Some(false), height))?; + let pro_tx_hashes = match pro_tx_list { + ProTxList::Hex(hex) => hex, + ProTxList::Info(info) => info.into_iter().map(|v| v.pro_tx_hash).collect(), + }; Ok(pro_tx_hashes) } diff --git a/packages/rs-sdk/src/core/error.rs b/packages/rs-sdk/src/core/error.rs new file mode 100644 index 0000000000..ec1b4925f6 --- /dev/null +++ b/packages/rs-sdk/src/core/error.rs @@ -0,0 +1,56 @@ +//! Errors that can occur in the Dash Core. + +use drive_proof_verifier::error::ContextProviderError; +use rs_dapi_client::CanRetry; + +/// Dash Core still warming up +pub const CORE_RPC_ERROR_IN_WARMUP: i32 = -28; +/// Dash Core Client is not connected +pub const CORE_RPC_CLIENT_NOT_CONNECTED: i32 = -9; +/// Dash Core still downloading initial blocks +pub const CORE_RPC_CLIENT_IN_INITIAL_DOWNLOAD: i32 = -10; + +#[derive(Debug, thiserror::Error)] +/// Errors that can occur when communicating with the Dash Core. +pub enum DashCoreError { + /// Error from Dash Core. + #[error("Dash Core RPC error: {0}")] + Rpc(#[from] dashcore_rpc::Error), + + /// Quorum is invalid. + #[error("Invalid quorum: {0}")] + InvalidQuorum(String), + + /// Fork activation error - most likely the fork is not activated yet. + #[error("Fork activation: {0}")] + ActivationForkError(String), +} + +impl From for ContextProviderError { + fn from(error: DashCoreError) -> Self { + match error { + DashCoreError::Rpc(e) => Self::DashCoreError(e.to_string()), + DashCoreError::InvalidQuorum(e) => Self::InvalidQuorum(e), + DashCoreError::ActivationForkError(e) => Self::ActivationForkError(e), + } + } +} + +impl CanRetry for DashCoreError { + fn can_retry(&self) -> bool { + use dashcore_rpc::jsonrpc::error::Error as JsonRpcError; + use dashcore_rpc::Error as RpcError; + match self { + DashCoreError::Rpc(RpcError::JsonRpc(JsonRpcError::Transport(..))) => true, + DashCoreError::Rpc(RpcError::JsonRpc(JsonRpcError::Rpc(e))) => { + matches!( + e.code, + CORE_RPC_ERROR_IN_WARMUP + | CORE_RPC_CLIENT_NOT_CONNECTED + | CORE_RPC_CLIENT_IN_INITIAL_DOWNLOAD, + ) + } + _ => false, + } + } +} diff --git a/packages/rs-sdk/src/core/mod.rs b/packages/rs-sdk/src/core/mod.rs index f642f3b26f..d5c4be9cd5 100644 --- a/packages/rs-sdk/src/core/mod.rs +++ b/packages/rs-sdk/src/core/mod.rs @@ -6,3 +6,5 @@ mod dash_core_client; mod transaction; #[cfg(feature = "mocks")] pub use dash_core_client::LowLevelDashCoreClient; +mod error; +pub use error::DashCoreError; diff --git a/packages/rs-sdk/src/core/transaction.rs b/packages/rs-sdk/src/core/transaction.rs index 39d196b57a..f3d2df46c2 100644 --- a/packages/rs-sdk/src/core/transaction.rs +++ b/packages/rs-sdk/src/core/transaction.rs @@ -98,7 +98,7 @@ impl Sdk { instant_send_lock_messages, ), ) => { - tracing::debug!( + tracing::trace!( "received {} instant lock message(s)", instant_send_lock_messages.messages.len() ); @@ -120,7 +120,7 @@ impl Sdk { output_index: 0, }); - tracing::debug!( + tracing::trace!( ?asset_lock_proof, "instant lock is matching to the broadcasted transaction, returning instant asset lock proof" ); @@ -136,7 +136,7 @@ impl Sdk { Some(transactions_with_proofs_response::Responses::RawMerkleBlock( raw_merkle_block, )) => { - tracing::debug!("received merkle block"); + tracing::trace!("received merkle block"); let merkle_block = MerkleBlock::consensus_decode(&mut raw_merkle_block.as_slice()) @@ -160,7 +160,7 @@ impl Sdk { continue; } - tracing::debug!( + tracing::trace!( "merkle block contains the transaction, obtaining core chain locked height" ); @@ -195,7 +195,7 @@ impl Sdk { sleep(Duration::from_secs(1)).await; } - tracing::debug!( + tracing::trace!( "the transaction is chainlocked on height {}, waiting platform for reaching the same core height", core_chain_locked_height ); @@ -226,7 +226,7 @@ impl Sdk { }, }); - tracing::debug!( + tracing::trace!( ?asset_lock_proof, "merkle block contains the broadcasted transaction, returning chain asset lock proof" ); @@ -236,9 +236,11 @@ impl Sdk { Some(transactions_with_proofs_response::Responses::RawTransactions(_)) => { tracing::trace!("received transaction(s), ignoring") } - None => tracing::trace!( - "received empty response as a workaround for the bug in tonic, ignoring" - ), + None => { + tracing::trace!( + "received empty response as a workaround for the bug in tonic, ignoring" + ) + } } } }; diff --git a/packages/rs-sdk/src/error.rs b/packages/rs-sdk/src/error.rs index 23def69d1a..75e114a800 100644 --- a/packages/rs-sdk/src/error.rs +++ b/packages/rs-sdk/src/error.rs @@ -10,6 +10,8 @@ use rs_dapi_client::{CanRetry, DapiClientError, ExecutionError}; use std::fmt::Debug; use std::time::Duration; +use crate::core::DashCoreError; + /// Error type for the SDK // TODO: Propagate server address and retry information so that the user can retrieve it #[derive(Debug, thiserror::Error)] @@ -44,7 +46,7 @@ pub enum Error { MerkleBlockError(#[from] dpp::dashcore::merkle_tree::MerkleBlockError), /// Core client error, for example, connection error #[error("Core client error: {0}")] - CoreClientError(#[from] dashcore_rpc::Error), + CoreClientError(#[from] DashCoreError), /// Dependency not found, for example data contract for a document not found #[error("Required {0} not found: {1}")] MissingDependency(String, String), @@ -125,9 +127,20 @@ where } } +impl From for Error { + fn from(value: dashcore_rpc::Error) -> Self { + Self::CoreClientError(value.into()) + } +} + impl CanRetry for Error { fn can_retry(&self) -> bool { - matches!(self, Error::StaleNode(..) | Error::TimeoutReached(_, _)) + match self { + Error::StaleNode(..) => true, + Error::TimeoutReached(..) => true, + Error::CoreClientError(e) => e.can_retry(), + _ => false, + } } } diff --git a/packages/rs-sdk/src/mock/provider.rs b/packages/rs-sdk/src/mock/provider.rs index 879c4137eb..ef0db5be92 100644 --- a/packages/rs-sdk/src/mock/provider.rs +++ b/packages/rs-sdk/src/mock/provider.rs @@ -215,7 +215,7 @@ impl ContextProvider for GrpcContextProvider { } fn get_platform_activation_height(&self) -> Result { - self.core.get_platform_activation_height() + Ok(self.core.get_platform_activation_height()?) } } diff --git a/packages/rs-sdk/src/sync.rs b/packages/rs-sdk/src/sync.rs index 5f5d266669..3b267f5a53 100644 --- a/packages/rs-sdk/src/sync.rs +++ b/packages/rs-sdk/src/sync.rs @@ -244,7 +244,6 @@ where mod test { use super::*; use derive_more::Display; - use http::Uri; use rs_dapi_client::ExecutionError; use std::{ future::Future, diff --git a/packages/rs-sdk/tests/fetch/config.rs b/packages/rs-sdk/tests/fetch/config.rs index f55484f5ce..5fc403560d 100644 --- a/packages/rs-sdk/tests/fetch/config.rs +++ b/packages/rs-sdk/tests/fetch/config.rs @@ -10,7 +10,7 @@ use dpp::{ }; use rs_dapi_client::{Address, AddressList}; use serde::Deserialize; -use std::{path::PathBuf, str::FromStr}; +use std::path::PathBuf; use zeroize::Zeroizing; /// Existing document ID diff --git a/packages/rs-sdk/tests/fetch/evonode.rs b/packages/rs-sdk/tests/fetch/evonode.rs index b2521ba864..9a01eabb9d 100644 --- a/packages/rs-sdk/tests/fetch/evonode.rs +++ b/packages/rs-sdk/tests/fetch/evonode.rs @@ -4,7 +4,6 @@ use super::{common::setup_logs, config::Config}; use dash_sdk::platform::{types::evonode::EvoNode, FetchUnproved}; use dpp::dashcore::{hashes::Hash, ProTxHash}; use drive_proof_verifier::types::EvoNodeStatus; -use http::Uri; use rs_dapi_client::Address; use std::time::Duration; /// Given some existing evonode URIs, WHEN we connect to them, THEN we get status.