diff --git a/packages/rs-drive/src/query/mod.rs b/packages/rs-drive/src/query/mod.rs index 7491454a647..f6aa81deb21 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -54,7 +54,7 @@ use dpp::document; use dpp::prelude::Identifier; #[cfg(feature = "server")] use { - crate::{drive::Drive, error::Error::GroveDB, fees::op::LowLevelDriveOperation}, + crate::{drive::Drive, fees::op::LowLevelDriveOperation}, dpp::block::block_info::BlockInfo, }; // Crate-local unconditional imports @@ -141,6 +141,26 @@ pub mod drive_contested_document_query; /// A query to get the block counts of proposers in an epoch pub mod proposer_block_count_query; +#[cfg(any(feature = "server", feature = "verify"))] +/// Represents a starting point for a query based on a specific document. +/// +/// This struct encapsulates all the necessary details to define the starting +/// conditions for a query, including the document to start from, its type, +/// associated index property, and whether the document itself should be included +/// in the query results. +#[derive(Debug, Clone)] +pub struct StartAtDocument<'a> { + /// The document that serves as the starting point for the query. + pub document: Document, + + /// The type of the document, providing metadata about its schema and structure. + pub document_type: DocumentTypeRef<'a>, + + /// Indicates whether the starting document itself should be included in the query results. + /// - `true`: The document is included in the results. + /// - `false`: The document is excluded, and the query starts from the next matching document. + pub included: bool, +} #[cfg(any(feature = "server", feature = "verify"))] /// Internal clauses struct #[derive(Clone, Debug, PartialEq, Default)] @@ -898,7 +918,7 @@ impl<'a> DriveDocumentQuery<'a> { let (starts_at_document, start_at_path_query) = match &self.start_at { None => Ok((None, None)), Some(starts_at) => { - // First if we have a startAt or or startsAfter we must get the element + // First if we have a startAt or startsAfter we must get the element // from the backing store let (start_at_document_path, start_at_document_key) = @@ -970,7 +990,7 @@ impl<'a> DriveDocumentQuery<'a> { vec![&start_at_path_query, &main_path_query], &platform_version.drive.grove_version, ) - .map_err(GroveDB)?; + .map_err(Error::GroveDB)?; merged.query.limit = limit.map(|a| a.saturating_add(1)); Ok(merged) } else { @@ -1252,13 +1272,16 @@ impl<'a> DriveDocumentQuery<'a> { #[cfg(any(feature = "server", feature = "verify"))] /// Returns a `Query` that either starts at or after the given document ID if given. fn inner_query_from_starts_at_for_id( - starts_at_document: &Option<(Document, DocumentTypeRef, &IndexProperty, bool)>, + starts_at_document: Option<&StartAtDocument>, left_to_right: bool, ) -> Query { // We only need items after the start at document let mut inner_query = Query::new_with_direction(left_to_right); - if let Some((document, _, _, included)) = starts_at_document { + if let Some(StartAtDocument { + document, included, .. + }) = starts_at_document + { let start_at_key = document.id().to_vec(); if *included { inner_query.insert_range_from(start_at_key..) @@ -1313,18 +1336,19 @@ impl<'a> DriveDocumentQuery<'a> { #[cfg(any(feature = "server", feature = "verify"))] /// Returns a `Query` that either starts at or after the given document if given. - // We are passing in starts_at_document 4 parameters - // The document - // The document type (borrowed) - // The index property (borrowed) - // if the element itself should be included. ie StartAt vs StartAfter fn inner_query_from_starts_at( - starts_at_document: &Option<(Document, DocumentTypeRef, &IndexProperty, bool)>, + starts_at_document: Option<&StartAtDocument>, + indexed_property: &IndexProperty, left_to_right: bool, platform_version: &PlatformVersion, ) -> Result { let mut inner_query = Query::new_with_direction(left_to_right); - if let Some((document, document_type, indexed_property, included)) = starts_at_document { + if let Some(StartAtDocument { + document, + document_type, + included, + }) = starts_at_document + { // We only need items after the start at document let start_at_key = document.get_raw_for_document_type( indexed_property.name.as_str(), @@ -1357,55 +1381,171 @@ impl<'a> DriveDocumentQuery<'a> { Ok(inner_query) } + #[cfg(any(feature = "server", feature = "verify"))] + fn recursive_create_query( + left_over_index_properties: &[&IndexProperty], + unique: bool, + starts_at_document: Option<&StartAtDocument>, //for key level, included + indexed_property: &IndexProperty, + order_by: Option<&IndexMap>, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match left_over_index_properties.split_first() { + None => Ok(None), + Some((first, left_over)) => { + let left_to_right = if let Some(order_by) = order_by { + order_by + .get(first.name.as_str()) + .map(|order_clause| order_clause.ascending) + .unwrap_or(first.ascending) + } else { + first.ascending + }; + + let mut inner_query = Self::inner_query_from_starts_at( + starts_at_document, + indexed_property, + left_to_right, + platform_version, + )?; + DriveDocumentQuery::recursive_insert_on_query( + &mut inner_query, + left_over, + unique, + starts_at_document, + left_to_right, + order_by, + platform_version, + )?; + Ok(Some(inner_query)) + } + } + } + #[cfg(any(feature = "server", feature = "verify"))] /// Recursively queries as long as there are leftover index properties. + /// The in_start_at_document_sub_path_needing_conditional is interesting. + /// It indicates whether the start at document should be applied as a conditional + /// For example if we have a tree + /// Root + /// ├── model + /// │ ├── sedan + /// │ │ ├── brand_name + /// │ │ │ ├── Honda + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── Accord + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── a47d2... + /// │ │ │ │ │ │ │ ├── e19c8... + /// │ │ │ │ │ │ │ └── f1a7b... + /// │ │ │ │ │ └── Civic + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── b65a7... + /// │ │ │ │ │ │ └── c43de... + /// │ │ │ ├── Toyota + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── Camry + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ └── 1a9d2... + /// │ │ │ │ │ └── Corolla + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── 3f7b4... + /// │ │ │ │ │ │ ├── 4e8fa... + /// │ │ │ │ │ │ └── 9b1c6... + /// │ ├── suv + /// │ │ ├── brand_name + /// │ │ │ ├── Ford* + /// │ │ │ │ ├── car_type* + /// │ │ │ │ │ ├── Escape* + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── 102bc... + /// │ │ │ │ │ │ │ ├── 29f8e... <- Set After this document + /// │ │ │ │ │ │ │ └── 6b1a3... + /// │ │ │ │ │ └── Explorer + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── b2a9d... + /// │ │ │ │ │ │ └── f4d5c... + /// │ │ │ ├── Nissan + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── Rogue + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── 5a9c3... + /// │ │ │ │ │ │ │ └── 7e4b9... + /// │ │ │ │ │ └── Murano + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── 8f6a2... + /// │ │ │ │ │ │ └── 9c7d4... + /// │ ├── truck + /// │ │ ├── brand_name + /// │ │ │ ├── Ford + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── F-150 + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── 72a3b... + /// │ │ │ │ │ │ │ └── 94c8e... + /// │ │ │ │ │ └── Ranger + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── 3f4b1... + /// │ │ │ │ │ │ ├── 6e7d2... + /// │ │ │ │ │ │ └── 8a1f5... + /// │ │ │ ├── Toyota + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── Tundra + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── 7c9a4... + /// │ │ │ │ │ │ │ └── a5d1e... + /// │ │ │ │ │ └── Tacoma + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── 1e7f4... + /// │ │ │ │ │ │ └── 6b9d3... + /// + /// let's say we are asking for suv's after 29f8e + /// here the * denotes the area needing a conditional + /// We need a conditional subquery on Ford to say only things after Ford (with Ford included) + /// We need a conditional subquery on Escape to say only things after Escape (with Escape included) fn recursive_insert_on_query( - query: Option<&mut Query>, + query: &mut Query, left_over_index_properties: &[&IndexProperty], unique: bool, - starts_at_document: &Option<(Document, DocumentTypeRef, &IndexProperty, bool)>, //for key level, included + starts_at_document: Option<&StartAtDocument>, //for key level, included default_left_to_right: bool, order_by: Option<&IndexMap>, platform_version: &PlatformVersion, ) -> Result, Error> { match left_over_index_properties.split_first() { None => { - if let Some(query) = query { - match unique { - true => { - query.set_subquery_key(vec![0]); - - // In the case things are NULL we allow to have multiple values - let inner_query = Self::inner_query_from_starts_at_for_id( - starts_at_document, - true, //for ids we always go left to right - ); - query.add_conditional_subquery( - QueryItem::Key(b"".to_vec()), - Some(vec![vec![0]]), - Some(inner_query), - ); - } - false => { - query.set_subquery_key(vec![0]); - // we just get all by document id order ascending - let full_query = Self::inner_query_from_starts_at_for_id( - &None, - default_left_to_right, - ); - query.set_subquery(full_query); - - let inner_query = Self::inner_query_from_starts_at_for_id( - starts_at_document, - default_left_to_right, - ); - - query.add_conditional_subquery( - QueryItem::Key(b"".to_vec()), - Some(vec![vec![0]]), - Some(inner_query), - ); - } + match unique { + true => { + query.set_subquery_key(vec![0]); + + // In the case things are NULL we allow to have multiple values + let inner_query = Self::inner_query_from_starts_at_for_id( + starts_at_document, + true, //for ids we always go left to right + ); + query.add_conditional_subquery( + QueryItem::Key(b"".to_vec()), + Some(vec![vec![0]]), + Some(inner_query), + ); + } + false => { + query.set_subquery_key(vec![0]); + // we just get all by document id order ascending + let full_query = + Self::inner_query_from_starts_at_for_id(None, default_left_to_right); + query.set_subquery(full_query); + + let inner_query = Self::inner_query_from_starts_at_for_id( + starts_at_document, + default_left_to_right, + ); + + query.add_conditional_subquery( + QueryItem::Key(b"".to_vec()), + Some(vec![vec![0]]), + Some(inner_query), + ); } } Ok(None) @@ -1420,79 +1560,223 @@ impl<'a> DriveDocumentQuery<'a> { first.ascending }; - match query { - None => { - let mut inner_query = Self::inner_query_from_starts_at( - starts_at_document, - left_to_right, - platform_version, - )?; - DriveDocumentQuery::recursive_insert_on_query( - Some(&mut inner_query), - left_over, - unique, - starts_at_document, - left_to_right, - order_by, + if let Some(start_at_document_inner) = starts_at_document { + let StartAtDocument { + document, + document_type, + included, + } = start_at_document_inner; + let start_at_key = document + .get_raw_for_document_type( + first.name.as_str(), + *document_type, + None, platform_version, - )?; - Ok(Some(inner_query)) - } - Some(query) => { - if let Some((document, document_type, _indexed_property, included)) = - starts_at_document - { - let start_at_key = document - .get_raw_for_document_type( - first.name.as_str(), - *document_type, - None, - platform_version, - ) - .ok() - .flatten(); - - // We should always include if we have left_over - let non_conditional_included = - !left_over.is_empty() | *included | start_at_key.is_none(); - - let mut non_conditional_query = Self::inner_query_starts_from_key( - start_at_key, - left_to_right, - non_conditional_included, - ); - - DriveDocumentQuery::recursive_insert_on_query( - Some(&mut non_conditional_query), - left_over, - unique, - starts_at_document, - left_to_right, - order_by, - platform_version, - )?; + ) + .ok() + .flatten(); + + // We should always include if we have left_over + let non_conditional_included = + !left_over.is_empty() || *included || start_at_key.is_none(); + + let mut non_conditional_query = Self::inner_query_starts_from_key( + start_at_key.clone(), + left_to_right, + non_conditional_included, + ); + + // We place None here on purpose, this has been well-thought-out + // and should not change. The reason is that the path of the start + // at document is used only on the conditional subquery and not on the + // main query + // for example in the following + // Our query will be with $ownerId == a3f9b81c4d7e6a9f5b1c3e8a2d9c4f7b + // With start after 8f2d5 + // We want to get from 2024-11-17T12:45:00Z + // withdrawal + // ├── $ownerId + // │ ├── a3f9b81c4d7e6a9f5b1c3e8a2d9c4f7b + // │ │ ├── $updatedAt + // │ │ │ ├── 2024-11-17T12:45:00Z <- conditional subquery here + // │ │ │ │ ├── status + // │ │ │ │ │ ├── 0 + // │ │ │ │ │ │ ├── 7a9f1... + // │ │ │ │ │ │ └── 4b8c3... + // │ │ │ │ │ ├── 1 + // │ │ │ │ │ │ ├── 8f2d5... <- start after + // │ │ │ │ │ │ └── 5c1e4... + // │ │ │ │ │ ├── 2 + // │ │ │ │ │ │ ├── 2e7a9... + // │ │ │ │ │ │ └── 1c8b3... + // │ │ │ ├── 2024-11-18T11:25:00Z <- we want all statuses here, so normal subquery, with None as start at document + // │ │ │ │ ├── status + // │ │ │ │ │ ├── 0 + // │ │ │ │ │ │ └── 1a4f2... + // │ │ │ │ │ ├── 2 + // │ │ │ │ │ │ ├── 3e7a9... + // │ │ │ │ │ │ └── 198b4... + // │ ├── b6d7e9c4a5f2b3d8e1a7c9f4b1e8a3f + // │ │ ├── $updatedAt + // │ │ │ ├── 2024-11-17T13:30:00Z + // │ │ │ │ ├── status + // │ │ │ │ │ ├── 0 + // │ │ │ │ │ │ ├── 6d7e2... + // │ │ │ │ │ │ └── 9c7f5... + // │ │ │ │ │ ├── 3 + // │ │ │ │ │ │ ├── 3a9b7... + // │ │ │ │ │ │ └── 8e5c4... + // │ │ │ │ │ ├── 4 + // │ │ │ │ │ │ ├── 1f7a8... + // │ │ │ │ │ │ └── 2c9b3... + // println!("going to call recursive_insert_on_query on non_conditional_query {} with left_over {:?}", non_conditional_query, left_over); + DriveDocumentQuery::recursive_insert_on_query( + &mut non_conditional_query, + left_over, + unique, + None, + left_to_right, + order_by, + platform_version, + )?; - query.set_subquery(non_conditional_query); - } else { - let mut inner_query = Query::new_with_direction(first.ascending); - inner_query.insert_all(); - DriveDocumentQuery::recursive_insert_on_query( - Some(&mut inner_query), - left_over, - unique, - starts_at_document, - left_to_right, - order_by, - platform_version, - )?; - query.set_subquery(inner_query); - } - query.set_subquery_key(first.name.as_bytes().to_vec()); - Ok(None) + DriveDocumentQuery::recursive_conditional_insert_on_query( + &mut non_conditional_query, + start_at_key, + left_over, + unique, + start_at_document_inner, + left_to_right, + order_by, + platform_version, + )?; + + query.set_subquery(non_conditional_query); + } else { + let mut inner_query = Query::new_with_direction(first.ascending); + inner_query.insert_all(); + DriveDocumentQuery::recursive_insert_on_query( + &mut inner_query, + left_over, + unique, + starts_at_document, + left_to_right, + order_by, + platform_version, + )?; + query.set_subquery(inner_query); + } + query.set_subquery_key(first.name.as_bytes().to_vec()); + Ok(None) + } + } + } + + #[cfg(any(feature = "server", feature = "verify"))] + fn recursive_conditional_insert_on_query( + query: &mut Query, + conditional_value: Option>, + left_over_index_properties: &[&IndexProperty], + unique: bool, + starts_at_document: &StartAtDocument, + default_left_to_right: bool, + order_by: Option<&IndexMap>, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match left_over_index_properties.split_first() { + None => { + match unique { + true => { + // In the case things are NULL we allow to have multiple values + let inner_query = Self::inner_query_from_starts_at_for_id( + Some(starts_at_document), + true, //for ids we always go left to right + ); + query.add_conditional_subquery( + QueryItem::Key(b"".to_vec()), + Some(vec![vec![0]]), + Some(inner_query), + ); + } + false => { + let inner_query = Self::inner_query_from_starts_at_for_id( + Some(starts_at_document), + default_left_to_right, + ); + + query.add_conditional_subquery( + QueryItem::Key(conditional_value.unwrap_or_default()), + Some(vec![vec![0]]), + Some(inner_query), + ); } } } + Some((first, left_over)) => { + let left_to_right = if let Some(order_by) = order_by { + order_by + .get(first.name.as_str()) + .map(|order_clause| order_clause.ascending) + .unwrap_or(first.ascending) + } else { + first.ascending + }; + + let StartAtDocument { + document, + document_type, + .. + } = starts_at_document; + + let lower_start_at_key = document + .get_raw_for_document_type( + first.name.as_str(), + *document_type, + None, + platform_version, + ) + .ok() + .flatten(); + + // We include it if we are not unique, + // or if we are unique but the value is empty + let non_conditional_included = !unique || lower_start_at_key.is_none(); + + let mut non_conditional_query = Self::inner_query_starts_from_key( + lower_start_at_key.clone(), + left_to_right, + non_conditional_included, + ); + + DriveDocumentQuery::recursive_insert_on_query( + &mut non_conditional_query, + left_over, + unique, + None, + left_to_right, + order_by, + platform_version, + )?; + + DriveDocumentQuery::recursive_conditional_insert_on_query( + &mut non_conditional_query, + lower_start_at_key, + left_over, + unique, + starts_at_document, + left_to_right, + order_by, + platform_version, + )?; + + query.add_conditional_subquery( + QueryItem::Key(conditional_value.unwrap_or_default()), + Some(vec![first.name.as_bytes().to_vec()]), + Some(non_conditional_query), + ); + } } + Ok(()) } #[cfg(any(feature = "server", feature = "verify"))] @@ -1529,8 +1813,7 @@ impl<'a> DriveDocumentQuery<'a> { !(self .internal_clauses .equal_clauses - .get(field.name.as_str()) - .is_some() + .contains_key(field.name.as_str()) || (last_clause.is_some() && last_clause.unwrap().field == field.name) || (subquery_clause.is_some() && subquery_clause.unwrap().field == field.name)) }) @@ -1569,14 +1852,17 @@ impl<'a> DriveDocumentQuery<'a> { let first_index = index.properties.first().ok_or(Error::Drive( DriveError::CorruptedContractIndexes("index must have properties".to_string()), ))?; // Index must have properties - Self::recursive_insert_on_query( - None, + Self::recursive_create_query( left_over_index_properties.as_slice(), index.unique, - &starts_at_document.map(|(document, included)| { - (document, self.document_type, first_index, included) - }), - first_index.ascending, + starts_at_document + .map(|(document, included)| StartAtDocument { + document, + document_type: self.document_type, + included, + }) + .as_ref(), + first_index, None, platform_version, )? @@ -1614,22 +1900,17 @@ impl<'a> DriveDocumentQuery<'a> { match subquery_clause { None => { - // There is a last_clause, but no subquery_clause, we should use the index property of the last clause - // We need to get the terminal indexes unused by clauses. - let last_index_property = index - .properties - .iter() - .find(|field| where_clause.field == field.name) - .ok_or(Error::Drive(DriveError::CorruptedContractIndexes( - "index must have last_clause field".to_string(), - )))?; Self::recursive_insert_on_query( - Some(&mut query), + &mut query, left_over_index_properties.as_slice(), index.unique, - &starts_at_document.map(|(document, included)| { - (document, self.document_type, last_index_property, included) - }), + starts_at_document + .map(|(document, included)| StartAtDocument { + document, + document_type: self.document_type, + included, + }) + .as_ref(), left_to_right, Some(&self.order_by), platform_version, @@ -1648,20 +1929,17 @@ impl<'a> DriveDocumentQuery<'a> { order_clause.ascending, platform_version, )?; - let last_index_property = index - .properties - .iter() - .find(|field| subquery_where_clause.field == field.name) - .ok_or(Error::Drive(DriveError::CorruptedContractIndexes( - "index must have subquery_clause field".to_string(), - )))?; Self::recursive_insert_on_query( - Some(&mut subquery), + &mut subquery, left_over_index_properties.as_slice(), index.unique, - &starts_at_document.map(|(document, included)| { - (document, self.document_type, last_index_property, included) - }), + starts_at_document + .map(|(document, included)| StartAtDocument { + document, + document_type: self.document_type, + included, + }) + .as_ref(), left_to_right, Some(&self.order_by), platform_version, diff --git a/packages/rs-drive/tests/query_tests.rs b/packages/rs-drive/tests/query_tests.rs index 23d84918859..6bad5144f95 100644 --- a/packages/rs-drive/tests/query_tests.rs +++ b/packages/rs-drive/tests/query_tests.rs @@ -68,15 +68,16 @@ use dpp::document::{DocumentV0Getters, DocumentV0Setters}; use dpp::fee::default_costs::CachedEpochIndexFeeVersions; use dpp::identity::TimestampMillis; use dpp::platform_value; +use dpp::platform_value::string_encoding::Encoding; #[cfg(feature = "server")] use dpp::prelude::DataContract; use dpp::tests::json_document::json_document_to_contract; #[cfg(feature = "server")] use dpp::util::cbor_serializer; -use once_cell::sync::Lazy; - use dpp::version::fee::FeeVersion; use dpp::version::PlatformVersion; +use once_cell::sync::Lazy; +use rand::prelude::StdRng; #[cfg(feature = "server")] use drive::drive::contract::test_helpers::add_init_contracts_structure_operations; @@ -434,12 +435,36 @@ struct Domain { subdomain_rules: SubdomainRules, } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Withdrawal { + #[serde(rename = "$id")] + pub id: Identifier, // Unique identifier for the withdrawal + + #[serde(rename = "$ownerId")] + pub owner_id: Identifier, // Identity of the withdrawal owner + + #[serde(rename = "$createdAt")] + pub created_at: TimestampMillis, + + #[serde(rename = "$updatedAt")] + pub updated_at: TimestampMillis, + + pub transaction_index: Option, // Optional sequential index of the transaction + pub transaction_sign_height: Option, // Optional Core height on which the transaction was signed + pub amount: u64, // Amount to withdraw (minimum: 1000) + pub core_fee_per_byte: u32, // Fee in Duffs/Byte (minimum: 1, max: 4294967295) + pub pooling: u8, // Pooling level (enum: 0, 1, 2) + pub output_script: Vec, // Byte array (size: 23-25) + pub status: u8, // Status (enum: 0 - Pending, 1 - Signed, etc.) +} + #[cfg(feature = "server")] #[test] fn test_serialization_and_deserialization() { let platform_version = PlatformVersion::latest(); - let domains = Domain::random_domains_in_parent(20, 100, "dash"); + let domains = Domain::random_domains_in_parent(20, None, 100, "dash"); let contract = json_document_to_contract( "tests/supporting_files/contract/dpns/dpns-contract.json", false, @@ -566,8 +591,10 @@ fn test_serialization_and_deserialization_with_null_values() { #[cfg(feature = "server")] impl Domain { /// Creates `count` random names as domain names for the given parent domain + /// If total owners in None it will create a new owner id per domain. fn random_domains_in_parent( count: u32, + total_owners: Option, seed: u64, normalized_parent_domain_name: &str, ) -> Vec { @@ -575,13 +602,29 @@ impl Domain { "tests/supporting_files/contract/family/first-names.txt", ); let mut vec: Vec = Vec::with_capacity(count as usize); + let mut rng = StdRng::seed_from_u64(seed); + + let owners = if let Some(total_owners) = total_owners { + if total_owners == 0 { + return vec![]; + } + (0..total_owners) + .map(|_| Identifier::random_with_rng(&mut rng)) + .collect() + } else { + vec![] + }; - let mut rng = rand::rngs::StdRng::seed_from_u64(seed); for _i in 0..count { let label = first_names.choose(&mut rng).unwrap(); let domain = Domain { id: Identifier::random_with_rng(&mut rng), - owner_id: Identifier::random_with_rng(&mut rng), + owner_id: if let Some(_) = total_owners { + // Pick a random owner from the owners list + *owners.choose(&mut rng).unwrap() + } else { + Identifier::random_with_rng(&mut rng) + }, label: Some(label.clone()), normalized_label: Some(label.to_lowercase()), normalized_parent_domain_name: normalized_parent_domain_name.to_string(), @@ -599,6 +642,75 @@ impl Domain { } } +#[cfg(feature = "server")] +impl Withdrawal { + /// Generate `count` random withdrawals + /// If `total_owners` is provided, assigns withdrawals to random owners from a predefined set. + pub fn random_withdrawals(count: u32, total_owners: Option, seed: u64) -> Vec { + let mut rng = StdRng::seed_from_u64(seed); + + // Generate a list of random owners if `total_owners` is provided + let owners: Vec = if let Some(total) = total_owners { + (0..total) + .map(|_| Identifier::random_with_rng(&mut rng)) + .collect() + } else { + vec![] + }; + + let mut next_transaction_index = 1; // Start transaction index from 1 + + let mut next_timestamp = 1732192259000; + + (0..count) + .map(|_| { + let owner_id = if !owners.is_empty() { + *owners.choose(&mut rng).unwrap() + } else { + Identifier::random_with_rng(&mut rng) + }; + + // Determine the status randomly + let status = if rng.gen_bool(0.5) { + 0 + } else { + rng.gen_range(1..=4) + }; // 0 = Pending, 1-4 = other statuses + + // Determine transaction index and sign height based on status + let (transaction_index, transaction_sign_height) = if status == 0 { + (None, None) // No transaction index or sign height for Pending status + } else { + let index = next_transaction_index; + next_transaction_index += 1; // Increment index for next withdrawal + (Some(index), Some(rng.gen_range(1..=500000))) // Set sign height only if transaction index is set + }; + + let output_script_length = rng.gen_range(23..=25); + let output_script: Vec = (0..output_script_length).map(|_| rng.gen()).collect(); + + let created_at = next_timestamp; + + next_timestamp += rng.gen_range(0..3) * 2000; + + Withdrawal { + id: Identifier::random_with_rng(&mut rng), + owner_id, + transaction_index, + transaction_sign_height, + amount: rng.gen_range(1000..=1_000_000), // Example range (minimum: 1000) + core_fee_per_byte: 0, // Always 0 + pooling: 0, // Always 0 + output_script, + status, + created_at, + updated_at: created_at, + } + }) + .collect() + } +} + #[cfg(feature = "server")] /// Adds `count` random domain names to the given contract pub fn add_domains_to_contract( @@ -606,10 +718,11 @@ pub fn add_domains_to_contract( contract: &DataContract, transaction: TransactionArg, count: u32, + total_owners: Option, seed: u64, ) { let platform_version = PlatformVersion::latest(); - let domains = Domain::random_domains_in_parent(count, seed, "dash"); + let domains = Domain::random_domains_in_parent(count, total_owners, seed, "dash"); let document_type = contract .document_type_for_name("domain") .expect("expected to get document type"); @@ -641,9 +754,56 @@ pub fn add_domains_to_contract( } } +#[cfg(feature = "server")] +/// Adds `count` random withdrawals to the given contract +pub fn add_withdrawals_to_contract( + drive: &Drive, + contract: &DataContract, + transaction: TransactionArg, + count: u32, + total_owners: Option, + seed: u64, +) { + let platform_version = PlatformVersion::latest(); + let withdrawals = Withdrawal::random_withdrawals(count, total_owners, seed); + let document_type = contract + .document_type_for_name("withdrawal") + .expect("expected to get document type"); + for domain in withdrawals { + let value = platform_value::to_value(domain).expect("expected value"); + let document = + Document::from_platform_value(value, platform_version).expect("expected value"); + + let storage_flags = Some(Cow::Owned(StorageFlags::SingleEpoch(0))); + + drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo((&document, storage_flags)), + owner_id: None, + }, + contract, + document_type, + }, + true, + BlockInfo::genesis(), + true, + transaction, + platform_version, + None, + ) + .expect("document should be inserted"); + } +} + #[cfg(feature = "server")] /// Sets up and inserts random domain name data to the DPNS contract to test queries on. -pub fn setup_dpns_tests_with_batches(count: u32, seed: u64) -> (Drive, DataContract) { +pub fn setup_dpns_tests_with_batches( + count: u32, + total_owners: Option, + seed: u64, +) -> (Drive, DataContract) { let drive = setup_drive(Some(DriveConfig::default())); let db_transaction = drive.grove.start_transaction(); @@ -667,7 +827,61 @@ pub fn setup_dpns_tests_with_batches(count: u32, seed: u64) -> (Drive, DataContr Some(&db_transaction), ); - add_domains_to_contract(&drive, &contract, Some(&db_transaction), count, seed); + add_domains_to_contract( + &drive, + &contract, + Some(&db_transaction), + count, + total_owners, + seed, + ); + drive + .grove + .commit_transaction(db_transaction) + .unwrap() + .expect("transaction should be committed"); + + (drive, contract) +} + +#[cfg(feature = "server")] +/// Sets up and inserts random withdrawal to the Withdrawal contract to test queries on. +pub fn setup_withdrawal_tests( + count: u32, + total_owners: Option, + seed: u64, +) -> (Drive, DataContract) { + let drive = setup_drive(Some(DriveConfig::default())); + + let db_transaction = drive.grove.start_transaction(); + + // Create contracts tree + let mut batch = GroveDbOpBatch::new(); + + add_init_contracts_structure_operations(&mut batch); + + let platform_version = PlatformVersion::latest(); + + drive + .grove_apply_batch(batch, false, Some(&db_transaction), &platform_version.drive) + .expect("expected to create contracts tree successfully"); + + // setup code + let contract = setup_contract( + &drive, + "tests/supporting_files/contract/withdrawals/withdrawals-contract.json", + None, + Some(&db_transaction), + ); + + add_withdrawals_to_contract( + &drive, + &contract, + Some(&db_transaction), + count, + total_owners, + seed, + ); drive .grove .commit_transaction(db_transaction) @@ -738,7 +952,7 @@ pub fn setup_dpns_tests_label_not_required(count: u32, seed: u64) -> (Drive, Dat Some(&db_transaction), ); - add_domains_to_contract(&drive, &contract, Some(&db_transaction), count, seed); + add_domains_to_contract(&drive, &contract, Some(&db_transaction), count, None, seed); drive .grove .commit_transaction(db_transaction) @@ -3078,7 +3292,7 @@ fn test_query_with_cached_contract() { #[cfg(feature = "server")] #[test] fn test_dpns_query_contract_verification() { - let (drive, contract) = setup_dpns_tests_with_batches(10, 11456); + let (drive, contract) = setup_dpns_tests_with_batches(10, None, 11456); let platform_version = PlatformVersion::latest(); @@ -3155,7 +3369,7 @@ fn test_contract_keeps_history_fetch_and_verification() { #[cfg(feature = "server")] #[test] fn test_dpns_query() { - let (drive, contract) = setup_dpns_tests_with_batches(10, 11456); + let (drive, contract) = setup_dpns_tests_with_batches(10, None, 11456); let platform_version = PlatformVersion::latest(); @@ -3707,7 +3921,7 @@ fn test_dpns_insertion_with_aliases() { #[test] fn test_dpns_query_start_at() { // The point of this test is to test the situation where we have a start at a certain value for the DPNS query. - let (drive, contract) = setup_dpns_tests_with_batches(10, 11456); + let (drive, contract) = setup_dpns_tests_with_batches(10, None, 11456); let platform_version = PlatformVersion::latest(); @@ -3801,7 +4015,7 @@ fn test_dpns_query_start_at() { #[test] fn test_dpns_query_start_after() { // The point of this test is to test the situation where we have a start at a certain value for the DPNS query. - let (drive, contract) = setup_dpns_tests_with_batches(10, 11456); + let (drive, contract) = setup_dpns_tests_with_batches(10, None, 11456); let platform_version = PlatformVersion::latest(); @@ -3895,7 +4109,7 @@ fn test_dpns_query_start_after() { #[test] fn test_dpns_query_start_at_desc() { // The point of this test is to test the situation where we have a start at a certain value for the DPNS query. - let (drive, contract) = setup_dpns_tests_with_batches(10, 11456); + let (drive, contract) = setup_dpns_tests_with_batches(10, None, 11456); let platform_version = PlatformVersion::latest(); @@ -3989,7 +4203,7 @@ fn test_dpns_query_start_at_desc() { #[test] fn test_dpns_query_start_after_desc() { // The point of this test is to test the situation where we have a start at a certain value for the DPNS query. - let (drive, contract) = setup_dpns_tests_with_batches(10, 11456); + let (drive, contract) = setup_dpns_tests_with_batches(10, None, 11456); let platform_version = PlatformVersion::latest(); @@ -4465,7 +4679,8 @@ fn test_dpns_query_start_after_with_null_id() { .expect("we should be able to deserialize the document"); let normalized_label_value = document .get("normalizedLabel") - .expect("we should be able to get the first name"); + .cloned() + .unwrap_or(Value::Null); if normalized_label_value.is_null() { String::from("") } else { @@ -4804,6 +5019,291 @@ fn test_dpns_query_start_after_with_null_id_desc() { assert_eq!(results, proof_results); } +#[cfg(feature = "server")] +#[test] +fn test_withdrawals_query_by_owner_id() { + // We create 10 withdrawals owned by 2 identities + let (drive, contract) = setup_withdrawal_tests(10, Some(2), 11456); + + let platform_version = PlatformVersion::latest(); + + let db_transaction = drive.grove.start_transaction(); + + let root_hash = drive + .grove + .root_hash(Some(&db_transaction), &platform_version.drive.grove_version) + .unwrap() + .expect("there is always a root hash"); + + let expected_app_hash = vec![ + 144, 177, 24, 41, 104, 174, 220, 135, 164, 0, 240, 215, 42, 60, 249, 142, 150, 169, 135, + 72, 151, 35, 238, 131, 164, 229, 106, 83, 198, 109, 65, 211, + ]; + + assert_eq!(root_hash.as_slice(), expected_app_hash); + + // Document Ids are + // document v0 : id:2kTB6gW4wCCnySj3UFUJQM3aUYBd6qDfLCY74BnWmFKu owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:09 updated_at:2024-11-21 12:31:09 amount:(i64)646767 coreFeePerByte:(i64)0 outputScript:bytes 00952c808390e575c8dd29fc07ccfed7b428e1ec2ffcb23e pooling:(i64)0 status:(i64)1 transactionIndex:(i64)4 transactionSignHeight:(i64)303186 + // document v0 : id:3T4aKmidGKA4ETnWYSedm6ETzrcdkfPL2r3D6eg6CSib owner_id:CH1EHBkN5FUuQ7z8ep1abroLPzzYjagvM5XV2NYR3DEh created_at:2024-11-21 12:31:01 updated_at:2024-11-21 12:31:01 amount:(i64)971045 coreFeePerByte:(i64)0 outputScript:bytes 525dfc160c160a7a52ef3301a7e55fccf41d73857f50a55a4d pooling:(i64)0 status:(i64)1 transactionIndex:(i64)2 transactionSignHeight:(i64)248787 + // document v0 : id:3X2QfUfR8EeVZQAKmEjcue5xDv3CZXrfPTgXkZ5vQo13 owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)122155 coreFeePerByte:(i64)0 outputScript:bytes f76eb8b953ff41040d906c25a4ae42884bedb41a07fc3a pooling:(i64)0 status:(i64)3 transactionIndex:(i64)7 transactionSignHeight:(i64)310881 + // document v0 : id:5ikeRNwvFekr6ex32B4dLEcCaSsgXXHJBx5rJ2rwuhEV owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:30:59 updated_at:2024-11-21 12:30:59 amount:(i64)725014 coreFeePerByte:(i64)0 outputScript:bytes 51f203a755a7ff25ba8645841f80403ee98134690b2c0dd5e2 pooling:(i64)0 status:(i64)3 transactionIndex:(i64)1 transactionSignHeight:(i64)4072 + // document v0 : id:74giZJn9fNczYRsxxh3wVnktJS1vzTiRWYinKK1rRcyj owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)151943 coreFeePerByte:(i64)0 outputScript:bytes 9db03f4c8a51e4e9855e008aae6121911b4831699c53ed pooling:(i64)0 status:(i64)1 transactionIndex:(i64)5 transactionSignHeight:(i64)343099 + // document v0 : id:8iqDAFxTzHYcmUWtcNnCRoj9Fss4HE1G3GP3HhVAZJhn owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:13 updated_at:2024-11-21 12:31:13 amount:(i64)409642 coreFeePerByte:(i64)0 outputScript:bytes 19fe0a2458a47e1726191f4dc94d11bcfacf821d024043 pooling:(i64)0 status:(i64)4 transactionIndex:(i64)8 transactionSignHeight:(i64)304397 + // document v0 : id:BdH274iP17nhquQVY4KMCAM6nwyPRc8AFJkUT91vxhbc owner_id:CH1EHBkN5FUuQ7z8ep1abroLPzzYjagvM5XV2NYR3DEh created_at:2024-11-21 12:31:03 updated_at:2024-11-21 12:31:03 amount:(i64)81005 coreFeePerByte:(i64)0 outputScript:bytes 2666e87b6cc7ddf2b63e7e52c348818c05e5562efa48f5 pooling:(i64)0 status:(i64)0 + // document v0 : id:CCjaU67Pe79Vt51oXvQ5SkyNiypofNX9DS9PYydN9tpD owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:01 updated_at:2024-11-21 12:31:01 amount:(i64)455074 coreFeePerByte:(i64)0 outputScript:bytes acde2e1652771b50a2c68fd330ee1d4b8e115631ce72375432 pooling:(i64)0 status:(i64)3 transactionIndex:(i64)3 transactionSignHeight:(i64)261103 + // document v0 : id:DxFzXvkb2mNQHmeVknsv3gWsc6rMtLk9AsS5zMpy6hou owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:05 updated_at:2024-11-21 12:31:05 amount:(i64)271303 coreFeePerByte:(i64)0 outputScript:bytes 0b845e8c3a4679f1913172f7fd939cc153f458519de8ed3d pooling:(i64)0 status:(i64)0 + // document v0 : id:FDnvFN7e72LcZEojTWNmJTP7uzok3BtvbKnaa5gjqCpW owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)123433 coreFeePerByte:(i64)0 outputScript:bytes 82712473b2d0fc5663afb1a08006913ccccbf38e091a8cc7 pooling:(i64)0 status:(i64)4 transactionIndex:(i64)6 transactionSignHeight:(i64)319518 + + let query_value = json!({ + "where": [ + ["$ownerId", "==", "A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ"] + ], + "limit": 2 + }); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let domain_document_type = contract + .document_type_for_name("withdrawal") + .expect("contract should have a domain document type"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + domain_document_type, + &drive.config, + ) + .expect("query should be built"); + let (results, _, _) = query + .execute_raw_results_no_proof(&drive, None, Some(&db_transaction), platform_version) + .expect("proof should be executed"); + let names: Vec = results + .iter() + .map(|result| { + let document = + Document::from_bytes(result.as_slice(), domain_document_type, platform_version) + .expect("we should be able to deserialize the document"); + document.id().to_string(Encoding::Base58) + }) + .collect(); + + let a_names = [ + "5ikeRNwvFekr6ex32B4dLEcCaSsgXXHJBx5rJ2rwuhEV".to_string(), + "CCjaU67Pe79Vt51oXvQ5SkyNiypofNX9DS9PYydN9tpD".to_string(), + ]; + + assert_eq!(names, a_names); + + let (proof_root_hash, proof_results, _) = query + .execute_with_proof_only_get_elements(&drive, None, None, platform_version) + .expect("we should be able to a proof"); + assert_eq!(root_hash, proof_root_hash); + assert_eq!(results, proof_results); +} + +#[cfg(feature = "server")] +#[test] +fn test_withdrawals_query_start_after_query_by_owner_id() { + // We create 10 withdrawals owned by 2 identities + let (drive, contract) = setup_withdrawal_tests(10, Some(2), 11456); + + let platform_version = PlatformVersion::latest(); + + let db_transaction = drive.grove.start_transaction(); + + let root_hash = drive + .grove + .root_hash(Some(&db_transaction), &platform_version.drive.grove_version) + .unwrap() + .expect("there is always a root hash"); + + let expected_app_hash = vec![ + 144, 177, 24, 41, 104, 174, 220, 135, 164, 0, 240, 215, 42, 60, 249, 142, 150, 169, 135, + 72, 151, 35, 238, 131, 164, 229, 106, 83, 198, 109, 65, 211, + ]; + + assert_eq!(root_hash.as_slice(), expected_app_hash); + + // Document Ids are + // document v0 : id:2kTB6gW4wCCnySj3UFUJQM3aUYBd6qDfLCY74BnWmFKu owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:09 updated_at:2024-11-21 12:31:09 amount:(i64)646767 coreFeePerByte:(i64)0 outputScript:bytes 00952c808390e575c8dd29fc07ccfed7b428e1ec2ffcb23e pooling:(i64)0 status:(i64)1 transactionIndex:(i64)4 transactionSignHeight:(i64)303186 + // document v0 : id:3T4aKmidGKA4ETnWYSedm6ETzrcdkfPL2r3D6eg6CSib owner_id:CH1EHBkN5FUuQ7z8ep1abroLPzzYjagvM5XV2NYR3DEh created_at:2024-11-21 12:31:01 updated_at:2024-11-21 12:31:01 amount:(i64)971045 coreFeePerByte:(i64)0 outputScript:bytes 525dfc160c160a7a52ef3301a7e55fccf41d73857f50a55a4d pooling:(i64)0 status:(i64)1 transactionIndex:(i64)2 transactionSignHeight:(i64)248787 + // document v0 : id:3X2QfUfR8EeVZQAKmEjcue5xDv3CZXrfPTgXkZ5vQo13 owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)122155 coreFeePerByte:(i64)0 outputScript:bytes f76eb8b953ff41040d906c25a4ae42884bedb41a07fc3a pooling:(i64)0 status:(i64)3 transactionIndex:(i64)7 transactionSignHeight:(i64)310881 + // document v0 : id:5ikeRNwvFekr6ex32B4dLEcCaSsgXXHJBx5rJ2rwuhEV owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:30:59 updated_at:2024-11-21 12:30:59 amount:(i64)725014 coreFeePerByte:(i64)0 outputScript:bytes 51f203a755a7ff25ba8645841f80403ee98134690b2c0dd5e2 pooling:(i64)0 status:(i64)3 transactionIndex:(i64)1 transactionSignHeight:(i64)4072 + // document v0 : id:74giZJn9fNczYRsxxh3wVnktJS1vzTiRWYinKK1rRcyj owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)151943 coreFeePerByte:(i64)0 outputScript:bytes 9db03f4c8a51e4e9855e008aae6121911b4831699c53ed pooling:(i64)0 status:(i64)1 transactionIndex:(i64)5 transactionSignHeight:(i64)343099 + // document v0 : id:8iqDAFxTzHYcmUWtcNnCRoj9Fss4HE1G3GP3HhVAZJhn owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:13 updated_at:2024-11-21 12:31:13 amount:(i64)409642 coreFeePerByte:(i64)0 outputScript:bytes 19fe0a2458a47e1726191f4dc94d11bcfacf821d024043 pooling:(i64)0 status:(i64)4 transactionIndex:(i64)8 transactionSignHeight:(i64)304397 + // document v0 : id:BdH274iP17nhquQVY4KMCAM6nwyPRc8AFJkUT91vxhbc owner_id:CH1EHBkN5FUuQ7z8ep1abroLPzzYjagvM5XV2NYR3DEh created_at:2024-11-21 12:31:03 updated_at:2024-11-21 12:31:03 amount:(i64)81005 coreFeePerByte:(i64)0 outputScript:bytes 2666e87b6cc7ddf2b63e7e52c348818c05e5562efa48f5 pooling:(i64)0 status:(i64)0 + // document v0 : id:CCjaU67Pe79Vt51oXvQ5SkyNiypofNX9DS9PYydN9tpD owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:01 updated_at:2024-11-21 12:31:01 amount:(i64)455074 coreFeePerByte:(i64)0 outputScript:bytes acde2e1652771b50a2c68fd330ee1d4b8e115631ce72375432 pooling:(i64)0 status:(i64)3 transactionIndex:(i64)3 transactionSignHeight:(i64)261103 + // document v0 : id:DxFzXvkb2mNQHmeVknsv3gWsc6rMtLk9AsS5zMpy6hou owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:05 updated_at:2024-11-21 12:31:05 amount:(i64)271303 coreFeePerByte:(i64)0 outputScript:bytes 0b845e8c3a4679f1913172f7fd939cc153f458519de8ed3d pooling:(i64)0 status:(i64)0 + // document v0 : id:FDnvFN7e72LcZEojTWNmJTP7uzok3BtvbKnaa5gjqCpW owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)123433 coreFeePerByte:(i64)0 outputScript:bytes 82712473b2d0fc5663afb1a08006913ccccbf38e091a8cc7 pooling:(i64)0 status:(i64)4 transactionIndex:(i64)6 transactionSignHeight:(i64)319518 + + let query_value = json!({ + "where": [ + ["$ownerId", "==", "A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ"] + ], + "startAfter": "CCjaU67Pe79Vt51oXvQ5SkyNiypofNX9DS9PYydN9tpD", + "limit": 3, + }); + + // This will use the identity recent index + // { + // "name": "identityRecent", + // "properties": [ + // { + // "$ownerId": "asc" + // }, + // { + // "$updatedAt": "asc" + // }, + // { + // "status": "asc" + // } + // ], + // "unique": false + // }, + + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let domain_document_type = contract + .document_type_for_name("withdrawal") + .expect("contract should have a domain document type"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + domain_document_type, + &drive.config, + ) + .expect("query should be built"); + let (results, _, _) = query + .execute_raw_results_no_proof(&drive, None, Some(&db_transaction), platform_version) + .expect("proof should be executed"); + let names: Vec = results + .iter() + .map(|result| { + let document = + Document::from_bytes(result.as_slice(), domain_document_type, platform_version) + .expect("we should be able to deserialize the document"); + document.id().to_string(Encoding::Base58) + }) + .collect(); + + // We only get back 2 values, even though we put limit 3 because the time with status 0 is an + // empty tree and consumes a limit + let a_names = [ + "DxFzXvkb2mNQHmeVknsv3gWsc6rMtLk9AsS5zMpy6hou".to_string(), + "2kTB6gW4wCCnySj3UFUJQM3aUYBd6qDfLCY74BnWmFKu".to_string(), + ]; + + assert_eq!(names, a_names); + + let (proof_root_hash, proof_results, _) = query + .execute_with_proof_only_get_elements(&drive, None, None, platform_version) + .expect("we should be able to a proof"); + assert_eq!(root_hash, proof_root_hash); + assert_eq!(results, proof_results); +} + +#[cfg(feature = "server")] +#[test] +fn test_withdrawals_query_start_after_query_by_owner_id_desc() { + // We create 10 withdrawals owned by 2 identities + let (drive, contract) = setup_withdrawal_tests(10, Some(2), 11456); + + let platform_version = PlatformVersion::latest(); + + let db_transaction = drive.grove.start_transaction(); + + let root_hash = drive + .grove + .root_hash(Some(&db_transaction), &platform_version.drive.grove_version) + .unwrap() + .expect("there is always a root hash"); + + let expected_app_hash = vec![ + 144, 177, 24, 41, 104, 174, 220, 135, 164, 0, 240, 215, 42, 60, 249, 142, 150, 169, 135, + 72, 151, 35, 238, 131, 164, 229, 106, 83, 198, 109, 65, 211, + ]; + + assert_eq!(root_hash.as_slice(), expected_app_hash); + + // Document Ids are + // document v0 : id:2kTB6gW4wCCnySj3UFUJQM3aUYBd6qDfLCY74BnWmFKu owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:09 updated_at:2024-11-21 12:31:09 amount:(i64)646767 coreFeePerByte:(i64)0 outputScript:bytes 00952c808390e575c8dd29fc07ccfed7b428e1ec2ffcb23e pooling:(i64)0 status:(i64)1 transactionIndex:(i64)4 transactionSignHeight:(i64)303186 + // document v0 : id:3T4aKmidGKA4ETnWYSedm6ETzrcdkfPL2r3D6eg6CSib owner_id:CH1EHBkN5FUuQ7z8ep1abroLPzzYjagvM5XV2NYR3DEh created_at:2024-11-21 12:31:01 updated_at:2024-11-21 12:31:01 amount:(i64)971045 coreFeePerByte:(i64)0 outputScript:bytes 525dfc160c160a7a52ef3301a7e55fccf41d73857f50a55a4d pooling:(i64)0 status:(i64)1 transactionIndex:(i64)2 transactionSignHeight:(i64)248787 + // document v0 : id:3X2QfUfR8EeVZQAKmEjcue5xDv3CZXrfPTgXkZ5vQo13 owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)122155 coreFeePerByte:(i64)0 outputScript:bytes f76eb8b953ff41040d906c25a4ae42884bedb41a07fc3a pooling:(i64)0 status:(i64)3 transactionIndex:(i64)7 transactionSignHeight:(i64)310881 + // document v0 : id:5ikeRNwvFekr6ex32B4dLEcCaSsgXXHJBx5rJ2rwuhEV owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:30:59 updated_at:2024-11-21 12:30:59 amount:(i64)725014 coreFeePerByte:(i64)0 outputScript:bytes 51f203a755a7ff25ba8645841f80403ee98134690b2c0dd5e2 pooling:(i64)0 status:(i64)3 transactionIndex:(i64)1 transactionSignHeight:(i64)4072 + // document v0 : id:74giZJn9fNczYRsxxh3wVnktJS1vzTiRWYinKK1rRcyj owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)151943 coreFeePerByte:(i64)0 outputScript:bytes 9db03f4c8a51e4e9855e008aae6121911b4831699c53ed pooling:(i64)0 status:(i64)1 transactionIndex:(i64)5 transactionSignHeight:(i64)343099 + // document v0 : id:8iqDAFxTzHYcmUWtcNnCRoj9Fss4HE1G3GP3HhVAZJhn owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:13 updated_at:2024-11-21 12:31:13 amount:(i64)409642 coreFeePerByte:(i64)0 outputScript:bytes 19fe0a2458a47e1726191f4dc94d11bcfacf821d024043 pooling:(i64)0 status:(i64)4 transactionIndex:(i64)8 transactionSignHeight:(i64)304397 + // document v0 : id:BdH274iP17nhquQVY4KMCAM6nwyPRc8AFJkUT91vxhbc owner_id:CH1EHBkN5FUuQ7z8ep1abroLPzzYjagvM5XV2NYR3DEh created_at:2024-11-21 12:31:03 updated_at:2024-11-21 12:31:03 amount:(i64)81005 coreFeePerByte:(i64)0 outputScript:bytes 2666e87b6cc7ddf2b63e7e52c348818c05e5562efa48f5 pooling:(i64)0 status:(i64)0 + // document v0 : id:CCjaU67Pe79Vt51oXvQ5SkyNiypofNX9DS9PYydN9tpD owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:01 updated_at:2024-11-21 12:31:01 amount:(i64)455074 coreFeePerByte:(i64)0 outputScript:bytes acde2e1652771b50a2c68fd330ee1d4b8e115631ce72375432 pooling:(i64)0 status:(i64)3 transactionIndex:(i64)3 transactionSignHeight:(i64)261103 + // document v0 : id:DxFzXvkb2mNQHmeVknsv3gWsc6rMtLk9AsS5zMpy6hou owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:05 updated_at:2024-11-21 12:31:05 amount:(i64)271303 coreFeePerByte:(i64)0 outputScript:bytes 0b845e8c3a4679f1913172f7fd939cc153f458519de8ed3d pooling:(i64)0 status:(i64)0 + // document v0 : id:FDnvFN7e72LcZEojTWNmJTP7uzok3BtvbKnaa5gjqCpW owner_id:A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ created_at:2024-11-21 12:31:11 updated_at:2024-11-21 12:31:11 amount:(i64)123433 coreFeePerByte:(i64)0 outputScript:bytes 82712473b2d0fc5663afb1a08006913ccccbf38e091a8cc7 pooling:(i64)0 status:(i64)4 transactionIndex:(i64)6 transactionSignHeight:(i64)319518 + + let query_value = json!({ + "where": [ + ["$ownerId", "==", "A8GdKdMT7eDvtjnmMXe1Z3YaTtJzZdxNDRkeLb8goFrZ"] + ], + "startAfter": "2kTB6gW4wCCnySj3UFUJQM3aUYBd6qDfLCY74BnWmFKu", + "limit": 3, + "orderBy": [ + ["$updatedAt", "desc"] + ] + }); + + // This will use the identity recent index + // { + // "name": "identityRecent", + // "properties": [ + // { + // "$ownerId": "asc" + // }, + // { + // "$updatedAt": "asc" + // }, + // { + // "status": "asc" + // } + // ], + // "unique": false + // }, + + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let domain_document_type = contract + .document_type_for_name("withdrawal") + .expect("contract should have a domain document type"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + domain_document_type, + &drive.config, + ) + .expect("query should be built"); + let (results, _, _) = query + .execute_raw_results_no_proof(&drive, None, Some(&db_transaction), platform_version) + .expect("proof should be executed"); + let names: Vec = results + .iter() + .map(|result| { + let document = + Document::from_bytes(result.as_slice(), domain_document_type, platform_version) + .expect("we should be able to deserialize the document"); + document.id().to_string(Encoding::Base58) + }) + .collect(); + + // We only get back 2 values, even though we put limit 3 because the time with status 0 is an + // empty tree and consumes a limit + let a_names = [ + "DxFzXvkb2mNQHmeVknsv3gWsc6rMtLk9AsS5zMpy6hou".to_string(), + "CCjaU67Pe79Vt51oXvQ5SkyNiypofNX9DS9PYydN9tpD".to_string(), + ]; + + assert_eq!(names, a_names); + + let (proof_root_hash, proof_results, _) = query + .execute_with_proof_only_get_elements(&drive, None, None, platform_version) + .expect("we should be able to a proof"); + assert_eq!(root_hash, proof_root_hash); + assert_eq!(results, proof_results); +} + #[cfg(feature = "server")] #[test] fn test_query_a_b_c_d_e_contract() { diff --git a/packages/rs-drive/tests/supporting_files/contract/withdrawals/withdrawals-contract.json b/packages/rs-drive/tests/supporting_files/contract/withdrawals/withdrawals-contract.json new file mode 100644 index 00000000000..5e12831bef5 --- /dev/null +++ b/packages/rs-drive/tests/supporting_files/contract/withdrawals/withdrawals-contract.json @@ -0,0 +1,141 @@ +{ + "$format_version": "0", + "id": "A6Z7WkPjzp8Qe77Av5PNxY2E8JFCYpSVdJ8tZE94PErh", + "ownerId": "B1XbULsStFtFhJoc6qmMKx8a3nH4YCsotupSWoBiFaKr", + "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" + ] + } + } +} \ No newline at end of file