drive/query/
vote_poll_contestant_votes_query.rs

1#[cfg(feature = "verify")]
2use super::ContractLookupFn;
3use crate::drive::votes::paths::VotePollPaths;
4#[cfg(any(feature = "server", feature = "verify"))]
5use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::resolve::ContestedDocumentResourceVotePollResolver;
6use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed;
7#[cfg(feature = "server")]
8use crate::drive::Drive;
9use crate::error::Error;
10#[cfg(feature = "server")]
11use crate::fees::op::LowLevelDriveOperation;
12#[cfg(feature = "server")]
13use crate::query::GroveError;
14use crate::query::Query;
15use bincode::{Decode, Encode};
16#[cfg(feature = "server")]
17use dpp::block::block_info::BlockInfo;
18use dpp::identifier::Identifier;
19#[cfg(feature = "server")]
20use dpp::platform_value;
21use dpp::voting::vote_choices::resource_vote_choice::ResourceVoteChoice::TowardsIdentity;
22use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll;
23#[cfg(feature = "server")]
24use grovedb::query_result_type::{QueryResultElements, QueryResultType};
25#[cfg(feature = "server")]
26use grovedb::TransactionArg;
27use grovedb::{PathQuery, SizedQuery};
28use platform_version::version::PlatformVersion;
29
30/// Vote Poll Drive Query struct
31#[derive(Debug, PartialEq, Clone, Encode, Decode)]
32pub struct ContestedDocumentVotePollVotesDriveQuery {
33    /// What vote poll are we asking for?
34    pub vote_poll: ContestedDocumentResourceVotePoll,
35    /// Which contestant do we want to get the votes for
36    pub contestant_id: Identifier,
37    /// Offset
38    pub offset: Option<u16>,
39    /// Limit
40    pub limit: Option<u16>,
41    /// Start at identity id
42    pub start_at: Option<([u8; 32], bool)>,
43    /// Ascending
44    pub order_ascending: bool,
45}
46
47impl ContestedDocumentVotePollVotesDriveQuery {
48    #[cfg(feature = "server")]
49    /// Resolves the contested document vote poll drive query.
50    ///
51    /// This method processes the query by interacting with the drive, using the provided
52    /// transaction and platform version to ensure consistency and compatibility.
53    ///
54    /// # Parameters
55    ///
56    /// * `drive`: A reference to the `Drive` object used for database interactions.
57    /// * `transaction`: The transaction argument used to ensure consistency during the resolve operation.
58    /// * `platform_version`: The platform version to ensure compatibility.
59    ///
60    /// # Returns
61    ///
62    /// * `Ok(ResolvedContestedDocumentVotePollDriveQuery)` - The resolved query information.
63    /// * `Err(Error)` - An error if the resolution process fails.
64    ///
65    /// # Errors
66    ///
67    /// This method returns an `Error` variant if there is an issue resolving the query.
68    /// The specific error depends on the underlying problem encountered during resolution.
69    pub fn resolve(
70        &self,
71        drive: &Drive,
72        transaction: TransactionArg,
73        platform_version: &PlatformVersion,
74    ) -> Result<ResolvedContestedDocumentVotePollVotesDriveQuery<'_>, Error> {
75        let ContestedDocumentVotePollVotesDriveQuery {
76            vote_poll,
77            contestant_id,
78            offset,
79            limit,
80            start_at,
81            order_ascending,
82        } = self;
83        Ok(ResolvedContestedDocumentVotePollVotesDriveQuery {
84            vote_poll: vote_poll.resolve_allow_borrowed(drive, transaction, platform_version)?,
85            contestant_id: *contestant_id,
86            offset: *offset,
87            limit: *limit,
88            start_at: *start_at,
89            order_ascending: *order_ascending,
90        })
91    }
92
93    /// Resolves the contested document vote poll drive query.
94    ///
95    /// See [ContestedDocumentVotePollVotesDriveQuery::resolve](ContestedDocumentVotePollVotesDriveQuery::resolve) for more information.
96    #[cfg(feature = "verify")]
97    pub fn resolve_with_known_contracts_provider<'a>(
98        &self,
99        known_contracts_provider: &ContractLookupFn,
100    ) -> Result<ResolvedContestedDocumentVotePollVotesDriveQuery<'a>, Error> {
101        let ContestedDocumentVotePollVotesDriveQuery {
102            vote_poll,
103            contestant_id,
104            offset,
105            limit,
106            start_at,
107            order_ascending,
108        } = self;
109        Ok(ResolvedContestedDocumentVotePollVotesDriveQuery {
110            vote_poll: vote_poll.resolve_with_known_contracts_provider(known_contracts_provider)?,
111            contestant_id: *contestant_id,
112            offset: *offset,
113            limit: *limit,
114            start_at: *start_at,
115            order_ascending: *order_ascending,
116        })
117    }
118
119    #[cfg(feature = "server")]
120    /// Executes a query with proof and returns the items and fee.
121    pub fn execute_with_proof(
122        self,
123        drive: &Drive,
124        block_info: Option<BlockInfo>,
125        transaction: TransactionArg,
126        platform_version: &PlatformVersion,
127    ) -> Result<(Vec<u8>, u64), Error> {
128        let mut drive_operations = vec![];
129        let items = self.execute_with_proof_internal(
130            drive,
131            transaction,
132            &mut drive_operations,
133            platform_version,
134        )?;
135        let cost = if let Some(block_info) = block_info {
136            let fee_result = Drive::calculate_fee(
137                None,
138                Some(drive_operations),
139                &block_info.epoch,
140                drive.config.epochs_per_era,
141                platform_version,
142                None,
143            )?;
144            fee_result.processing_fee
145        } else {
146            0
147        };
148        Ok((items, cost))
149    }
150
151    #[cfg(feature = "server")]
152    /// Executes an internal query with proof and returns the items.
153    pub(crate) fn execute_with_proof_internal(
154        self,
155        drive: &Drive,
156        transaction: TransactionArg,
157        drive_operations: &mut Vec<LowLevelDriveOperation>,
158        platform_version: &PlatformVersion,
159    ) -> Result<Vec<u8>, Error> {
160        let resolved = self.resolve(drive, transaction, platform_version)?;
161        let path_query = resolved.construct_path_query(platform_version)?;
162        drive.grove_get_proved_path_query(
163            &path_query,
164            transaction,
165            drive_operations,
166            &platform_version.drive,
167        )
168    }
169
170    #[cfg(feature = "server")]
171    /// Executes a query with no proof and returns the items, skipped items, and fee.
172    pub fn execute_no_proof_with_cost(
173        &self,
174        drive: &Drive,
175        block_info: Option<BlockInfo>,
176        transaction: TransactionArg,
177        platform_version: &PlatformVersion,
178    ) -> Result<(Vec<Identifier>, u64), Error> {
179        let mut drive_operations = vec![];
180        let result =
181            self.execute_no_proof(drive, transaction, &mut drive_operations, platform_version)?;
182        let cost = if let Some(block_info) = block_info {
183            let fee_result = Drive::calculate_fee(
184                None,
185                Some(drive_operations),
186                &block_info.epoch,
187                drive.config.epochs_per_era,
188                platform_version,
189                None,
190            )?;
191            fee_result.processing_fee
192        } else {
193            0
194        };
195        Ok((result, cost))
196    }
197
198    #[cfg(feature = "server")]
199    /// Executes an internal query with no proof and returns the values and skipped items.
200    pub fn execute_no_proof(
201        &self,
202        drive: &Drive,
203        transaction: TransactionArg,
204        drive_operations: &mut Vec<LowLevelDriveOperation>,
205        platform_version: &PlatformVersion,
206    ) -> Result<Vec<Identifier>, Error> {
207        let resolved = self.resolve(drive, transaction, platform_version)?;
208        let path_query = resolved.construct_path_query(platform_version)?;
209        let query_result = drive.grove_get_path_query(
210            &path_query,
211            transaction,
212            QueryResultType::QueryPathKeyElementTrioResultType,
213            drive_operations,
214            &platform_version.drive,
215        );
216        match query_result {
217            Err(Error::GroveDB(e))
218                if matches!(
219                    e.as_ref(),
220                    GroveError::PathKeyNotFound(_)
221                        | GroveError::PathNotFound(_)
222                        | GroveError::PathParentLayerNotFound(_)
223                ) =>
224            {
225                Ok(vec![])
226            }
227            Err(e) => Err(e),
228            Ok((query_result_elements, _skipped)) => {
229                let voters = query_result_elements
230                    .to_keys()
231                    .into_iter()
232                    .map(Identifier::try_from)
233                    .collect::<Result<Vec<Identifier>, platform_value::Error>>()?;
234
235                Ok(voters)
236            }
237        }
238    }
239
240    #[cfg(feature = "server")]
241    #[allow(unused)]
242    /// Executes an internal query with no proof and returns the values and skipped items.
243    pub(crate) fn execute_no_proof_internal(
244        &self,
245        drive: &Drive,
246        result_type: QueryResultType,
247        transaction: TransactionArg,
248        drive_operations: &mut Vec<LowLevelDriveOperation>,
249        platform_version: &PlatformVersion,
250    ) -> Result<(QueryResultElements, u16), Error> {
251        let resolved = self.resolve(drive, transaction, platform_version)?;
252        let path_query = resolved.construct_path_query(platform_version)?;
253        let query_result = drive.grove_get_path_query(
254            &path_query,
255            transaction,
256            result_type,
257            drive_operations,
258            &platform_version.drive,
259        );
260        match query_result {
261            Err(Error::GroveDB(e))
262                if matches!(
263                    e.as_ref(),
264                    GroveError::PathKeyNotFound(_)
265                        | GroveError::PathNotFound(_)
266                        | GroveError::PathParentLayerNotFound(_)
267                ) =>
268            {
269                Ok((QueryResultElements::new(), 0))
270            }
271            _ => {
272                let (data, skipped) = query_result?;
273                {
274                    Ok((data, skipped))
275                }
276            }
277        }
278    }
279}
280/// Vote Poll Drive Query struct
281#[derive(Debug, PartialEq, Clone)]
282pub struct ResolvedContestedDocumentVotePollVotesDriveQuery<'a> {
283    /// What vote poll are we asking for?
284    pub vote_poll: ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed<'a>,
285    /// Who's votes are we looking for
286    pub contestant_id: Identifier,
287    /// Offset
288    pub offset: Option<u16>,
289    /// Limit
290    pub limit: Option<u16>,
291    /// Start at identity id, the bool is if it is also included
292    pub start_at: Option<([u8; 32], bool)>,
293    /// Ascending
294    pub order_ascending: bool,
295}
296
297impl ResolvedContestedDocumentVotePollVotesDriveQuery<'_> {
298    /// Operations to construct a path query.
299    pub fn construct_path_query(
300        &self,
301        platform_version: &PlatformVersion,
302    ) -> Result<PathQuery, Error> {
303        let path = self
304            .vote_poll
305            .contender_voting_path(&TowardsIdentity(self.contestant_id), platform_version)?;
306
307        let mut query = Query::new_with_direction(self.order_ascending);
308
309        // this is a range on all elements
310        match &self.start_at {
311            None => {
312                query.insert_all();
313            }
314            Some((starts_at_key_bytes, start_at_included)) => {
315                let starts_at_key = starts_at_key_bytes.to_vec();
316                match self.order_ascending {
317                    true => match start_at_included {
318                        true => query.insert_range_from(starts_at_key..),
319                        false => query.insert_range_after(starts_at_key..),
320                    },
321                    false => match start_at_included {
322                        true => query.insert_range_to_inclusive(..=starts_at_key),
323                        false => query.insert_range_to(..starts_at_key),
324                    },
325                }
326            }
327        }
328
329        Ok(PathQuery {
330            path,
331            query: SizedQuery {
332                query,
333                limit: self.limit,
334                offset: self.offset,
335            },
336        })
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed;
344    use crate::util::object_size_info::DataContractResolvedInfo;
345    use dpp::tests::fixtures::get_dpns_data_contract_fixture;
346    use dpp::version::PlatformVersion;
347    use grovedb::QueryItem;
348
349    /// Helper to construct a resolved contestant votes query using the DPNS
350    /// "domain" contested index.
351    fn build_resolved_query(
352        contract: &dpp::data_contract::DataContract,
353        contestant_id: Identifier,
354        offset: Option<u16>,
355        limit: Option<u16>,
356        start_at: Option<([u8; 32], bool)>,
357        order_ascending: bool,
358    ) -> ResolvedContestedDocumentVotePollVotesDriveQuery<'_> {
359        let document_type_name = "domain".to_string();
360        let index_name = "parentNameAndLabel".to_string();
361
362        let parent_domain_value = dpp::platform_value::Value::Text("dash".to_string());
363        let label_value = dpp::platform_value::Value::Text("test-name".to_string());
364
365        let index_values = vec![parent_domain_value, label_value];
366
367        let vote_poll = ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed {
368            contract: DataContractResolvedInfo::BorrowedDataContract(contract),
369            document_type_name,
370            index_name,
371            index_values,
372        };
373
374        ResolvedContestedDocumentVotePollVotesDriveQuery {
375            vote_poll,
376            contestant_id,
377            offset,
378            limit,
379            start_at,
380            order_ascending,
381        }
382    }
383
384    // -----------------------------------------------------------------------
385    // construct_path_query tests
386    // -----------------------------------------------------------------------
387
388    #[test]
389    fn construct_path_query_no_start_ascending() {
390        let platform_version = PlatformVersion::latest();
391        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
392        let contract = dpns.data_contract_owned();
393
394        let contestant_id = Identifier::from([0xAA; 32]);
395        let query = build_resolved_query(
396            &contract,
397            contestant_id,
398            None,     // offset
399            Some(10), // limit
400            None,     // start_at
401            true,     // ascending
402        );
403
404        let pq = query
405            .construct_path_query(platform_version)
406            .expect("should build path query");
407
408        // Path should end with the contestant identifier and voting storage key
409        assert!(!pq.path.is_empty());
410        assert_eq!(pq.query.limit, Some(10));
411        assert_eq!(pq.query.offset, None);
412        assert!(pq.query.query.left_to_right);
413
414        // No start -> RangeFull
415        let items = &pq.query.query.items;
416        assert_eq!(items.len(), 1);
417        assert!(matches!(&items[0], QueryItem::RangeFull(..)));
418    }
419
420    #[test]
421    fn construct_path_query_no_start_descending() {
422        let platform_version = PlatformVersion::latest();
423        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
424        let contract = dpns.data_contract_owned();
425
426        let contestant_id = Identifier::from([0xBB; 32]);
427        let query = build_resolved_query(&contract, contestant_id, None, None, None, false);
428
429        let pq = query
430            .construct_path_query(platform_version)
431            .expect("should build path query");
432
433        assert!(!pq.query.query.left_to_right);
434        assert_eq!(pq.query.limit, None);
435    }
436
437    #[test]
438    fn construct_path_query_start_at_included_ascending() {
439        let platform_version = PlatformVersion::latest();
440        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
441        let contract = dpns.data_contract_owned();
442
443        let contestant_id = Identifier::from([0xCC; 32]);
444        let start_key = [0x42u8; 32];
445        let query = build_resolved_query(
446            &contract,
447            contestant_id,
448            None,
449            Some(5),
450            Some((start_key, true)),
451            true,
452        );
453
454        let pq = query
455            .construct_path_query(platform_version)
456            .expect("should build path query");
457
458        let items = &pq.query.query.items;
459        assert_eq!(items.len(), 1);
460        assert!(
461            matches!(&items[0], QueryItem::RangeFrom(r) if r.start == start_key.to_vec()),
462            "ascending + included = RangeFrom"
463        );
464    }
465
466    #[test]
467    fn construct_path_query_start_at_excluded_ascending() {
468        let platform_version = PlatformVersion::latest();
469        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
470        let contract = dpns.data_contract_owned();
471
472        let contestant_id = Identifier::from([0xDD; 32]);
473        let start_key = [0x42u8; 32];
474        let query = build_resolved_query(
475            &contract,
476            contestant_id,
477            None,
478            Some(5),
479            Some((start_key, false)),
480            true,
481        );
482
483        let pq = query
484            .construct_path_query(platform_version)
485            .expect("should build path query");
486
487        let items = &pq.query.query.items;
488        assert_eq!(items.len(), 1);
489        assert!(
490            matches!(&items[0], QueryItem::RangeAfter(r) if r.start == start_key.to_vec()),
491            "ascending + excluded = RangeAfter"
492        );
493    }
494
495    #[test]
496    fn construct_path_query_start_at_included_descending() {
497        let platform_version = PlatformVersion::latest();
498        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
499        let contract = dpns.data_contract_owned();
500
501        let contestant_id = Identifier::from([0xEE; 32]);
502        let start_key = [0x42u8; 32];
503        let query = build_resolved_query(
504            &contract,
505            contestant_id,
506            None,
507            Some(5),
508            Some((start_key, true)),
509            false,
510        );
511
512        let pq = query
513            .construct_path_query(platform_version)
514            .expect("should build path query");
515
516        let items = &pq.query.query.items;
517        assert_eq!(items.len(), 1);
518        assert!(
519            matches!(&items[0], QueryItem::RangeToInclusive(r) if r.end == start_key.to_vec()),
520            "descending + included = RangeToInclusive"
521        );
522    }
523
524    #[test]
525    fn construct_path_query_start_at_excluded_descending() {
526        let platform_version = PlatformVersion::latest();
527        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
528        let contract = dpns.data_contract_owned();
529
530        let contestant_id = Identifier::from([0xFF; 32]);
531        let start_key = [0x42u8; 32];
532        let query = build_resolved_query(
533            &contract,
534            contestant_id,
535            None,
536            Some(5),
537            Some((start_key, false)),
538            false,
539        );
540
541        let pq = query
542            .construct_path_query(platform_version)
543            .expect("should build path query");
544
545        let items = &pq.query.query.items;
546        assert_eq!(items.len(), 1);
547        assert!(
548            matches!(&items[0], QueryItem::RangeTo(r) if r.end == start_key.to_vec()),
549            "descending + excluded = RangeTo"
550        );
551    }
552
553    #[test]
554    fn construct_path_query_with_offset_and_limit() {
555        let platform_version = PlatformVersion::latest();
556        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
557        let contract = dpns.data_contract_owned();
558
559        let contestant_id = Identifier::from([0x11; 32]);
560        let query = build_resolved_query(
561            &contract,
562            contestant_id,
563            Some(3),  // offset
564            Some(20), // limit
565            None,
566            true,
567        );
568
569        let pq = query
570            .construct_path_query(platform_version)
571            .expect("should build path query");
572
573        assert_eq!(pq.query.limit, Some(20));
574        assert_eq!(pq.query.offset, Some(3));
575    }
576}