diff --git a/Cargo.lock b/Cargo.lock index 3daafcea7d..9f6c164398 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1312,6 +1312,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bip37-bloom-filter", + "chrono", "ciborium", "clap 4.5.7", "dapi-grpc", diff --git a/packages/rs-drive-proof-verifier/src/from_request.rs b/packages/rs-drive-proof-verifier/src/from_request.rs index 0eee6607bb..ff2a74de36 100644 --- a/packages/rs-drive-proof-verifier/src/from_request.rs +++ b/packages/rs-drive-proof-verifier/src/from_request.rs @@ -300,7 +300,16 @@ impl TryFromRequest for VotePollsByDocumentTypeQue index_name: req.index_name.clone(), start_at_value: req .start_at_value_info - .map(|i| (i.start_value, i.start_value_included)), + .map(|i| { + let (value, _): (Value, _) = + bincode::decode_from_slice(&i.start_value, BINCODE_CONFIG).map_err( + |e| Error::RequestError { + error: format!("cannot decode start value: {}", e), + }, + )?; + Ok::<_, Error>((value, i.start_value_included)) + }) + .transpose()?, start_index_values: bincode_decode_values(req.start_index_values.iter())?, end_index_values: bincode_decode_values(req.end_index_values.iter())?, limit: req.count.map(|v| v as u16), @@ -320,14 +329,20 @@ impl TryFromRequest for VotePollsByDocumentTypeQue start_index_values: bincode_encode_values(&self.start_index_values)?, index_name: self.index_name.clone(), order_ascending: self.order_ascending, - start_at_value_info: self.start_at_value.as_ref().map( - |(start_value, start_value_included)| { - get_contested_resources_request_v0::StartAtValueInfo { - start_value: start_value.clone(), + start_at_value_info: self + .start_at_value + .as_ref() + .map(|(start_value, start_value_included)| { + Ok::<_, Error>(get_contested_resources_request_v0::StartAtValueInfo { + start_value: bincode::encode_to_vec(start_value, BINCODE_CONFIG).map_err( + |e| Error::RequestError { + error: format!("cannot encode start value: {}", e), + }, + )?, start_value_included: *start_value_included, - } - }, - ), + }) + }) + .transpose()?, } .into()) } diff --git a/packages/rs-drive-proof-verifier/src/lib.rs b/packages/rs-drive-proof-verifier/src/lib.rs index bee807bda6..bf38d555ef 100644 --- a/packages/rs-drive-proof-verifier/src/lib.rs +++ b/packages/rs-drive-proof-verifier/src/lib.rs @@ -9,7 +9,7 @@ mod provider; pub mod types; mod verify; pub use error::Error; -pub use proof::FromProof; +pub use proof::{FromProof, Length}; pub use provider::ContextProvider; #[cfg(feature = "mocks")] pub use provider::MockContextProvider; diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index b1a89489db..5955c040de 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -64,7 +64,7 @@ pub type DataContracts = RetrievedObjects; /// /// Mapping between the contenders identity IDs and their info. /// If a contender is not found, it is represented as `None`. -#[derive(Default)] +#[derive(Default, Debug, Clone)] #[cfg_attr( feature = "mocks", derive(Encode, Decode, PlatformSerialize, PlatformDeserialize,), @@ -159,12 +159,36 @@ pub type IdentityBalance = u64; pub type IdentityBalanceAndRevision = (u64, Revision); /// Contested resource values. -#[derive(Debug, derive_more::From, Clone)] +#[derive(Debug, derive_more::From, Clone, PartialEq)] pub enum ContestedResource { /// Generic [Value] Value(Value), } +impl ContestedResource { + /// Get the value. + pub fn encode_to_vec( + &self, + platform_version: &PlatformVersion, + ) -> Result, bincode::error::EncodeError> { + platform_serialization::platform_encode_to_vec( + self, + bincode::config::standard(), + platform_version, + ) + } +} + +impl TryInto for ContestedResource { + type Error = crate::Error; + + fn try_into(self) -> Result { + match self { + ContestedResource::Value(value) => Ok(value), + } + } +} + #[cfg(feature = "mocks")] impl PlatformVersionEncode for ContestedResource { fn platform_encode( @@ -173,7 +197,7 @@ impl PlatformVersionEncode for ContestedResource { _platform_version: &platform_version::PlatformVersion, ) -> Result<(), bincode::error::EncodeError> { match self { - ContestedResource::Value(document) => document.encode(encoder), + ContestedResource::Value(value) => value.encode(encoder), } } } @@ -248,7 +272,7 @@ pub type ResourceVotesByIdentity = RetrievedObjects; derive(Encode, Decode, PlatformSerialize, PlatformDeserialize), platform_serialize(unversioned) )] -pub struct PrefundedSpecializedBalance(Credits); +pub struct PrefundedSpecializedBalance(pub Credits); impl PrefundedSpecializedBalance { /// Get the balance. pub fn to_credits(&self) -> Credits { diff --git a/packages/rs-sdk/Cargo.toml b/packages/rs-sdk/Cargo.toml index afd978dc43..fd1a5498a6 100644 --- a/packages/rs-sdk/Cargo.toml +++ b/packages/rs-sdk/Cargo.toml @@ -51,6 +51,7 @@ data-contracts = { path = "../data-contracts" } tokio-test = { version = "0.4.4" } clap = { version = "4.5.4", features = ["derive"] } sanitize-filename = { version = "0.5.0" } +chrono = { version = "0.4.38" } [features] default = ["mocks", "offline-testing"] diff --git a/packages/rs-sdk/src/core_client.rs b/packages/rs-sdk/src/core_client.rs index acb2372077..656fff61b1 100644 --- a/packages/rs-sdk/src/core_client.rs +++ b/packages/rs-sdk/src/core_client.rs @@ -5,8 +5,11 @@ use dashcore_rpc::{ dashcore::{hashes::Hash, Amount, QuorumHash}, - dashcore_rpc_json as json, Auth, Client, RpcApi, + dashcore_rpc_json as json, + json::{ProTxList, ProTxListType}, + Auth, Client, RpcApi, }; +use dpp::dashcore::ProTxHash; use drive_proof_verifier::error::ContextProviderError; use std::{fmt::Debug, sync::Mutex}; @@ -126,4 +129,25 @@ impl CoreClient { })?; Ok(pubkey) } + + /// Require list of validators from Core. + /// + /// See also [Dash Core documentation](https://docs.dash.org/projects/core/en/stable/docs/api/remote-procedure-calls-evo.html#protx-list) + #[allow(unused)] + pub fn protx_list( + &self, + height: Option, + protx_type: Option, + ) -> Result, Error> { + 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(), + })?; + + Ok(pro_tx_hashes) + } } diff --git a/packages/rs-sdk/src/platform.rs b/packages/rs-sdk/src/platform.rs index 0718da718d..54667e92c2 100644 --- a/packages/rs-sdk/src/platform.rs +++ b/packages/rs-sdk/src/platform.rs @@ -30,5 +30,5 @@ pub use { document_query::DocumentQuery, fetch::Fetch, fetch_many::FetchMany, - query::{LimitQuery, Query, DEFAULT_EPOCH_QUERY_LIMIT}, + query::{LimitQuery, Query, QueryStartInfo, DEFAULT_EPOCH_QUERY_LIMIT}, }; diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index 21cba50abd..8750082200 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -3,6 +3,8 @@ //! [Query] trait is used to specify individual objects as well as search criteria for fetching multiple objects from the platform. use dapi_grpc::mock::Mockable; use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::GetContestedResourceIdentityVotesRequestV0; +use dapi_grpc::platform::v0::get_contested_resource_voters_for_identity_request::GetContestedResourceVotersForIdentityRequestV0; +use dapi_grpc::platform::v0::get_contested_resources_request::GetContestedResourcesRequestV0; use dapi_grpc::platform::v0::{ self as proto, get_identity_keys_request, get_identity_keys_request::GetIdentityKeysRequestV0, AllKeys, GetContestedResourceVoteStateRequest, GetContestedResourceVotersForIdentityRequest, @@ -15,6 +17,7 @@ use dapi_grpc::platform::v0::{ GetVotePollsByEndDateRequest, }; use dashcore_rpc::dashcore::{hashes::Hash, ProTxHash}; +use dpp::version::PlatformVersionError; use dpp::{block::epoch::EpochIndex, prelude::Identifier}; use drive::query::contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery; use drive::query::vote_poll_contestant_votes_query::ContestedDocumentVotePollVotesDriveQuery; @@ -317,6 +320,34 @@ impl Query for VotePollsByDocumentTypeQuery { } } +impl Query for LimitQuery { + fn query(self, prove: bool) -> Result { + use proto::get_contested_resources_request::{ + get_contested_resources_request_v0::StartAtValueInfo, Version, + }; + let query = match self.query.query(prove)?.version { + Some(Version::V0(v0)) => GetContestedResourcesRequestV0 { + start_at_value_info: self.start_info.map(|v| StartAtValueInfo { + start_value: v.start_key, + start_value_included: v.start_included, + }), + ..v0 + } + .into(), + None => { + return Err(Error::Protocol( + PlatformVersionError::UnknownVersionError( + "version not present in request".into(), + ) + .into(), + )) + } + }; + + Ok(query) + } +} + impl Query for ContestedDocumentVotePollDriveQuery { fn query(self, prove: bool) -> Result { if !prove { @@ -330,6 +361,33 @@ impl Query for ContestedDocumentVotePollDr } } +impl Query + for LimitQuery +{ + fn query(self, prove: bool) -> Result { + use proto::get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0::StartAtIdentifierInfo; + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + let result = match self.query.query(prove)?.version { + Some(proto::get_contested_resource_vote_state_request::Version::V0(v0)) => + proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 { + start_at_identifier_info: self.start_info.map(|v| StartAtIdentifierInfo { + start_identifier: v.start_key, + start_identifier_included: v.start_included, + }), + ..v0 + }.into(), + + None =>return Err(Error::Protocol( + PlatformVersionError::UnknownVersionError("version not present in request".into()).into(), + )), + }; + + Ok(result) + } +} + impl Query for ContestedDocumentVotePollVotesDriveQuery { @@ -345,6 +403,36 @@ impl Query } } +impl Query + for LimitQuery +{ + fn query(self, prove: bool) -> Result { + use proto::get_contested_resource_voters_for_identity_request::{ + get_contested_resource_voters_for_identity_request_v0::StartAtIdentifierInfo, Version, + }; + let query = match self.query.query(prove)?.version { + Some(Version::V0(v0)) => GetContestedResourceVotersForIdentityRequestV0 { + start_at_identifier_info: self.start_info.map(|v| StartAtIdentifierInfo { + start_identifier: v.start_key, + start_identifier_included: v.start_included, + }), + ..v0 + } + .into(), + None => { + return Err(Error::Protocol( + PlatformVersionError::UnknownVersionError( + "version not present in request".into(), + ) + .into(), + )) + } + }; + + Ok(query) + } +} + impl Query for ContestedResourceVotesGivenByIdentityQuery { @@ -360,6 +448,23 @@ impl Query } } +impl Query for ProTxHash { + fn query(self, prove: bool) -> Result { + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + Ok(GetContestedResourceIdentityVotesRequestV0 { + identity_id: self.to_byte_array().to_vec(), + prove, + limit: None, + offset: None, + order_ascending: true, + start_at_vote_poll_id_info: None, + } + .into()) + } +} + impl Query for VotePollsByEndDateDriveQuery { fn query(self, prove: bool) -> Result { if !prove { diff --git a/packages/rs-sdk/tests/fetch/common.rs b/packages/rs-sdk/tests/fetch/common.rs index e7847db9f2..12bfeb7761 100644 --- a/packages/rs-sdk/tests/fetch/common.rs +++ b/packages/rs-sdk/tests/fetch/common.rs @@ -72,7 +72,7 @@ pub fn mock_data_contract( pub fn setup_logs() { tracing_subscriber::fmt::fmt() .with_env_filter(tracing_subscriber::EnvFilter::new( - "info,dash_sdk=trace,drive_proof_verifier=trace,main=debug,h2=info", + "info,dash_sdk=trace,dash_sdk::platform::fetch=debug,drive_proof_verifier=debug,main=debug,h2=info", )) .pretty() .with_ansi(true) diff --git a/packages/rs-sdk/tests/fetch/contested_resource.rs b/packages/rs-sdk/tests/fetch/contested_resource.rs index 5ee7257fef..3d8aeda42b 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource.rs @@ -1,15 +1,34 @@ //! Tests of ContestedResource object use crate::fetch::{common::setup_logs, config::Config}; +use core::panic; use dash_sdk::platform::FetchMany; -use dpp::platform_value::Value; -use drive::query::vote_polls_by_document_type_query::VotePollsByDocumentTypeQuery; +use dpp::{ + platform_value::Value, + voting::{ + contender_structs::ContenderWithSerializedDocument, + vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll, + }, +}; +use drive::query::{ + vote_poll_vote_state_query::{ + ContestedDocumentVotePollDriveQuery, ContestedDocumentVotePollDriveQueryResultType, + }, + vote_polls_by_document_type_query::VotePollsByDocumentTypeQuery, +}; use drive_proof_verifier::types::ContestedResource; +pub(crate) const INDEX_VALUE: &str = "dada"; + +/// Test that we can fetch contested resources +/// +/// ## Preconditions +/// +/// 1. At least one contested resource (DPNS name) exists #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[cfg_attr( feature = "network-testing", - ignore = "requires a DPNS name to be registered" + ignore = "requires manual DPNS names setup for masternode voting tests; see fn check_mn_voting_prerequisities()" )] async fn test_contested_resources_ok() { setup_logs(); @@ -17,6 +36,9 @@ async fn test_contested_resources_ok() { let cfg = Config::new(); let sdk = cfg.setup_api("test_contested_resources_ok").await; + check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisities"); let index_name = "parentNameAndLabel"; @@ -34,6 +56,434 @@ async fn test_contested_resources_ok() { let rss = ContestedResource::fetch_many(&sdk, query) .await .expect("fetch contested resources"); - + tracing::debug!(contested_resources=?rss, "Contested resources"); assert!(!rss.0.is_empty()); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[cfg_attr( + feature = "network-testing", + ignore = "requires manual DPNS names setup for masternode voting tests; see fn check_mn_voting_prerequisities()" +)] +/// Test [ContestedResource] start index (`start_at_value`) +/// +/// ## Preconditions +/// +/// 1. At least 2 contested resources (eg. different DPNS names) exist +async fn contested_resources_start_at_value() { + setup_logs(); + + let cfg = Config::new(); + + let sdk = cfg.setup_api("contested_resources_start_at_value").await; + check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisities"); + + // Given all contested resources sorted ascending + let index_name = "parentNameAndLabel"; + for order_ascending in [true, false] { + let query_all = VotePollsByDocumentTypeQuery { + contract_id: cfg.existing_data_contract_id, + document_type_name: cfg.existing_document_type_name.clone(), + index_name: index_name.to_string(), + start_at_value: None, + start_index_values: vec![Value::Text("dash".into())], + end_index_values: vec![], + limit: Some(50), + order_ascending, + }; + + let all = ContestedResource::fetch_many(&sdk, query_all.clone()) + .await + .expect("fetch contested resources"); + + tracing::debug!(contested_resources=?all, order_ascending, "All contested resources"); + for inclusive in [true, false] { + // when I set start_at_value to some value, + for (i, start) in all.0.iter().enumerate() { + let ContestedResource::Value(start_value) = start.clone(); + + let query = VotePollsByDocumentTypeQuery { + start_at_value: Some((start_value, inclusive)), + ..query_all.clone() + }; + + let rss = ContestedResource::fetch_many(&sdk, query) + .await + .expect("fetch contested resources"); + tracing::debug!(?start, contested_resources=?rss, "Contested resources"); + + for (j, fetched) in rss.0.into_iter().enumerate() { + let all_index = if inclusive { i + j } else { i + j + 1 }; + + assert_eq!( + fetched, + (all.0[all_index]), + "when starting with {:?} order ascending {} with inclusive {}, fetched element {} ({:?}) must equal all element {} ({:?})", + start, + order_ascending, + inclusive, + j, + fetched, + all_index, + all.0[all_index] + ); + } + } + } + } +} + +/// Test that we can fetch contested resources with a limit +/// +/// ## Preconditions +/// +/// 1. At least 3 contested resources (eg. different DPNS names) exist +// TODO: fails due to PLAN-656, not tested enough so it can be faulty +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[cfg_attr( + feature = "network-testing", + ignore = "requires manual DPNS names setup for masternode voting tests; see fn check_mn_voting_prerequisities()" +)] +async fn contested_resources_limit() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg.setup_api("contested_resources_limit").await; + check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisities"); + + const LIMIT: u16 = 2; + const LIMIT_ALL: u16 = 100; + let index_name = "parentNameAndLabel"; + + for order_ascending in [true, false] { + let query_all = VotePollsByDocumentTypeQuery { + contract_id: cfg.existing_data_contract_id, + document_type_name: cfg.existing_document_type_name.clone(), + index_name: index_name.to_string(), + start_at_value: None, + start_index_values: vec![Value::Text("dash".into())], + end_index_values: vec![], + limit: Some(LIMIT_ALL), + order_ascending, + }; + let all = ContestedResource::fetch_many(&sdk, query_all.clone()) + .await + .expect("fetch contested resources"); + let count_all = all.0.len() as u16; + + // When we query for 2 contested values at a time, we get all of them + let mut i = 0; + let mut start_at_value = None; + while i < count_all && i < LIMIT_ALL { + let query = VotePollsByDocumentTypeQuery { + limit: Some(LIMIT), + start_at_value, + order_ascending, + ..query_all.clone() + }; + + let rss = ContestedResource::fetch_many(&sdk, query) + .await + .expect("fetch contested resources"); + tracing::debug!(contested_resources=?rss, "Contested resources"); + let length = rss.0.len(); + let expected = if i + LIMIT > count_all { + count_all - i + } else { + LIMIT + }; + assert_eq!(length, expected as usize); + tracing::debug!(contested_resources=?rss, i, "Contested resources"); + + for (j, fetched) in rss.0.iter().enumerate() { + let all_index = i + j as u16; + assert_eq!( + fetched, + &(all.0[all_index as usize]), + "fetched element {} ({:?}) must equal all element {} ({:?}) when ascending {}", + j, + fetched, + all_index, + all.0[all_index as usize], + order_ascending, + ); + } + + let ContestedResource::Value(last) = + rss.0.into_iter().last().expect("last contested resource"); + start_at_value = Some((last, false)); + + i += length as u16; + } + assert_eq!(i, count_all, "all contested resources fetched"); + } +} + +/// Check various queries for [ContestedResource] that contain invalid field values +/// +/// ## Preconditions +/// +/// None +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn contested_resources_fields() { + setup_logs(); + + type MutFn = fn(&mut VotePollsByDocumentTypeQuery); + struct TestCase { + name: &'static str, + query_mut_fn: MutFn, + expect: Result<&'static str, &'static str>, + } + + let test_cases: Vec = vec![ + TestCase { + name: "index value empty string is Ok", + query_mut_fn: |q| q.start_index_values = vec![Value::Text("".to_string())], + expect: Ok(""), + }, + TestCase { + name: "non existing document type returns InvalidArgument", + query_mut_fn: |q| q.document_type_name = "some random non-existing name".to_string(), + expect: Err( + r#"code: InvalidArgument, message: "document type some random non-existing name not found"#, + ), + }, + TestCase { + name: "non existing index returns InvalidArgument", + query_mut_fn: |q| q.index_name = "nx index".to_string(), + expect: Err( + r#"code: InvalidArgument, message: "index with name nx index is not the contested index"#, + ), + }, + TestCase { + name: "existing non-contested index returns InvalidArgument", + query_mut_fn: |q| q.index_name = "dashIdentityId".to_string(), + expect: Err( + r#"code: InvalidArgument, message: "index with name dashIdentityId is not the contested index"#, + ), + }, + TestCase { + // this fails with code: Internal, see PLAN-563 + name: "start_at_value wrong index type returns InvalidArgument PLAN-563", + query_mut_fn: |q| q.start_at_value = Some((Value::Array(vec![]), true)), + expect: Err(r#"code: InvalidArgument"#), + }, + TestCase { + name: "start_index_values empty vec returns top-level keys", + query_mut_fn: |q| q.start_index_values = vec![], + expect: Ok(r#"ContestedResources([Value(Text("dash"))])"#), + }, + TestCase { + name: "start_index_values empty string returns zero results", + query_mut_fn: |q| q.start_index_values = vec![Value::Text("".to_string())], + expect: Ok(r#"ContestedResources([])"#), + }, + TestCase { + // fails due to PLAN-662 + name: "start_index_values with two values PLAN-662", + query_mut_fn: |q| { + q.start_index_values = vec![ + Value::Text("dash".to_string()), + Value::Text("dada".to_string()), + ] + }, + expect: Ok(r#"ContestedResources([Value(Text("dash"))])"#), + }, + TestCase { + // fails due to PLAN-662 + name: "too many items in start_index_values PLAN-662", + query_mut_fn: |q| { + q.start_index_values = vec![ + Value::Text("dash".to_string()), + Value::Text("dada".to_string()), + Value::Text("eee".to_string()), + ] + }, + expect: Ok( + r#"code: InvalidArgument, message: "missing index values error: the start index values and the end index"#, + ), + }, + TestCase { + // fails due to PLAN-663 + name: "Non existing end_index_values PLAN-663", + query_mut_fn: |q| q.end_index_values = vec![Value::Text("non existing".to_string())], + expect: Ok(r#"ContestedResources([Value(Text("dash"))])"#), + }, + TestCase { + // fails due to PLAN-663 + name: "wrong type of end_index_values should return InvalidArgument PLAN-663", + query_mut_fn: |q| q.end_index_values = vec![Value::Array(vec![0.into(), 1.into()])], + expect: Ok(r#"code: InvalidArgument"#), + }, + TestCase { + // fails due to PLAN-664 + name: "limit 0 returns InvalidArgument PLAN-664", + query_mut_fn: |q| q.limit = Some(0), + expect: Ok(r#"code: InvalidArgument"#), + }, + TestCase { + name: "limit std::u16::MAX returns InvalidArgument PLAN-664", + query_mut_fn: |q| q.limit = Some(std::u16::MAX), + expect: Ok(r#"code: InvalidArgument"#), + }, + ]; + + let cfg = Config::new(); + + check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisities"); + + let base_query = VotePollsByDocumentTypeQuery { + contract_id: cfg.existing_data_contract_id, + document_type_name: cfg.existing_document_type_name.clone(), + index_name: "parentNameAndLabel".to_string(), + start_at_value: None, + // start_index_values: vec![], // Value(Text("dash")), Value(Text(""))]) + start_index_values: vec![Value::Text("dash".to_string())], + end_index_values: vec![], + limit: None, + order_ascending: false, + }; + + // check if the base query works + let base_query_sdk = cfg.setup_api("contested_resources_fields_base_query").await; + let result = ContestedResource::fetch_many(&base_query_sdk, base_query.clone()).await; + assert!( + result.is_ok_and(|v| !v.0.is_empty()), + "base query should return some results" + ); + + let mut failures: Vec<(&'static str, String)> = Default::default(); + + for test_case in test_cases { + tracing::debug!("Running test case: {}", test_case.name); + // create new sdk to ensure that test cases don't interfere with each other + let sdk = cfg + .setup_api(&format!("contested_resources_fields_{}", test_case.name)) + .await; + + let mut query = base_query.clone(); + (test_case.query_mut_fn)(&mut query); + + let result = ContestedResource::fetch_many(&sdk, query).await; + match test_case.expect { + Ok(expected) if result.is_ok() => { + let result_string = format!("{:?}", result.as_ref().expect("result")); + if !result_string.contains(expected) { + failures.push(( + test_case.name, + format!("expected: {:#?}\ngot: {:?}\n", expected, result), + )); + } + } + Err(expected) if result.is_err() => { + let result = result.expect_err("error"); + if !result.to_string().contains(expected) { + failures.push(( + test_case.name, + format!("expected: {:#?}\ngot {:?}\n", expected, result), + )); + } + } + expected => { + failures.push(( + test_case.name, + format!("expected: {:#?}\ngot: {:?}\n", expected, result), + )); + } + } + } + if !failures.is_empty() { + for failure in &failures { + tracing::error!(?failure, "Failed: {}", failure.0); + } + let failed_cases = failures + .iter() + .map(|(name, _)| name.to_string()) + .collect::>() + .join("\n* "); + + panic!( + "{} test cases failed:\n{}\n\n{}\n", + failures.len(), + failed_cases, + failures + .iter() + .map(|(name, msg)| format!("===========================\n{}:\n\n{:?}", name, msg)) + .collect::>() + .join("\n") + ); + } +} + +/// Ensure prerequsities for masternode voting tests are met +pub async fn check_mn_voting_prerequisities(cfg: &Config) -> Result<(), Vec> { + let sdk = cfg.setup_api("check_mn_voting_prerequisities").await; + let mut errors = Vec::new(); + + let index_name = "parentNameAndLabel".to_string(); + + let query_contested_resources = VotePollsByDocumentTypeQuery { + contract_id: cfg.existing_data_contract_id, + document_type_name: cfg.existing_document_type_name.clone(), + index_name: index_name.to_string(), + start_at_value: None, + start_index_values: vec![Value::Text("dash".into())], + end_index_values: vec![], + limit: None, + order_ascending: true, + }; + + // Check if we have enough contested resources; this implies that we have + // at least 1 vote poll for each of them + let contested_resources = ContestedResource::fetch_many(&sdk, query_contested_resources) + .await + .expect("fetch contested resources"); + if contested_resources.0.len() < 3 { + errors.push(format!( + "Please create at least 3 different DPNS names for masternode voting tests, found {}", + contested_resources.0.len() + )); + } + + // ensure we have enough contenders + let query_all = ContestedDocumentVotePollDriveQuery { + limit: None, + offset: None, + start_at: None, + vote_poll: ContestedDocumentResourceVotePoll { + index_name: "parentNameAndLabel".to_string(), + index_values: vec![ + Value::Text("dash".into()), + Value::Text(INDEX_VALUE.to_string()), + ], + document_type_name: cfg.existing_document_type_name.clone(), + contract_id: cfg.existing_data_contract_id, + }, + allow_include_locked_and_abstaining_vote_tally: true, + result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, + }; + + let all_contenders = ContenderWithSerializedDocument::fetch_many(&sdk, query_all.clone()) + .await + .expect("fetch many contenders"); + if all_contenders.contenders.len() < 3 { + errors.push(format!( + "Please create 3 identities and create DPNS name `{}` for each of them, found {}", + INDEX_VALUE, + all_contenders.contenders.len() + )); + } + + if errors.is_empty() { + Ok(()) + } else { + tracing::error!(?errors, "Prerequisities for masternode voting tests not met, please configure the network accordingly"); + Err(errors) + } +} diff --git a/packages/rs-sdk/tests/fetch/contested_resource_identity_votes.rs b/packages/rs-sdk/tests/fetch/contested_resource_identity_votes.rs index addda645e8..0902e88d77 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource_identity_votes.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource_identity_votes.rs @@ -2,10 +2,12 @@ use crate::fetch::{common::setup_logs, config::Config}; use dash_sdk::platform::FetchMany; -use dpp::voting::votes::resource_vote::ResourceVote; +use dpp::{ + dashcore::ProTxHash, identifier::Identifier, voting::votes::resource_vote::ResourceVote, +}; use drive::query::contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery; -/// Given some data contract ID, document type and document ID, when I fetch it, then I get it. +/// When we request votes for a non-existing identity, we should get no votes. #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn contested_resource_identity_votes_not_found() { setup_logs(); @@ -15,17 +17,53 @@ async fn contested_resource_identity_votes_not_found() { .setup_api("contested_resource_identity_votes_not_found") .await; + // Given some non-existing identity ID + let identity_id = Identifier::new([0xff; 32]); + + // When I query for votes given by this identity let query = ContestedResourceVotesGivenByIdentityQuery { - identity_id: cfg.existing_identity_id, + identity_id, limit: None, offset: None, order_ascending: true, start_at: None, }; - let votes = ResourceVote::fetch_many(&sdk, query) .await .expect("fetch votes for identity"); + // Then I get no votes assert!(votes.is_empty(), "no votes expected for this query"); } + +/// When we request votes for an existing identity, we should get some votes. +/// +/// ## Preconditions +/// +/// 1. At least one vote exists for the given masternode identity (protx hash). +#[cfg_attr( + feature = "network-testing", + ignore = "requires manual DPNS names setup for masternode voting tests; see fn check_mn_voting_prerequisities()" +)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn contested_resource_identity_votes_ok() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg.setup_api("contested_resource_identity_votes_ok").await; + + // Given some existing identity ID, that is, proTxHash of some Validator + + // TODO: Fetch proTxHash from the network instead of hardcoding; it's not so trivial as it must support our mocking + // mechanisms + let protx_hex = "7624E7D0D7C8837D4D02A19700F4116091A8AD145352420193DE8828F6D00BBF"; + let protx = ProTxHash::from_hex(protx_hex).expect("ProTxHash from hex"); + + // When I query for votes given by this identity + let votes = ResourceVote::fetch_many(&sdk, protx) + .await + .expect("fetch votes for identity"); + + // Then I get some votes + assert!(!votes.is_empty(), "votes expected for this query"); +} diff --git a/packages/rs-sdk/tests/fetch/contested_resource_polls_by_ts.rs b/packages/rs-sdk/tests/fetch/contested_resource_polls_by_ts.rs new file mode 100644 index 0000000000..3f970fa6ef --- /dev/null +++ b/packages/rs-sdk/tests/fetch/contested_resource_polls_by_ts.rs @@ -0,0 +1,208 @@ +//! Test VotePollsByEndDateDriveQuery + +use crate::fetch::{common::setup_logs, config::Config}; +use dash_sdk::platform::FetchMany; +use dpp::{identity::TimestampMillis, voting::vote_polls::VotePoll}; +use drive::query::VotePollsByEndDateDriveQuery; +use std::collections::BTreeMap; + +/// Test that we can fetch vote polls +/// +/// ## Preconditions +/// +/// 1. At least one vote poll exists +#[cfg_attr( + feature = "network-testing", + ignore = "requires manual DPNS names setup for masternode voting tests; see fn check_mn_voting_prerequisities()" +)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn vote_polls_by_ts_ok() { + setup_logs(); + + let cfg = Config::new(); + + let sdk = cfg.setup_api("vote_polls_by_ts_ok").await; + super::contested_resource::check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisities"); + + let query = VotePollsByEndDateDriveQuery { + limit: None, + offset: None, + order_ascending: true, + start_time: None, + end_time: None, + }; + + let rss = VotePoll::fetch_many(&sdk, query) + .await + .expect("fetch contested resources"); + tracing::info!("vote polls retrieved: {:?}", rss); + assert!(!rss.0.is_empty()); +} + +/// Test that we can fetch vote polls ordered by timestamp, ascending and descending +/// +/// ## Preconditions +/// +/// 1. At least 2 vote polls exist +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[cfg_attr( + feature = "network-testing", + ignore = "requires manual DPNS names setup for masternode voting tests; see fn check_mn_voting_prerequisities()" +)] +// fails due to PLAN-661 +async fn vote_polls_by_ts_order() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg.setup_api("vote_polls_by_ts_order").await; + super::contested_resource::check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisities"); + + let base_query = VotePollsByEndDateDriveQuery { + limit: None, + offset: None, + order_ascending: true, + start_time: None, + end_time: None, + }; + + for order_ascending in [true, false] { + let query = VotePollsByEndDateDriveQuery { + order_ascending, + ..base_query.clone() + }; + + let rss = VotePoll::fetch_many(&sdk, query) + .await + .expect("fetch contested resources"); + tracing::debug!(order_ascending, ?rss, "vote polls retrieved"); + assert!(!rss.0.is_empty()); + let enumerated = rss.0.iter().enumerate().collect::>(); + for (i, (ts, _)) in &enumerated { + if *i > 0 { + let (prev_ts, _) = &enumerated[&(i - 1)]; + if order_ascending { + assert!( + ts >= prev_ts, + "ascending order: item {} ({}) must be >= than item {} ({})", + ts, + i, + prev_ts, + i - 1 + ); + } else { + assert!( + ts <= prev_ts, + "descending order: item {} ({}) must be >= than item {} ({})", + ts, + i, + prev_ts, + i - 1 + ); + } + } + } + } +} + +/// Test that we can fetch vote polls with a limit +/// +/// ## Preconditions +/// +/// 1. At least 3 vote poll exists +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[cfg_attr( + feature = "network-testing", + ignore = "requires manual DPNS names setup for masternode voting tests; see fn check_mn_voting_prerequisities()" +)] + +// fails due to PLAN-659 +async fn vote_polls_by_ts_limit() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg.setup_api("vote_polls_by_ts_limit").await; + super::contested_resource::check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisities"); + + // Given index with more than 2 contested resources + const LIMIT: usize = 2; + const LIMIT_ALL: usize = 100; + + let test_start_time: TimestampMillis = chrono::Utc::now().timestamp_millis() as u64; + + let query_all = VotePollsByEndDateDriveQuery { + limit: Some(LIMIT_ALL as u16), + offset: None, + order_ascending: true, + start_time: None, + end_time: Some((test_start_time, true)), + }; + + let all = VotePoll::fetch_many(&sdk, query_all.clone()) + .await + .expect("fetch vote polls"); + // this counts timestamps, not vote polls themselves + let count_all_timestamps = all.0.len(); + assert_ne!(count_all_timestamps, 0, "at least one vote poll expected"); + + let all_values = all.0.into_iter().collect::>(); + + tracing::debug!(count_all_timestamps, "Count all"); + // When we query for 2 contested values at a time, we get all of them + let mut checked_count: usize = 0; + let mut start_time = None; + + for inclusive in [true, false] { + while checked_count < LIMIT_ALL { + let query = VotePollsByEndDateDriveQuery { + limit: Some(LIMIT as u16), + start_time, + ..query_all.clone() + }; + + let rss = VotePoll::fetch_many(&sdk, query) + .await + .expect("fetch vote polls"); + + let Some(last) = rss.0.keys().last().copied() else { + // no more vote polls + break; + }; + + tracing::debug!(polls=?rss, inclusive, ?start_time, checked_count, "Vote pools"); + let length = rss.0.len(); + + for (j, current) in rss.0.iter().enumerate() { + let all_idx = if inclusive && (j + checked_count > 0) { + j + checked_count - 1 + } else { + j + checked_count + }; + let expected = &all_values[all_idx]; + assert_eq!(*current.0, expected.0, "timestamp should match"); + assert_eq!(current.1, &expected.1, "vote polls should match"); + } + + let expected = if checked_count + LIMIT > count_all_timestamps { + count_all_timestamps - checked_count + } else { + LIMIT + }; + assert_eq!(length, expected as usize); + tracing::debug!(polls=?rss, checked_count, "Vote polls"); + + start_time = Some((last, inclusive)); + checked_count += if inclusive { length - 1 } else { length }; + } + } + assert_eq!( + checked_count, + count_all_timestamps * 2, + "all vote polls should be checked twice (inclusive and exclusive)" + ); +} diff --git a/packages/rs-sdk/tests/fetch/contested_resource_pools_by_ts.rs b/packages/rs-sdk/tests/fetch/contested_resource_pools_by_ts.rs deleted file mode 100644 index 805763d911..0000000000 --- a/packages/rs-sdk/tests/fetch/contested_resource_pools_by_ts.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Test VotePollsByEndDateDriveQuery - -use dash_sdk::platform::FetchMany; -use dpp::voting::vote_polls::VotePoll; -use drive::query::VotePollsByEndDateDriveQuery; - -use crate::fetch::{common::setup_logs, config::Config}; - -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -#[cfg_attr( - feature = "network-testing", - ignore = "requires a DPNS name to be registered" -)] -async fn test_vote_polls_by_ts_ok() { - setup_logs(); - - let cfg = Config::new(); - - let sdk = cfg.setup_api("test_vote_polls_by_ts_ok").await; - - let query = VotePollsByEndDateDriveQuery { - limit: None, - offset: None, - order_ascending: true, - start_time: None, - end_time: None, - }; - - let rss = VotePoll::fetch_many(&sdk, query) - .await - .expect("fetch contested resources"); - tracing::info!("vote polls retrieved: {:?}", rss); - assert!(!rss.0.is_empty()); -} diff --git a/packages/rs-sdk/tests/fetch/contested_resource_vote_state.rs b/packages/rs-sdk/tests/fetch/contested_resource_vote_state.rs index 18c8c1056f..e1d4e2e818 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource_vote_state.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource_vote_state.rs @@ -1,9 +1,16 @@ //! Tests for SDK requests that return one or more [Contender] objects. -use crate::fetch::{common::setup_logs, config::Config}; -use dash_sdk::platform::{DocumentQuery, Fetch, FetchMany}; +use crate::fetch::{ + common::setup_logs, config::Config, contested_resource::check_mn_voting_prerequisities, +}; +use dash_sdk::platform::{Fetch, FetchMany}; use dpp::{ - data_contract::DataContract, - document::Document, + data_contract::{accessors::v0::DataContractV0Getters, DataContract}, + document::{ + serialization_traits::DocumentPlatformConversionMethodsV0, Document, DocumentV0Getters, + }, + identifier::Identifier, + platform_value::Value, + util::strings::convert_to_homograph_safe_chars, voting::{ contender_structs::ContenderWithSerializedDocument, vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll, @@ -12,9 +19,8 @@ use dpp::{ use drive::query::vote_poll_vote_state_query::{ ContestedDocumentVotePollDriveQuery, ContestedDocumentVotePollDriveQueryResultType, }; -use std::sync::Arc; -/// Given some data contract ID, document type and document ID, when I fetch it, then I get it. +/// Ensure we get proof of non-existence when querying for a non-existing index value. #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn contested_resource_vote_states_not_found() { setup_logs(); @@ -23,30 +29,57 @@ async fn contested_resource_vote_states_not_found() { let sdk = cfg .setup_api("contested_resource_vote_states_not_found") .await; - + // Given some existing data contract ID and non-existing label let data_contract_id = cfg.existing_data_contract_id; + let label = "non existing name"; - let contract = Arc::new( - DataContract::fetch(&sdk, data_contract_id) - .await - .expect("fetch data contract") - .expect("data contract not found"), - ); + // When I query for vote poll states + let query = ContestedDocumentVotePollDriveQuery { + limit: None, + offset: None, + start_at: None, + vote_poll: ContestedDocumentResourceVotePoll { + index_name: "parentNameAndLabel".to_string(), + index_values: vec![label.into()], + document_type_name: cfg.existing_document_type_name, + contract_id: data_contract_id, + }, + allow_include_locked_and_abstaining_vote_tally: true, + // TODO test other result types + result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, + }; - // Fetch multiple documents so that we get document ID - let all_docs_query = - DocumentQuery::new(Arc::clone(&contract), &cfg.existing_document_type_name) - .expect("create SdkDocumentQuery"); - let first_doc = Document::fetch_many(&sdk, all_docs_query) + let contenders = ContenderWithSerializedDocument::fetch_many(&sdk, query) .await - .expect("fetch many documents") - .pop_first() - .expect("first item must exist") - .1 - .expect("document must exist"); - - tracing::info!("first_doc: {}", first_doc.to_string()); - // Now query for individual document + .expect("fetch many contenders"); + // Then I get no contenders + assert!( + contenders.contenders.is_empty(), + "no contenders expected for this query" + ); +} + +/// Asking for non-existing contract should return error. +/// +/// Note: due to the way error handling is implemented, this test will not work +/// correctly in offline mode. +#[cfg_attr( + feature = "offline-testing", + ignore = "offline mode does not support this test" +)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn contested_resource_vote_states_nx_contract() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg + .setup_api("contested_resource_vote_states_nx_contract") + .await; + + // Given some non-existing contract ID + let data_contract_id = Identifier::new([0xff; 32]); + + // When I query for votes referring this contract ID let query = ContestedDocumentVotePollDriveQuery { limit: None, offset: None, @@ -62,12 +95,392 @@ async fn contested_resource_vote_states_not_found() { result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, }; + // Then I get an error + let result = if let Err(e) = ContenderWithSerializedDocument::fetch_many(&sdk, query).await { + e + } else { + panic!("asking for non-existing contract should return error.") + }; + + if let dash_sdk::error::Error::DapiClientError(e) = result { + assert!( + e.contains( + "Transport(Status { code: InvalidArgument, message: \"contract not found error" + ), + "we should get contract not found error" + ); + } else { + panic!("expected 'contract not found' transport error"); + }; +} + +/// Ensure we can successfully query for existing index values. +/// +/// ## Preconditions +/// +/// 1. There must be at least one contender for name "dash" and value "dada". +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn contested_resource_vote_states_ok() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg.setup_api("contested_resource_vote_states_ok").await; + // Given some existing data contract and existing label + let data_contract_id = cfg.existing_data_contract_id; + let label = Value::Text(convert_to_homograph_safe_chars("dada")); + let document_type_name = "domain".to_string(); + + let data_contract = DataContract::fetch_by_identifier(&sdk, data_contract_id) + .await + .expect("fetch data contract") + .expect("found data contract"); + let document_type = data_contract + .document_type_for_name(&document_type_name) + .expect("found document type"); + + // When I query for vote poll states with existing index values + let query = ContestedDocumentVotePollDriveQuery { + limit: None, + offset: None, + start_at: None, + vote_poll: ContestedDocumentResourceVotePoll { + index_name: "parentNameAndLabel".to_string(), + index_values: vec![Value::Text("dash".into()), label], + document_type_name, + contract_id: data_contract_id, + }, + allow_include_locked_and_abstaining_vote_tally: true, + result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, + }; + let contenders = ContenderWithSerializedDocument::fetch_many(&sdk, query) .await .expect("fetch many contenders"); + tracing::debug!(contenders=?contenders, "Contenders"); + // Then I get contenders + assert!( + !contenders.contenders.is_empty(), + "contenders expected for this query" + ); + + // verify that the contenders have the expected properties and we don't have duplicates + let mut seen = std::collections::BTreeSet::new(); + for contender in contenders.contenders { + let serialized_document = contender + .1 + .serialized_document() + .as_ref() + .expect("serialized doc"); + + let doc = Document::from_bytes(serialized_document, document_type, sdk.version()) + .expect("doc from bytes"); + assert!(seen.insert(doc.id()), "duplicate contender"); + let properties = doc.properties(); + assert_eq!(properties["parentDomainName"], Value::Text("dash".into())); + assert_eq!(properties["label"], Value::Text("dada".into())); + tracing::debug!(?properties, "document properties"); + } +} +/// Ensure we can limit the number of returned contenders. +/// +/// ## Preconditions +/// +/// 1. There must be at least 3 condenders for name "dash" and value "dada". +/// +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn contested_resource_vote_states_with_limit() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg + .setup_api("contested_resource_vote_states_with_limit") + .await; + check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisites not met"); + + // Given more contenders for some `label` than the limit + let data_contract_id = cfg.existing_data_contract_id; + let limit: u16 = 2; + let label = Value::Text("dada".into()); + + // ensure we have enough contenders + let query_all = ContestedDocumentVotePollDriveQuery { + limit: None, + offset: None, + start_at: None, + vote_poll: ContestedDocumentResourceVotePoll { + index_name: "parentNameAndLabel".to_string(), + index_values: vec![Value::Text("dash".into()), label.clone()], + document_type_name: cfg.existing_document_type_name, + contract_id: data_contract_id, + }, + allow_include_locked_and_abstaining_vote_tally: true, + result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, + }; + + let all_contenders = ContenderWithSerializedDocument::fetch_many(&sdk, query_all.clone()) + .await + .expect("fetch many contenders") + .contenders + .len(); assert!( - contenders.contenders.is_empty(), - "no contenders expected for this query" + all_contenders > limit as usize, + "we need more than {} contenders for this test", + limit + ); + + // When I query for vote poll states with a limit + let query = ContestedDocumentVotePollDriveQuery { + limit: Some(limit), + ..query_all + }; + + let contenders = ContenderWithSerializedDocument::fetch_many(&sdk, query) + .await + .expect("fetch many contenders"); + // Then I get no more than the limit of contenders + tracing::debug!(contenders=?contenders, "Contenders"); + + assert_eq!( + contenders.contenders.len(), + limit as usize, + "number of contenders for {:?} should must be at least {}", + label, + limit ); } + +/// Check various queries for [ContenderWithSerializedDocument] that contain invalid field values +/// +/// ## Preconditions +/// +/// None +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn contested_resource_vote_states_fields() { + setup_logs(); + + type MutFn = fn(&mut ContestedDocumentVotePollDriveQuery); + struct TestCase { + name: &'static str, + query_mut_fn: MutFn, + expect: Result<&'static str, &'static str>, + } + + let test_cases: Vec = vec![ + TestCase { + name: "limit 0 PLAN-664", + query_mut_fn: |q| q.limit = Some(0), + expect: Ok("..."), + }, + TestCase { + name: "limit std::u16::MAX PLAN-664", + query_mut_fn: |q| q.limit = Some(std::u16::MAX), + expect: Ok("..."), + }, + TestCase { + name: "offset not None", + query_mut_fn: |q| q.offset = Some(1), + expect: Err( + r#"Generic("ContestedDocumentVotePollDriveQuery.offset field is internal and must be set to None")"#, + ), + }, + TestCase { + // TODO: pagination test + name: "start_at does not exist", + query_mut_fn: |q| q.start_at = Some(([0x11; 32], true)), + expect: Ok("Contenders { contenders: {Identifier("), + }, + TestCase { + name: "start_at 0xff;32", + query_mut_fn: |q| q.start_at = Some(([0xff; 32], true)), + expect: Ok("Contenders { contenders: {Identifier("), + }, + TestCase { + name: "non existing document type returns InvalidArgument", + query_mut_fn: |q| q.vote_poll.document_type_name = "nx doctype".to_string(), + expect: Err(r#"code: InvalidArgument, message: "document type nx doctype not found"#), + }, + TestCase { + name: "non existing index returns InvalidArgument", + query_mut_fn: |q| q.vote_poll.index_name = "nx index".to_string(), + expect: Err( + r#"code: InvalidArgument, message: "index with name nx index is not the contested index"#, + ), + }, + TestCase { + name: "existing non-contested index returns InvalidArgument", + query_mut_fn: |q| q.vote_poll.index_name = "dashIdentityId".to_string(), + expect: Err( + r#"code: InvalidArgument, message: "index with name dashIdentityId is not the contested index"#, + ), + }, + TestCase { + // todo maybe this should fail? or return everything? + name: "index_values empty vec returns zero results PLAN-665", + query_mut_fn: |q| q.vote_poll.index_values = vec![], + expect: Ok(r#"Contenders { contenders: {},"#), + }, + TestCase { + name: "index_values empty string returns zero results", + query_mut_fn: |q| q.vote_poll.index_values = vec![Value::Text("".to_string())], + expect: Ok("contenders: {}"), + }, + TestCase { + name: "index_values with one value returns results PLAN-665", + query_mut_fn: |q| q.vote_poll.index_values = vec![Value::Text("dash".to_string())], + expect: Ok("contenders: {...}"), + }, + TestCase { + name: "index_values with two values returns contenders ", + query_mut_fn: |q| { + q.vote_poll.index_values = vec![ + Value::Text("dash".to_string()), + Value::Text("dada".to_string()), + ] + }, + expect: Ok("contenders: {Identifier("), + }, + TestCase { + name: "index_values too many items should return error PLAN-665", + query_mut_fn: |q| { + q.vote_poll.index_values = vec![ + Value::Text("dash".to_string()), + Value::Text("dada".to_string()), + Value::Text("eee".to_string()), + ] + }, + expect: Ok( + r#"code: InvalidArgument, message: "missing index values error: the start index values and the end index"#, + ), + }, + TestCase { + name: "invalid contract id should cause InvalidArgument error", + query_mut_fn: |q| q.vote_poll.contract_id = Identifier::from([0xff; 32]), + expect: Err(r#"InvalidArgument, message: "contract not found error"#), + }, + TestCase { + name: + "allow_include_locked_and_abstaining_vote_tally false should return some contenders", + query_mut_fn: |q| q.allow_include_locked_and_abstaining_vote_tally = false, + expect: Ok(r#"contenders: {Identifier(IdentifierBytes32"#), + }, + TestCase { + name: "result_type Documents", + query_mut_fn: |q| { + q.result_type = ContestedDocumentVotePollDriveQueryResultType::Documents + }, + expect: Ok(r#"]), vote_tally: None })"#), + }, + TestCase { + name: "result_type DocumentsAndVoteTally", + query_mut_fn: |q| { + q.result_type = ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally + }, + expect: Ok(r#"]), vote_tally: Some("#), + }, + TestCase { + name: "result_type VoteTally", + query_mut_fn: |q| { + q.result_type = ContestedDocumentVotePollDriveQueryResultType::VoteTally + }, + expect: Ok(r#"serialized_document: None, vote_tally: Some"#), + }, + ]; + + let cfg = Config::new(); + check_mn_voting_prerequisities(&cfg) + .await + .expect("prerequisities"); + + let base_query = ContestedDocumentVotePollDriveQuery { + limit: None, + offset: None, + start_at: None, + vote_poll: ContestedDocumentResourceVotePoll { + index_name: "parentNameAndLabel".to_string(), + index_values: vec![Value::Text("dash".into()), Value::Text("dada".into())], + document_type_name: cfg.existing_document_type_name.clone(), + contract_id: cfg.existing_data_contract_id, + }, + allow_include_locked_and_abstaining_vote_tally: true, + result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally, + }; + + // check if the base query works + let base_query_sdk = cfg + .setup_api("contested_resource_vote_states_fields_base_query") + .await; + let result = + ContenderWithSerializedDocument::fetch_many(&base_query_sdk, base_query.clone()).await; + assert!( + result.is_ok_and(|v| !v.contenders.is_empty()), + "base query should return some results" + ); + + let mut failures: Vec<(&'static str, String)> = Default::default(); + + for test_case in test_cases { + tracing::debug!("Running test case: {}", test_case.name); + // create new sdk to ensure that test cases don't interfere with each other + let sdk = cfg + .setup_api(&format!( + "contested_resources_vote_states_fields_{}", + test_case.name + )) + .await; + + let mut query = base_query.clone(); + (test_case.query_mut_fn)(&mut query); + + let result = ContenderWithSerializedDocument::fetch_many(&sdk, query).await; + match test_case.expect { + Ok(expected) if result.is_ok() => { + let result_string = format!("{:?}", result.as_ref().expect("result")); + if !result_string.contains(expected) { + failures.push(( + test_case.name, + format!("expected: {:#?}\ngot: {:?}\n", expected, result), + )); + } + } + Err(expected) if result.is_err() => { + let result = result.expect_err("error"); + if !result.to_string().contains(expected) { + failures.push(( + test_case.name, + format!("expected: {:#?}\ngot {:?}\n", expected, result), + )); + } + } + expected => { + failures.push(( + test_case.name, + format!("expected: {:#?}\ngot: {:?}\n", expected, result), + )); + } + } + } + if !failures.is_empty() { + for failure in &failures { + tracing::error!(?failure, "Failed: {}", failure.0); + } + let failed_cases = failures + .iter() + .map(|(name, _)| name.to_string()) + .collect::>() + .join("\n* "); + + panic!( + "{} test cases failed:\n* {}\n\n{}\n", + failures.len(), + failed_cases, + failures + .iter() + .map(|(name, msg)| format!("===========================\n{}:\n\n{:?}", name, msg)) + .collect::>() + .join("\n") + ); + } +} diff --git a/packages/rs-sdk/tests/fetch/contested_resource_voters.rs b/packages/rs-sdk/tests/fetch/contested_resource_voters.rs index 9e507fe3a2..20ef80e0aa 100644 --- a/packages/rs-sdk/tests/fetch/contested_resource_voters.rs +++ b/packages/rs-sdk/tests/fetch/contested_resource_voters.rs @@ -1,11 +1,13 @@ //! Test GetContestedResourceVotersForIdentityRequest -use dash_sdk::platform::FetchMany; +use dash_sdk::platform::{Fetch, FetchMany}; +use dpp::{identifier::Identifier, identity::Identity, platform_value::Value}; use drive::query::vote_poll_contestant_votes_query::ContestedDocumentVotePollVotesDriveQuery; use drive_proof_verifier::types::Voter; use crate::fetch::{common::setup_logs, config::Config}; +/// When we request votes for a non-existing identity, we should get no votes. #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_contested_resource_voters_for_identity_not_found() { setup_logs(); @@ -16,6 +18,7 @@ async fn test_contested_resource_voters_for_identity_not_found() { .setup_api("test_contested_resource_voters_for_identity_not_found") .await; + let contestant_id = Identifier::new([0xff; 32]); let index_name = "parentNameAndLabel"; let query = ContestedDocumentVotePollVotesDriveQuery { @@ -29,7 +32,7 @@ async fn test_contested_resource_voters_for_identity_not_found() { index_name: index_name.to_string(), index_values: vec!["dash".into()], }, - contestant_id: cfg.existing_identity_id, + contestant_id, }; let rss = Voter::fetch_many(&sdk, query) @@ -38,3 +41,59 @@ async fn test_contested_resource_voters_for_identity_not_found() { assert!(rss.0.is_empty()); } + +/// When we request votes for an existing contestant, we should get some votes. +/// +/// ## Preconditions +/// +/// 1. Votes exist for the given contestant. +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn contested_resource_voters_for_existing_contestant() { + setup_logs(); + + let cfg = Config::new(); + let sdk = cfg + .setup_api("contested_resource_voters_for_existing_contestant") + .await; + + // Given a known contestant ID that has votes + // TODO: lookup contestant ID + let contestant_id = Identifier::from_string( + "D63rWKSagCgEE53XPkouP3swN9n87jHvjesFEZEh1cLr", + dpp::platform_value::string_encoding::Encoding::Base58, + ) + .expect("valid contestant ID"); + + let index_name = "parentNameAndLabel"; + let index_value = Value::Text("dada".to_string()); + // double-check that the contestant identity exist + let _contestant_identity = Identity::fetch(&sdk, contestant_id) + .await + .expect("fetch identity") + .expect("contestant identity must exist"); + + // When I query for votes given to this contestant + let query = ContestedDocumentVotePollVotesDriveQuery { + limit: None, + offset: None, + order_ascending: true, + start_at: None, + vote_poll: dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll { + contract_id: cfg.existing_data_contract_id, + document_type_name: cfg.existing_document_type_name, + index_name: index_name.to_string(), + index_values: vec!["dash".into(), index_value], + }, + contestant_id, + }; + + let rss = Voter::fetch_many(&sdk, query) + .await + .expect("fetch contested resources"); + + // We expect to find votes for the known contestant + assert!( + !rss.0.is_empty(), + "Expected to find votes for the existing contestant" + ); +} diff --git a/packages/rs-sdk/tests/fetch/mod.rs b/packages/rs-sdk/tests/fetch/mod.rs index bdcb4073d6..76e6c84c69 100644 --- a/packages/rs-sdk/tests/fetch/mod.rs +++ b/packages/rs-sdk/tests/fetch/mod.rs @@ -11,7 +11,7 @@ mod common; mod config; mod contested_resource; mod contested_resource_identity_votes; -mod contested_resource_pools_by_ts; +mod contested_resource_polls_by_ts; mod contested_resource_vote_state; mod contested_resource_voters; mod data_contract; diff --git a/packages/rs-sdk/tests/fetch/prefunded_specialized_balance.rs b/packages/rs-sdk/tests/fetch/prefunded_specialized_balance.rs index 8ab44ccd40..88d016783d 100644 --- a/packages/rs-sdk/tests/fetch/prefunded_specialized_balance.rs +++ b/packages/rs-sdk/tests/fetch/prefunded_specialized_balance.rs @@ -1,8 +1,9 @@ //! Test GetPrefundedSpecializedBalanceRequest use crate::fetch::{common::setup_logs, config::Config}; -use dash_sdk::platform::Fetch; -use dpp::identifier::Identifier; +use dash_sdk::platform::{Fetch, FetchMany}; +use dpp::{identifier::Identifier, voting::vote_polls::VotePoll}; +use drive::query::VotePollsByEndDateDriveQuery; use drive_proof_verifier::types::PrefundedSpecializedBalance; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -23,3 +24,52 @@ async fn test_prefunded_specialized_balance_not_found() { assert!(rss.is_none()); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_prefunded_specialized_balance_ok() { + setup_logs(); + + let cfg = Config::new(); + + let sdk = cfg.setup_api("test_prefunded_specialized_balance_ok").await; + + // Given some vote poll + let query = VotePollsByEndDateDriveQuery { + limit: None, + offset: None, + order_ascending: true, + start_time: None, + end_time: None, + }; + + let polls = VotePoll::fetch_many(&sdk, query) + .await + .expect("fetch vote polls"); + tracing::debug!("vote polls retrieved: {:?}", polls); + + let poll = polls + .0 + .first_key_value() + .expect("need at least one vote poll timestamp") + .1 + .first() + .expect("need at least one vote poll"); + + // Vote poll specialized balance ID + let balance_id = poll + .specialized_balance_id() + .expect("vote poll specialized balance ID") + .expect("must have specialized balance ID"); + + let balance = PrefundedSpecializedBalance::fetch(&sdk, balance_id) + .await + .expect("fetch prefunded specialized balance") + .expect("prefunded specialized balance expected for this query"); + + tracing::debug!(balance=?balance, "Prefunded specialized balance"); + + assert!( + balance.to_credits() > 0, + "prefunded specialized balance expected for this query" + ); +} diff --git a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_DocumentQuery_22942c9d389d6b8518bda36199d5789c2cb3ec52359f6119ce08b53acca4ee93.json b/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_DocumentQuery_22942c9d389d6b8518bda36199d5789c2cb3ec52359f6119ce08b53acca4ee93.json deleted file mode 100644 index affacd4a01..0000000000 Binary files a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_DocumentQuery_22942c9d389d6b8518bda36199d5789c2cb3ec52359f6119ce08b53acca4ee93.json and /dev/null differ diff --git a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetContestedResourceVoteStateRequest_a7fa439911e16ae02a457f40a4a3968800dd88d3cd55c96ddc7005206ae8a91f.json b/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetContestedResourceVoteStateRequest_d45be23de4d7a849521ffc62a060307684913cd9df63fe77d5bfbec1351fc28d.json similarity index 81% rename from packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetContestedResourceVoteStateRequest_a7fa439911e16ae02a457f40a4a3968800dd88d3cd55c96ddc7005206ae8a91f.json rename to packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetContestedResourceVoteStateRequest_d45be23de4d7a849521ffc62a060307684913cd9df63fe77d5bfbec1351fc28d.json index b570da68b8..461e5ec44f 100644 Binary files a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetContestedResourceVoteStateRequest_a7fa439911e16ae02a457f40a4a3968800dd88d3cd55c96ddc7005206ae8a91f.json and b/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetContestedResourceVoteStateRequest_d45be23de4d7a849521ffc62a060307684913cd9df63fe77d5bfbec1351fc28d.json differ diff --git a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetDataContractRequest_e87a2e6acef76975c30eb7272da71733fb6ad13495beb7ca1b6a6c4ceb30e0f7.json b/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetDataContractRequest_e87a2e6acef76975c30eb7272da71733fb6ad13495beb7ca1b6a6c4ceb30e0f7.json index 35d918b6b3..cd23871fb3 100644 Binary files a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetDataContractRequest_e87a2e6acef76975c30eb7272da71733fb6ad13495beb7ca1b6a6c4ceb30e0f7.json and b/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/msg_GetDataContractRequest_e87a2e6acef76975c30eb7272da71733fb6ad13495beb7ca1b6a6c4ceb30e0f7.json differ diff --git a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/quorum_pubkey-106-01f298bc2eaa95b3d60a818e7e07d5e2c4adee52c750b8024bb58964b3a48a76.json b/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/quorum_pubkey-106-01f298bc2eaa95b3d60a818e7e07d5e2c4adee52c750b8024bb58964b3a48a76.json new file mode 100644 index 0000000000..fed490402e --- /dev/null +++ b/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/quorum_pubkey-106-01f298bc2eaa95b3d60a818e7e07d5e2c4adee52c750b8024bb58964b3a48a76.json @@ -0,0 +1 @@ +9838eb549dc11d015245e1c764a0e69aba65975e8c08faa8f2ff03c48465892794b60301cba3cfdd298cff2c01d12e3c \ No newline at end of file diff --git a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/quorum_pubkey-106-492dae0f1dd6ead4f24c870bd0c119aacf74ce5a6e2b6587fd5a1b4dd74623ae.json b/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/quorum_pubkey-106-492dae0f1dd6ead4f24c870bd0c119aacf74ce5a6e2b6587fd5a1b4dd74623ae.json deleted file mode 100644 index ccd69e3827..0000000000 --- a/packages/rs-sdk/tests/vectors/contested_resource_vote_states_not_found/quorum_pubkey-106-492dae0f1dd6ead4f24c870bd0c119aacf74ce5a6e2b6587fd5a1b4dd74623ae.json +++ /dev/null @@ -1 +0,0 @@ -aa51132a4e62cec89b731aa8baf4043fea777c484a4856e3b7c9a13c651ff2a9057d30cd251190bde749ac8f7c0026b6 \ No newline at end of file