drive/query/
vote_poll_vote_state_query.rs

1use crate::drive::votes::paths::{
2    VotePollPaths, RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32, RESOURCE_LOCK_VOTE_TREE_KEY_U8_32,
3    RESOURCE_STORED_INFO_KEY_U8_32,
4};
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::drive::DriveError;
10use crate::error::query::QuerySyntaxError;
11use crate::error::Error;
12#[cfg(feature = "server")]
13use crate::fees::op::LowLevelDriveOperation;
14#[cfg(feature = "server")]
15use crate::query::GroveError;
16use bincode::{Decode, Encode};
17use dpp::block::block_info::BlockInfo;
18use dpp::data_contract::DataContract;
19use dpp::identifier::Identifier;
20#[cfg(feature = "server")]
21use dpp::serialization::PlatformDeserializable;
22#[cfg(feature = "server")]
23use dpp::voting::contender_structs::ContenderWithSerializedDocumentV0;
24use dpp::voting::contender_structs::{
25    ContenderWithSerializedDocument, FinalizedContenderWithSerializedDocument,
26};
27#[cfg(feature = "server")]
28use dpp::voting::vote_info_storage::contested_document_vote_poll_stored_info::ContestedDocumentVotePollStoredInfo;
29#[cfg(feature = "server")]
30use dpp::voting::vote_info_storage::contested_document_vote_poll_stored_info::ContestedDocumentVotePollStoredInfoV0Getters;
31use dpp::voting::vote_info_storage::contested_document_vote_poll_winner_info::ContestedDocumentVotePollWinnerInfo;
32use dpp::voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll;
33#[cfg(feature = "server")]
34use grovedb::query_result_type::QueryResultType;
35#[cfg(feature = "server")]
36use grovedb::{Element, TransactionArg};
37use grovedb::{PathQuery, Query, QueryItem, SizedQuery};
38use platform_version::version::PlatformVersion;
39
40/// Represents the types of results that can be obtained from a contested document vote poll query.
41///
42/// This enum defines the various types of results that can be returned when querying the drive
43/// for contested document vote poll information.
44#[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
45pub enum ContestedDocumentVotePollDriveQueryResultType {
46    /// The documents associated with the vote poll are returned in the query result.
47    Documents,
48    /// The vote tally results are returned in the query result.
49    VoteTally,
50    /// Both the documents and the vote tally results are returned in the query result.
51    DocumentsAndVoteTally,
52    /// We are searching for a single document only.
53    SingleDocumentByContender(Identifier),
54}
55
56impl ContestedDocumentVotePollDriveQueryResultType {
57    /// Helper method to say if this result type should return vote tally
58    pub fn has_vote_tally(&self) -> bool {
59        match self {
60            ContestedDocumentVotePollDriveQueryResultType::Documents => false,
61            ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(_) => false,
62            ContestedDocumentVotePollDriveQueryResultType::VoteTally => true,
63            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => true,
64        }
65    }
66
67    /// Helper method to say if this result type should return documents
68    pub fn has_documents(&self) -> bool {
69        match self {
70            ContestedDocumentVotePollDriveQueryResultType::Documents => true,
71            ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(_) => true,
72            ContestedDocumentVotePollDriveQueryResultType::VoteTally => false,
73            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => true,
74        }
75    }
76}
77
78impl TryFrom<i32> for ContestedDocumentVotePollDriveQueryResultType {
79    type Error = Error;
80
81    fn try_from(value: i32) -> Result<Self, Self::Error> {
82        match value {
83            0 => Ok(ContestedDocumentVotePollDriveQueryResultType::Documents),
84            1 => Ok(ContestedDocumentVotePollDriveQueryResultType::VoteTally),
85            2 => Ok(ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally),
86            3 => Err(Error::Query(QuerySyntaxError::Unsupported(
87                "unsupported to get SingleDocumentByContender query result type".to_string()
88            ))),
89            n => Err(Error::Query(QuerySyntaxError::Unsupported(format!(
90                "unsupported contested document vote poll drive query result type {}, only 0, 1, 2 and 3 are supported",
91                n
92            )))),
93        }
94    }
95}
96
97/// Vote Poll Drive Query struct
98#[derive(Debug, PartialEq, Clone, Encode, Decode)]
99pub struct ContestedDocumentVotePollDriveQuery {
100    /// What vote poll are we asking for?
101    pub vote_poll: ContestedDocumentResourceVotePoll,
102    /// What result type are we interested in
103    pub result_type: ContestedDocumentVotePollDriveQueryResultType,
104    /// Offset
105    pub offset: Option<u16>,
106    /// Limit for returned contestant info, including locked or abstaining votes does not change this
107    pub limit: Option<u16>,
108    /// Start at identity id
109    pub start_at: Option<([u8; 32], bool)>,
110    /// Include locked and abstaining vote tally
111    /// This is not automatic, it will just be at the beginning if the order is ascending
112    /// If the order is descending, we will get a value if we finish the query
113    pub allow_include_locked_and_abstaining_vote_tally: bool,
114}
115
116/// Represents the result of executing a contested document vote poll drive query.
117///
118/// This struct holds the list of contenders and the number of skipped items
119/// when an offset is given.
120#[derive(Debug, PartialEq, Eq, Clone, Default)]
121pub struct ContestedDocumentVotePollDriveQueryExecutionResult {
122    /// The list of contenders returned by the query.
123    pub contenders: Vec<ContenderWithSerializedDocument>,
124    /// Locked tally
125    pub locked_vote_tally: Option<u32>,
126    /// Abstaining tally
127    pub abstaining_vote_tally: Option<u32>,
128    /// Finalization info
129    pub winner: Option<(ContestedDocumentVotePollWinnerInfo, BlockInfo)>,
130    /// The number of skipped items when an offset is given.
131    pub skipped: u16,
132}
133
134/// Represents the result of executing a contested document vote poll drive query.
135///
136/// This struct holds the list of contenders and the number of skipped items
137/// when an offset is given.
138#[derive(Debug, PartialEq, Eq, Clone, Default)]
139pub struct FinalizedContestedDocumentVotePollDriveQueryExecutionResult {
140    /// The list of contenders returned by the query.
141    pub contenders: Vec<FinalizedContenderWithSerializedDocument>,
142    /// Locked tally
143    pub locked_vote_tally: u32,
144    /// Abstaining tally
145    pub abstaining_vote_tally: u32,
146}
147
148impl TryFrom<ContestedDocumentVotePollDriveQueryExecutionResult>
149    for FinalizedContestedDocumentVotePollDriveQueryExecutionResult
150{
151    type Error = Error;
152
153    fn try_from(
154        value: ContestedDocumentVotePollDriveQueryExecutionResult,
155    ) -> Result<Self, Self::Error> {
156        let ContestedDocumentVotePollDriveQueryExecutionResult {
157            contenders,
158            locked_vote_tally,
159            abstaining_vote_tally,
160            ..
161        } = value;
162
163        let finalized_contenders = contenders
164            .into_iter()
165            .map(|contender| {
166                let finalized: FinalizedContenderWithSerializedDocument = contender.try_into()?;
167                Ok(finalized)
168            })
169            .collect::<Result<Vec<_>, Error>>()?;
170
171        Ok(
172            FinalizedContestedDocumentVotePollDriveQueryExecutionResult {
173                contenders: finalized_contenders,
174                locked_vote_tally: locked_vote_tally.ok_or(Error::Drive(
175                    DriveError::CorruptedCodeExecution("expected a locked tally"),
176                ))?,
177                abstaining_vote_tally: abstaining_vote_tally.ok_or(Error::Drive(
178                    DriveError::CorruptedCodeExecution("expected an abstaining tally"),
179                ))?,
180            },
181        )
182    }
183}
184
185impl ContestedDocumentVotePollDriveQuery {
186    #[cfg(feature = "server")]
187    /// Resolves the contested document vote poll drive query.
188    ///
189    /// This method processes the query by interacting with the drive, using the provided
190    /// transaction and platform version to ensure consistency and compatibility.
191    ///
192    /// # Parameters
193    ///
194    /// * `drive`: A reference to the `Drive` object used for database interactions.
195    /// * `transaction`: The transaction argument used to ensure consistency during the resolve operation.
196    /// * `platform_version`: The platform version to ensure compatibility.
197    ///
198    /// # Returns
199    ///
200    /// * `Ok(ResolvedContestedDocumentVotePollDriveQuery)` - The resolved query information.
201    /// * `Err(Error)` - An error if the resolution process fails.
202    ///
203    /// # Errors
204    ///
205    /// This method returns an `Error` variant if there is an issue resolving the query.
206    /// The specific error depends on the underlying problem encountered during resolution.
207    pub fn resolve(
208        &self,
209        drive: &Drive,
210        transaction: TransactionArg,
211        platform_version: &PlatformVersion,
212    ) -> Result<ResolvedContestedDocumentVotePollDriveQuery<'_>, Error> {
213        let ContestedDocumentVotePollDriveQuery {
214            vote_poll,
215            result_type,
216            offset,
217            limit,
218            start_at,
219            allow_include_locked_and_abstaining_vote_tally,
220        } = self;
221        Ok(ResolvedContestedDocumentVotePollDriveQuery {
222            vote_poll: vote_poll.resolve_allow_borrowed(drive, transaction, platform_version)?,
223            result_type: *result_type,
224            offset: *offset,
225            limit: *limit,
226            start_at: *start_at,
227            allow_include_locked_and_abstaining_vote_tally:
228                *allow_include_locked_and_abstaining_vote_tally,
229        })
230    }
231
232    #[cfg(feature = "verify")]
233    /// Resolves with a known contract provider
234    pub fn resolve_with_known_contracts_provider<'a>(
235        &self,
236        known_contracts_provider_fn: &super::ContractLookupFn,
237    ) -> Result<ResolvedContestedDocumentVotePollDriveQuery<'a>, Error> {
238        let ContestedDocumentVotePollDriveQuery {
239            vote_poll,
240            result_type,
241            offset,
242            limit,
243            start_at,
244            allow_include_locked_and_abstaining_vote_tally,
245        } = self;
246        Ok(ResolvedContestedDocumentVotePollDriveQuery {
247            vote_poll: vote_poll
248                .resolve_with_known_contracts_provider(known_contracts_provider_fn)?,
249            result_type: *result_type,
250            offset: *offset,
251            limit: *limit,
252            start_at: *start_at,
253            allow_include_locked_and_abstaining_vote_tally:
254                *allow_include_locked_and_abstaining_vote_tally,
255        })
256    }
257
258    #[cfg(any(feature = "verify", feature = "server"))]
259    /// Resolves with a provided borrowed contract
260    pub fn resolve_with_provided_borrowed_contract<'a>(
261        &self,
262        data_contract: &'a DataContract,
263    ) -> Result<ResolvedContestedDocumentVotePollDriveQuery<'a>, Error> {
264        let ContestedDocumentVotePollDriveQuery {
265            vote_poll,
266            result_type,
267            offset,
268            limit,
269            start_at,
270            allow_include_locked_and_abstaining_vote_tally,
271        } = self;
272        Ok(ResolvedContestedDocumentVotePollDriveQuery {
273            vote_poll: vote_poll.resolve_with_provided_borrowed_contract(data_contract)?,
274            result_type: *result_type,
275            offset: *offset,
276            limit: *limit,
277            start_at: *start_at,
278            allow_include_locked_and_abstaining_vote_tally:
279                *allow_include_locked_and_abstaining_vote_tally,
280        })
281    }
282
283    #[cfg(feature = "server")]
284    /// Executes a query with proof and returns the items and fee.
285    pub fn execute_with_proof(
286        self,
287        drive: &Drive,
288        block_info: Option<BlockInfo>,
289        transaction: TransactionArg,
290        platform_version: &PlatformVersion,
291    ) -> Result<(Vec<u8>, u64), Error> {
292        let mut drive_operations = vec![];
293        let items = self.execute_with_proof_internal(
294            drive,
295            transaction,
296            &mut drive_operations,
297            platform_version,
298        )?;
299        let cost = if let Some(block_info) = block_info {
300            let fee_result = Drive::calculate_fee(
301                None,
302                Some(drive_operations),
303                &block_info.epoch,
304                drive.config.epochs_per_era,
305                platform_version,
306                None,
307            )?;
308            fee_result.processing_fee
309        } else {
310            0
311        };
312        Ok((items, cost))
313    }
314
315    #[cfg(feature = "server")]
316    /// Executes an internal query with proof and returns the items.
317    pub(crate) fn execute_with_proof_internal(
318        self,
319        drive: &Drive,
320        transaction: TransactionArg,
321        drive_operations: &mut Vec<LowLevelDriveOperation>,
322        platform_version: &PlatformVersion,
323    ) -> Result<Vec<u8>, Error> {
324        let resolved = self.resolve(drive, transaction, platform_version)?;
325        let path_query = resolved.construct_path_query(platform_version)?;
326        // println!("{:?}", &path_query);
327        drive.grove_get_proved_path_query(
328            &path_query,
329            transaction,
330            drive_operations,
331            &platform_version.drive,
332        )
333    }
334
335    #[cfg(feature = "server")]
336    /// Executes a query with no proof and returns the items, skipped items, and fee.
337    pub fn execute_no_proof_with_cost(
338        &self,
339        drive: &Drive,
340        block_info: Option<BlockInfo>,
341        transaction: TransactionArg,
342        platform_version: &PlatformVersion,
343    ) -> Result<(ContestedDocumentVotePollDriveQueryExecutionResult, u64), Error> {
344        let mut drive_operations = vec![];
345        let result =
346            self.execute_no_proof(drive, transaction, &mut drive_operations, platform_version)?;
347        let cost = if let Some(block_info) = block_info {
348            let fee_result = Drive::calculate_fee(
349                None,
350                Some(drive_operations),
351                &block_info.epoch,
352                drive.config.epochs_per_era,
353                platform_version,
354                None,
355            )?;
356            fee_result.processing_fee
357        } else {
358            0
359        };
360        Ok((result, cost))
361    }
362
363    #[cfg(feature = "server")]
364    /// Executes an internal query with no proof and returns the values and skipped items.
365    pub fn execute_no_proof(
366        &self,
367        drive: &Drive,
368        transaction: TransactionArg,
369        drive_operations: &mut Vec<LowLevelDriveOperation>,
370        platform_version: &PlatformVersion,
371    ) -> Result<ContestedDocumentVotePollDriveQueryExecutionResult, Error> {
372        let resolved = self.resolve(drive, transaction, platform_version)?;
373        resolved.execute(drive, transaction, drive_operations, platform_version)
374    }
375}
376
377/// Vote Poll Drive Query struct
378#[derive(Debug, PartialEq, Clone)]
379pub struct ResolvedContestedDocumentVotePollDriveQuery<'a> {
380    /// What vote poll are we asking for?
381    pub vote_poll: ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed<'a>,
382    /// What result type are we interested in
383    pub result_type: ContestedDocumentVotePollDriveQueryResultType,
384    /// Offset
385    pub offset: Option<u16>,
386    /// Limit
387    pub limit: Option<u16>,
388    /// Start at identity id, the bool is if it is also included
389    pub start_at: Option<([u8; 32], bool)>,
390    /// Include locked and abstaining vote tally
391    pub allow_include_locked_and_abstaining_vote_tally: bool,
392}
393
394impl ResolvedContestedDocumentVotePollDriveQuery<'_> {
395    /// Operations to construct a path query.
396    pub fn construct_path_query(
397        &self,
398        platform_version: &PlatformVersion,
399    ) -> Result<PathQuery, Error> {
400        let path = self.vote_poll.contenders_path(platform_version)?;
401
402        let mut query = Query::new();
403
404        let allow_include_locked_and_abstaining_vote_tally = self
405            .allow_include_locked_and_abstaining_vote_tally
406            && self.result_type.has_vote_tally();
407
408        // We have the following
409        // Stored Info [[0;31],0] Abstain votes [[0;31],1] Lock Votes [[0;31],2]
410
411        // this is a range on all elements
412        let limit = match &self.start_at {
413            None => {
414                if allow_include_locked_and_abstaining_vote_tally {
415                    match &self.result_type {
416                            ContestedDocumentVotePollDriveQueryResultType::Documents => {
417                                // Documents don't care about the vote tallies
418                                query.insert_range_after(RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.to_vec()..);
419                                self.limit
420                            }
421                            ContestedDocumentVotePollDriveQueryResultType::VoteTally => {
422                                query.insert_all();
423                                self.limit.map(|limit| limit.saturating_add(3))
424                            }
425                            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => {
426                                query.insert_all();
427                                self.limit.map(|limit| limit.saturating_mul(2).saturating_add(3))
428                            }
429                            ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(contender_id) => {
430                                query.insert_key(contender_id.to_vec());
431                                self.limit
432                            }
433                        }
434                } else {
435                    match &self.result_type {
436                            ContestedDocumentVotePollDriveQueryResultType::Documents => {
437                                query.insert_range_after(RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.to_vec()..);
438                                self.limit
439                            }
440                            ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(contender_id) => {
441                                query.insert_key(contender_id.to_vec());
442                                self.limit
443                            }
444                            ContestedDocumentVotePollDriveQueryResultType::VoteTally => {
445                                query.insert_key(RESOURCE_STORED_INFO_KEY_U8_32.to_vec());
446                                query.insert_range_after(RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.to_vec()..);
447                                self.limit.map(|limit| limit.saturating_add(1))
448                            }
449                            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => {
450                                query.insert_key(RESOURCE_STORED_INFO_KEY_U8_32.to_vec());
451                                query.insert_range_after(RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.to_vec()..);
452                                self.limit.map(|limit| limit.saturating_mul(2).saturating_add(1))
453                            }
454                        }
455                }
456            }
457            Some((starts_at_key_bytes, start_at_included)) => {
458                let starts_at_key = starts_at_key_bytes.to_vec();
459                match start_at_included {
460                    true => query.insert_range_from(starts_at_key..),
461                    false => query.insert_range_after(starts_at_key..),
462                }
463                match &self.result_type {
464                    ContestedDocumentVotePollDriveQueryResultType::Documents
465                    | ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(_)
466                    | ContestedDocumentVotePollDriveQueryResultType::VoteTally => self.limit,
467                    ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => {
468                        self.limit.map(|limit| limit.saturating_mul(2))
469                    }
470                }
471            }
472        };
473
474        let (subquery_path, subquery) = match self.result_type {
475            ContestedDocumentVotePollDriveQueryResultType::Documents
476            | ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(_) => {
477                (Some(vec![vec![0]]), None)
478            }
479            ContestedDocumentVotePollDriveQueryResultType::VoteTally => (Some(vec![vec![1]]), None),
480            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => {
481                let mut query = Query::new();
482                query.insert_keys(vec![vec![0], vec![1]]);
483                (None, Some(query.into()))
484            }
485        };
486
487        query.default_subquery_branch.subquery_path = subquery_path;
488        query.default_subquery_branch.subquery = subquery;
489
490        if allow_include_locked_and_abstaining_vote_tally {
491            query.add_conditional_subquery(
492                QueryItem::Key(RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.to_vec()),
493                Some(vec![vec![1]]),
494                None,
495            );
496            query.add_conditional_subquery(
497                QueryItem::Key(RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32.to_vec()),
498                Some(vec![vec![1]]),
499                None,
500            );
501        }
502
503        query.add_conditional_subquery(
504            QueryItem::Key(RESOURCE_STORED_INFO_KEY_U8_32.to_vec()),
505            None,
506            None,
507        );
508
509        Ok(PathQuery {
510            path,
511            query: SizedQuery {
512                query,
513                limit,
514                offset: self.offset,
515            },
516        })
517    }
518
519    #[cfg(feature = "server")]
520    /// Executes the query with no proof
521    pub fn execute(
522        &self,
523        drive: &Drive,
524        transaction: TransactionArg,
525        drive_operations: &mut Vec<LowLevelDriveOperation>,
526        platform_version: &PlatformVersion,
527    ) -> Result<ContestedDocumentVotePollDriveQueryExecutionResult, Error> {
528        let path_query = self.construct_path_query(platform_version)?;
529        // println!("path_query {:?}", &path_query);
530        let query_result = drive.grove_get_path_query(
531            &path_query,
532            transaction,
533            QueryResultType::QueryPathKeyElementTrioResultType,
534            drive_operations,
535            &platform_version.drive,
536        );
537        match query_result {
538            Err(Error::GroveDB(e))
539                if matches!(
540                    e.as_ref(),
541                    GroveError::PathKeyNotFound(_)
542                        | GroveError::PathNotFound(_)
543                        | GroveError::PathParentLayerNotFound(_)
544                ) =>
545            {
546                Ok(ContestedDocumentVotePollDriveQueryExecutionResult::default())
547            }
548            Err(e) => Err(e),
549            Ok((query_result_elements, skipped)) => {
550                match self.result_type {
551                    ContestedDocumentVotePollDriveQueryResultType::Documents
552                    | ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(_) =>
553                    {
554                        // with documents only we don't need to work about lock and abstaining tree
555                        let contenders = query_result_elements
556                            .to_path_key_elements()
557                            .into_iter()
558                            .map(|(mut path, _key, document)| {
559                                let identity_id = path.pop().ok_or(Error::Drive(
560                                    DriveError::CorruptedDriveState(
561                                        "the path must have a last element".to_string(),
562                                    ),
563                                ))?;
564                                Ok(ContenderWithSerializedDocumentV0 {
565                                    identity_id: Identifier::try_from(identity_id)?,
566                                    serialized_document: Some(document.into_item_bytes()?),
567                                    vote_tally: None,
568                                }
569                                .into())
570                            })
571                            .collect::<Result<Vec<ContenderWithSerializedDocument>, Error>>()?;
572
573                        Ok(ContestedDocumentVotePollDriveQueryExecutionResult {
574                            contenders,
575                            locked_vote_tally: None,
576                            abstaining_vote_tally: None,
577                            winner: None,
578                            skipped,
579                        })
580                    }
581                    ContestedDocumentVotePollDriveQueryResultType::VoteTally => {
582                        let mut contenders = Vec::new();
583                        let mut locked_vote_tally: Option<u32> = None;
584                        let mut abstaining_vote_tally: Option<u32> = None;
585                        let mut winner = None;
586
587                        for (path, first_key, element) in
588                            query_result_elements.to_path_key_elements().into_iter()
589                        {
590                            let Some(identity_bytes) = path.last() else {
591                                return Err(Error::Drive(DriveError::CorruptedDriveState(
592                                    "the path must have a last element".to_string(),
593                                )));
594                            };
595                            match element {
596                                Element::SumTree(_, sum_tree_value, _) => {
597                                    if sum_tree_value < 0 || sum_tree_value > u32::MAX as i64 {
598                                        return Err(Error::Drive(DriveError::CorruptedDriveState(format!(
599                                            "sum tree value for vote tally must be between 0 and u32::Max, received {} from state",
600                                            sum_tree_value
601                                        ))));
602                                    }
603
604                                    if identity_bytes.as_slice()
605                                        == RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.as_slice()
606                                    {
607                                        locked_vote_tally = Some(sum_tree_value as u32);
608                                    } else if identity_bytes.as_slice()
609                                        == RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32.as_slice()
610                                    {
611                                        abstaining_vote_tally = Some(sum_tree_value as u32);
612                                    } else {
613                                        contenders.push(
614                                            ContenderWithSerializedDocumentV0 {
615                                                identity_id: Identifier::try_from(identity_bytes)?,
616                                                serialized_document: None,
617                                                vote_tally: Some(sum_tree_value as u32),
618                                            }
619                                            .into(),
620                                        );
621                                    }
622                                }
623                                Element::Item(serialized_item_info, _) => {
624                                    if first_key.as_slice() == RESOURCE_STORED_INFO_KEY_U8_32 {
625                                        // this is the stored info, let's check to see if the vote is over
626                                        let finalized_contested_document_vote_poll_stored_info = ContestedDocumentVotePollStoredInfo::deserialize_from_bytes(&serialized_item_info)?;
627                                        if finalized_contested_document_vote_poll_stored_info
628                                            .vote_poll_status()
629                                            .awarded_or_locked()
630                                        {
631                                            locked_vote_tally = Some(
632                                                finalized_contested_document_vote_poll_stored_info
633                                                    .last_locked_votes()
634                                                    .ok_or(Error::Drive(
635                                                        DriveError::CorruptedDriveState(
636                                                            "we should have last locked votes"
637                                                                .to_string(),
638                                                        ),
639                                                    ))?,
640                                            );
641                                            abstaining_vote_tally = Some(
642                                                finalized_contested_document_vote_poll_stored_info
643                                                    .last_abstain_votes()
644                                                    .ok_or(Error::Drive(
645                                                        DriveError::CorruptedDriveState(
646                                                            "we should have last abstain votes"
647                                                                .to_string(),
648                                                        ),
649                                                    ))?,
650                                            );
651                                            winner = Some((
652                                                finalized_contested_document_vote_poll_stored_info.winner(),
653                                                finalized_contested_document_vote_poll_stored_info
654                                                    .last_finalization_block().ok_or(Error::Drive(DriveError::CorruptedDriveState(
655                                                    "we should have a last finalization block".to_string(),
656                                                )))?,
657                                            ));
658                                            contenders = finalized_contested_document_vote_poll_stored_info
659                                                .contender_votes_in_vec_of_contender_with_serialized_document().ok_or(Error::Drive(DriveError::CorruptedDriveState(
660                                                "we should have a last contender votes".to_string(),
661                                            )))?;
662                                        }
663                                    } else {
664                                        return Err(Error::Drive(
665                                            DriveError::CorruptedDriveState(
666                                                "the only item that should be returned should be stored info"
667                                                    .to_string(),
668                                            ),
669                                        ));
670                                    }
671                                }
672                                _ => {
673                                    return Err(Error::Drive(DriveError::CorruptedDriveState(
674                                        "unexpected element type in result".to_string(),
675                                    )));
676                                }
677                            }
678                        }
679                        Ok(ContestedDocumentVotePollDriveQueryExecutionResult {
680                            contenders,
681                            locked_vote_tally,
682                            abstaining_vote_tally,
683                            winner,
684                            skipped,
685                        })
686                    }
687                    ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => {
688                        let mut elements_iter =
689                            query_result_elements.to_path_key_elements().into_iter();
690                        let mut contenders = vec![];
691                        let mut locked_vote_tally: Option<u32> = None;
692                        let mut abstaining_vote_tally: Option<u32> = None;
693                        let mut winner = None;
694
695                        // Handle ascending order
696                        while let Some((path, first_key, element)) = elements_iter.next() {
697                            let Some(identity_bytes) = path.last() else {
698                                return Err(Error::Drive(DriveError::CorruptedDriveState(
699                                    "the path must have a last element".to_string(),
700                                )));
701                            };
702
703                            match element {
704                                Element::SumTree(_, sum_tree_value, _) => {
705                                    if sum_tree_value < 0 || sum_tree_value > u32::MAX as i64 {
706                                        return Err(Error::Drive(DriveError::CorruptedDriveState(format!(
707                                            "sum tree value for vote tally must be between 0 and u32::Max, received {} from state",
708                                            sum_tree_value
709                                        ))));
710                                    }
711
712                                    if identity_bytes.as_slice()
713                                        == RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.as_slice()
714                                    {
715                                        locked_vote_tally = Some(sum_tree_value as u32);
716                                    } else if identity_bytes.as_slice()
717                                        == RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32.as_slice()
718                                    {
719                                        abstaining_vote_tally = Some(sum_tree_value as u32);
720                                    } else {
721                                        return Err(Error::Drive(DriveError::CorruptedDriveState(
722                                            "unexpected key for sum tree value".to_string(),
723                                        )));
724                                    }
725                                }
726                                Element::Item(serialized_item_info, _) => {
727                                    if first_key.as_slice() == RESOURCE_STORED_INFO_KEY_U8_32 {
728                                        // this is the stored info, let's check to see if the vote is over
729                                        let finalized_contested_document_vote_poll_stored_info = ContestedDocumentVotePollStoredInfo::deserialize_from_bytes(&serialized_item_info)?;
730                                        if finalized_contested_document_vote_poll_stored_info
731                                            .vote_poll_status()
732                                            .awarded_or_locked()
733                                        {
734                                            locked_vote_tally = Some(
735                                                finalized_contested_document_vote_poll_stored_info
736                                                    .last_locked_votes()
737                                                    .ok_or(Error::Drive(
738                                                        DriveError::CorruptedDriveState(
739                                                            "we should have last locked votes"
740                                                                .to_string(),
741                                                        ),
742                                                    ))?,
743                                            );
744                                            abstaining_vote_tally = Some(
745                                                finalized_contested_document_vote_poll_stored_info
746                                                    .last_abstain_votes()
747                                                    .ok_or(Error::Drive(
748                                                        DriveError::CorruptedDriveState(
749                                                            "we should have last abstain votes"
750                                                                .to_string(),
751                                                        ),
752                                                    ))?,
753                                            );
754                                            winner = Some((
755                                                finalized_contested_document_vote_poll_stored_info.winner(),
756                                                finalized_contested_document_vote_poll_stored_info
757                                                    .last_finalization_block().ok_or(Error::Drive(DriveError::CorruptedDriveState(
758                                                    "we should have a last finalization block".to_string(),
759                                                )))?,
760                                            ));
761                                            contenders = finalized_contested_document_vote_poll_stored_info
762                                                .contender_votes_in_vec_of_contender_with_serialized_document().ok_or(Error::Drive(DriveError::CorruptedDriveState(
763                                                "we should have a last contender votes".to_string(),
764                                            )))?;
765                                        }
766                                    } else {
767                                        // We should find a sum tree paired with this document
768                                        if let Some((
769                                            path_tally,
770                                            second_key,
771                                            Element::SumTree(_, sum_tree_value, _),
772                                        )) = elements_iter.next()
773                                        {
774                                            if path != path_tally {
775                                                return Err(Error::Drive(DriveError::CorruptedDriveState(format!("the two results in a chunk when requesting documents and vote tally should both have the same path asc, got {}:{}, and {}:{}", path.iter().map(hex::encode).collect::<Vec<_>>().join("/"), hex::encode(first_key), path_tally.iter().map(hex::encode).collect::<Vec<_>>().join("/"), hex::encode(second_key)))));
776                                            }
777
778                                            if sum_tree_value < 0
779                                                || sum_tree_value > u32::MAX as i64
780                                            {
781                                                return Err(Error::Drive(DriveError::CorruptedDriveState(format!(
782                                                    "sum tree value for vote tally must be between 0 and u32::Max, received {} from state",
783                                                    sum_tree_value
784                                                ))));
785                                            }
786
787                                            let identity_id =
788                                                Identifier::from_bytes(identity_bytes)?;
789                                            let contender = ContenderWithSerializedDocumentV0 {
790                                                identity_id,
791                                                serialized_document: Some(serialized_item_info),
792                                                vote_tally: Some(sum_tree_value as u32),
793                                            }
794                                            .into();
795                                            contenders.push(contender);
796                                        } else {
797                                            return Err(Error::Drive(
798                                                DriveError::CorruptedDriveState(
799                                                    "we should have a sum item after a normal item"
800                                                        .to_string(),
801                                                ),
802                                            ));
803                                        }
804                                    }
805                                }
806                                _ => {
807                                    return Err(Error::Drive(DriveError::CorruptedDriveState(
808                                        "unexpected element type in result".to_string(),
809                                    )));
810                                }
811                            }
812                        }
813
814                        Ok(ContestedDocumentVotePollDriveQueryExecutionResult {
815                            contenders,
816                            locked_vote_tally,
817                            abstaining_vote_tally,
818                            winner,
819                            skipped,
820                        })
821                    }
822                }
823            }
824        }
825    }
826}
827
828#[cfg(test)]
829mod tests {
830    use super::*;
831    use dpp::identifier::Identifier;
832    use dpp::tests::fixtures::get_dpns_data_contract_fixture;
833    use dpp::version::PlatformVersion;
834    use dpp::voting::contender_structs::ContenderWithSerializedDocumentV0;
835
836    use crate::drive::votes::resolved::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed;
837    use crate::util::object_size_info::DataContractResolvedInfo;
838
839    /// Helper: build a `ResolvedContestedDocumentVotePollDriveQuery` using
840    /// the DPNS "domain" document type's contested index (`parentNameAndLabel`).
841    fn build_resolved_query(
842        contract: &dpp::data_contract::DataContract,
843        result_type: ContestedDocumentVotePollDriveQueryResultType,
844        offset: Option<u16>,
845        limit: Option<u16>,
846        start_at: Option<([u8; 32], bool)>,
847        allow_include_locked_and_abstaining: bool,
848    ) -> ResolvedContestedDocumentVotePollDriveQuery<'_> {
849        // The DPNS "domain" document type has a contested index "parentNameAndLabel"
850        // with properties: normalizedParentDomainName, normalizedLabel
851        let document_type_name = "domain".to_string();
852        let index_name = "parentNameAndLabel".to_string();
853
854        let parent_domain_value = dpp::platform_value::Value::Text("dash".to_string());
855        let label_value = dpp::platform_value::Value::Text("test-name".to_string());
856
857        let index_values = vec![parent_domain_value, label_value];
858
859        let vote_poll = ContestedDocumentResourceVotePollWithContractInfoAllowBorrowed {
860            contract: DataContractResolvedInfo::BorrowedDataContract(contract),
861            document_type_name,
862            index_name,
863            index_values,
864        };
865
866        ResolvedContestedDocumentVotePollDriveQuery {
867            vote_poll,
868            result_type,
869            offset,
870            limit,
871            start_at,
872            allow_include_locked_and_abstaining_vote_tally: allow_include_locked_and_abstaining,
873        }
874    }
875
876    // -----------------------------------------------------------------------
877    // ContestedDocumentVotePollDriveQueryResultType helper methods
878    // -----------------------------------------------------------------------
879
880    #[test]
881    fn has_vote_tally_returns_correct_values() {
882        use ContestedDocumentVotePollDriveQueryResultType::*;
883        assert!(!Documents.has_vote_tally());
884        assert!(VoteTally.has_vote_tally());
885        assert!(DocumentsAndVoteTally.has_vote_tally());
886        assert!(!SingleDocumentByContender(Identifier::default()).has_vote_tally());
887    }
888
889    #[test]
890    fn has_documents_returns_correct_values() {
891        use ContestedDocumentVotePollDriveQueryResultType::*;
892        assert!(Documents.has_documents());
893        assert!(!VoteTally.has_documents());
894        assert!(DocumentsAndVoteTally.has_documents());
895        assert!(SingleDocumentByContender(Identifier::default()).has_documents());
896    }
897
898    // -----------------------------------------------------------------------
899    // TryFrom<i32> for ContestedDocumentVotePollDriveQueryResultType
900    // -----------------------------------------------------------------------
901
902    #[test]
903    fn try_from_i32_valid_values() {
904        let docs = ContestedDocumentVotePollDriveQueryResultType::try_from(0).unwrap();
905        assert_eq!(
906            docs,
907            ContestedDocumentVotePollDriveQueryResultType::Documents
908        );
909
910        let tally = ContestedDocumentVotePollDriveQueryResultType::try_from(1).unwrap();
911        assert_eq!(
912            tally,
913            ContestedDocumentVotePollDriveQueryResultType::VoteTally
914        );
915
916        let both = ContestedDocumentVotePollDriveQueryResultType::try_from(2).unwrap();
917        assert_eq!(
918            both,
919            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally
920        );
921    }
922
923    #[test]
924    fn try_from_i32_value_3_returns_unsupported_error() {
925        let result = ContestedDocumentVotePollDriveQueryResultType::try_from(3);
926        assert!(result.is_err());
927        let err = result.unwrap_err();
928        assert!(
929            matches!(err, Error::Query(QuerySyntaxError::Unsupported(msg)) if msg.contains("SingleDocumentByContender"))
930        );
931    }
932
933    #[test]
934    fn try_from_i32_out_of_range_returns_unsupported_error() {
935        let result = ContestedDocumentVotePollDriveQueryResultType::try_from(99);
936        assert!(result.is_err());
937        let err = result.unwrap_err();
938        assert!(
939            matches!(err, Error::Query(QuerySyntaxError::Unsupported(msg)) if msg.contains("99"))
940        );
941
942        let result_neg = ContestedDocumentVotePollDriveQueryResultType::try_from(-1);
943        assert!(result_neg.is_err());
944    }
945
946    // -----------------------------------------------------------------------
947    // TryFrom<ContestedDocumentVotePollDriveQueryExecutionResult>
948    //   for FinalizedContestedDocumentVotePollDriveQueryExecutionResult
949    // -----------------------------------------------------------------------
950
951    #[test]
952    fn finalized_try_from_success_with_complete_data() {
953        let id = Identifier::from([0xAA; 32]);
954        let contender = ContenderWithSerializedDocumentV0 {
955            identity_id: id,
956            serialized_document: Some(vec![1, 2, 3]),
957            vote_tally: Some(42),
958        };
959        let result = ContestedDocumentVotePollDriveQueryExecutionResult {
960            contenders: vec![contender.into()],
961            locked_vote_tally: Some(10),
962            abstaining_vote_tally: Some(5),
963            winner: None,
964            skipped: 0,
965        };
966
967        let finalized: FinalizedContestedDocumentVotePollDriveQueryExecutionResult =
968            result.try_into().expect("should convert");
969        assert_eq!(finalized.contenders.len(), 1);
970        assert_eq!(finalized.locked_vote_tally, 10);
971        assert_eq!(finalized.abstaining_vote_tally, 5);
972    }
973
974    #[test]
975    fn finalized_try_from_fails_without_locked_tally() {
976        let result = ContestedDocumentVotePollDriveQueryExecutionResult {
977            contenders: vec![],
978            locked_vote_tally: None,
979            abstaining_vote_tally: Some(5),
980            winner: None,
981            skipped: 0,
982        };
983
984        let conversion: Result<FinalizedContestedDocumentVotePollDriveQueryExecutionResult, _> =
985            result.try_into();
986        assert!(conversion.is_err());
987    }
988
989    #[test]
990    fn finalized_try_from_fails_without_abstaining_tally() {
991        let result = ContestedDocumentVotePollDriveQueryExecutionResult {
992            contenders: vec![],
993            locked_vote_tally: Some(10),
994            abstaining_vote_tally: None,
995            winner: None,
996            skipped: 0,
997        };
998
999        let conversion: Result<FinalizedContestedDocumentVotePollDriveQueryExecutionResult, _> =
1000            result.try_into();
1001        assert!(conversion.is_err());
1002    }
1003
1004    #[test]
1005    fn finalized_try_from_fails_when_contender_missing_document() {
1006        let contender = ContenderWithSerializedDocumentV0 {
1007            identity_id: Identifier::from([0xBB; 32]),
1008            serialized_document: None, // missing
1009            vote_tally: Some(10),
1010        };
1011        let result = ContestedDocumentVotePollDriveQueryExecutionResult {
1012            contenders: vec![contender.into()],
1013            locked_vote_tally: Some(10),
1014            abstaining_vote_tally: Some(5),
1015            winner: None,
1016            skipped: 0,
1017        };
1018
1019        let conversion: Result<FinalizedContestedDocumentVotePollDriveQueryExecutionResult, _> =
1020            result.try_into();
1021        assert!(conversion.is_err());
1022    }
1023
1024    #[test]
1025    fn finalized_try_from_fails_when_contender_missing_vote_tally() {
1026        let contender = ContenderWithSerializedDocumentV0 {
1027            identity_id: Identifier::from([0xCC; 32]),
1028            serialized_document: Some(vec![1]),
1029            vote_tally: None, // missing
1030        };
1031        let result = ContestedDocumentVotePollDriveQueryExecutionResult {
1032            contenders: vec![contender.into()],
1033            locked_vote_tally: Some(10),
1034            abstaining_vote_tally: Some(5),
1035            winner: None,
1036            skipped: 0,
1037        };
1038
1039        let conversion: Result<FinalizedContestedDocumentVotePollDriveQueryExecutionResult, _> =
1040            result.try_into();
1041        assert!(conversion.is_err());
1042    }
1043
1044    // -----------------------------------------------------------------------
1045    // construct_path_query on ResolvedContestedDocumentVotePollDriveQuery
1046    // -----------------------------------------------------------------------
1047
1048    #[test]
1049    fn construct_path_query_documents_no_start_no_tally() {
1050        let platform_version = PlatformVersion::latest();
1051        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1052        let contract = dpns.data_contract_owned();
1053
1054        let query = build_resolved_query(
1055            &contract,
1056            ContestedDocumentVotePollDriveQueryResultType::Documents,
1057            None,    // offset
1058            Some(5), // limit
1059            None,    // start_at
1060            false,   // allow tally
1061        );
1062
1063        let path_query = query
1064            .construct_path_query(platform_version)
1065            .expect("should build path query");
1066
1067        // Path should have multiple components (voting root + contested + active polls + contract + doc type + index key + index values)
1068        assert!(!path_query.path.is_empty());
1069
1070        // Limit should pass through directly for Documents without tally
1071        assert_eq!(path_query.query.limit, Some(5));
1072        assert_eq!(path_query.query.offset, None);
1073
1074        // The query items should contain a RangeAfter (after RESOURCE_LOCK_VOTE_TREE_KEY)
1075        let items = &path_query.query.query.items;
1076        assert_eq!(
1077            items.len(),
1078            1,
1079            "should have exactly 1 query item for Documents without tally"
1080        );
1081        assert!(
1082            matches!(&items[0], QueryItem::RangeAfter(..)),
1083            "expected RangeAfter, got {:?}",
1084            &items[0]
1085        );
1086
1087        // Subquery path should point to document storage [vec![0]]
1088        assert_eq!(
1089            path_query.query.query.default_subquery_branch.subquery_path,
1090            Some(vec![vec![0]])
1091        );
1092    }
1093
1094    #[test]
1095    fn construct_path_query_vote_tally_with_locked_and_abstaining() {
1096        let platform_version = PlatformVersion::latest();
1097        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1098        let contract = dpns.data_contract_owned();
1099
1100        let query = build_resolved_query(
1101            &contract,
1102            ContestedDocumentVotePollDriveQueryResultType::VoteTally,
1103            None,     // offset
1104            Some(10), // limit
1105            None,     // start_at
1106            true,     // allow tally (enabled AND result_type has_vote_tally)
1107        );
1108
1109        let path_query = query
1110            .construct_path_query(platform_version)
1111            .expect("should build path query");
1112
1113        // With allow_include_locked_and_abstaining + VoteTally, query is insert_all()
1114        // and limit is original + 3
1115        assert_eq!(path_query.query.limit, Some(13));
1116
1117        // Query should be RangeFull (insert_all)
1118        let items = &path_query.query.query.items;
1119        assert_eq!(items.len(), 1);
1120        assert!(
1121            matches!(&items[0], QueryItem::RangeFull(..)),
1122            "expected RangeFull, got {:?}",
1123            &items[0]
1124        );
1125
1126        // Subquery path should point to vote tally [vec![1]]
1127        assert_eq!(
1128            path_query.query.query.default_subquery_branch.subquery_path,
1129            Some(vec![vec![1]])
1130        );
1131    }
1132
1133    #[test]
1134    fn construct_path_query_documents_and_vote_tally_with_locked_and_abstaining() {
1135        let platform_version = PlatformVersion::latest();
1136        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1137        let contract = dpns.data_contract_owned();
1138
1139        let query = build_resolved_query(
1140            &contract,
1141            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally,
1142            None,     // offset
1143            Some(10), // limit
1144            None,     // start_at
1145            true,     // allow tally
1146        );
1147
1148        let path_query = query
1149            .construct_path_query(platform_version)
1150            .expect("should build path query");
1151
1152        // With allow_include + DocumentsAndVoteTally: limit = limit * 2 + 3
1153        assert_eq!(path_query.query.limit, Some(23));
1154
1155        // Subquery should be a query with keys [0, 1] (not a path)
1156        assert!(path_query
1157            .query
1158            .query
1159            .default_subquery_branch
1160            .subquery
1161            .is_some());
1162        assert!(path_query
1163            .query
1164            .query
1165            .default_subquery_branch
1166            .subquery_path
1167            .is_none());
1168    }
1169
1170    #[test]
1171    fn construct_path_query_vote_tally_without_locked_and_abstaining() {
1172        let platform_version = PlatformVersion::latest();
1173        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1174        let contract = dpns.data_contract_owned();
1175
1176        let query = build_resolved_query(
1177            &contract,
1178            ContestedDocumentVotePollDriveQueryResultType::VoteTally,
1179            None,     // offset
1180            Some(10), // limit
1181            None,     // start_at
1182            false,    // allow_include_locked_and_abstaining = false
1183        );
1184
1185        let path_query = query
1186            .construct_path_query(platform_version)
1187            .expect("should build path query");
1188
1189        // Without locked/abstaining: VoteTally inserts StoredInfo key + RangeAfter
1190        // limit = limit + 1
1191        assert_eq!(path_query.query.limit, Some(11));
1192
1193        // Should have 2 query items: Key(RESOURCE_STORED_INFO) and RangeAfter
1194        let items = &path_query.query.query.items;
1195        assert_eq!(items.len(), 2);
1196        assert!(
1197            matches!(&items[0], QueryItem::Key(k) if *k == RESOURCE_STORED_INFO_KEY_U8_32.to_vec())
1198        );
1199        assert!(matches!(&items[1], QueryItem::RangeAfter(..)));
1200    }
1201
1202    #[test]
1203    fn construct_path_query_with_start_at_included() {
1204        let platform_version = PlatformVersion::latest();
1205        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1206        let contract = dpns.data_contract_owned();
1207
1208        let start_key = [0x42u8; 32];
1209        let query = build_resolved_query(
1210            &contract,
1211            ContestedDocumentVotePollDriveQueryResultType::Documents,
1212            None,                    // offset
1213            Some(5),                 // limit
1214            Some((start_key, true)), // start_at included
1215            false,
1216        );
1217
1218        let path_query = query
1219            .construct_path_query(platform_version)
1220            .expect("should build path query");
1221
1222        // With start_at included, should be RangeFrom
1223        let items = &path_query.query.query.items;
1224        assert_eq!(items.len(), 1);
1225        assert!(
1226            matches!(&items[0], QueryItem::RangeFrom(r) if r.start == start_key.to_vec()),
1227            "expected RangeFrom starting at start_key"
1228        );
1229        assert_eq!(path_query.query.limit, Some(5));
1230    }
1231
1232    #[test]
1233    fn construct_path_query_with_start_at_excluded() {
1234        let platform_version = PlatformVersion::latest();
1235        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1236        let contract = dpns.data_contract_owned();
1237
1238        let start_key = [0x42u8; 32];
1239        let query = build_resolved_query(
1240            &contract,
1241            ContestedDocumentVotePollDriveQueryResultType::Documents,
1242            None,                     // offset
1243            Some(5),                  // limit
1244            Some((start_key, false)), // start_at NOT included
1245            false,
1246        );
1247
1248        let path_query = query
1249            .construct_path_query(platform_version)
1250            .expect("should build path query");
1251
1252        // With start_at excluded, should be RangeAfter
1253        let items = &path_query.query.query.items;
1254        assert_eq!(items.len(), 1);
1255        assert!(
1256            matches!(&items[0], QueryItem::RangeAfter(r) if r.start == start_key.to_vec()),
1257            "expected RangeAfter starting at start_key"
1258        );
1259    }
1260
1261    #[test]
1262    fn construct_path_query_with_offset() {
1263        let platform_version = PlatformVersion::latest();
1264        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1265        let contract = dpns.data_contract_owned();
1266
1267        let query = build_resolved_query(
1268            &contract,
1269            ContestedDocumentVotePollDriveQueryResultType::Documents,
1270            Some(3),  // offset
1271            Some(10), // limit
1272            None,     // start_at
1273            false,
1274        );
1275
1276        let path_query = query
1277            .construct_path_query(platform_version)
1278            .expect("should build path query");
1279
1280        assert_eq!(path_query.query.offset, Some(3));
1281        assert_eq!(path_query.query.limit, Some(10));
1282    }
1283
1284    #[test]
1285    fn construct_path_query_no_limit() {
1286        let platform_version = PlatformVersion::latest();
1287        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1288        let contract = dpns.data_contract_owned();
1289
1290        let query = build_resolved_query(
1291            &contract,
1292            ContestedDocumentVotePollDriveQueryResultType::Documents,
1293            None, // offset
1294            None, // no limit
1295            None, // start_at
1296            false,
1297        );
1298
1299        let path_query = query
1300            .construct_path_query(platform_version)
1301            .expect("should build path query");
1302
1303        assert_eq!(path_query.query.limit, None);
1304    }
1305
1306    #[test]
1307    fn construct_path_query_documents_and_vote_tally_with_start_at_doubles_limit() {
1308        let platform_version = PlatformVersion::latest();
1309        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1310        let contract = dpns.data_contract_owned();
1311
1312        let start_key = [0x50u8; 32];
1313        let query = build_resolved_query(
1314            &contract,
1315            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally,
1316            None,                    // offset
1317            Some(10),                // limit
1318            Some((start_key, true)), // start_at included
1319            false,
1320        );
1321
1322        let path_query = query
1323            .construct_path_query(platform_version)
1324            .expect("should build path query");
1325
1326        // With start_at + DocumentsAndVoteTally: limit = limit * 2
1327        assert_eq!(path_query.query.limit, Some(20));
1328    }
1329
1330    #[test]
1331    fn construct_path_query_single_document_by_contender() {
1332        let platform_version = PlatformVersion::latest();
1333        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1334        let contract = dpns.data_contract_owned();
1335
1336        let contender_id = Identifier::from([0xDD; 32]);
1337        let query = build_resolved_query(
1338            &contract,
1339            ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(contender_id),
1340            None,    // offset
1341            Some(1), // limit
1342            None,    // start_at
1343            false,
1344        );
1345
1346        let path_query = query
1347            .construct_path_query(platform_version)
1348            .expect("should build path query");
1349
1350        // Should have a Key query item with the contender_id bytes
1351        let items = &path_query.query.query.items;
1352        assert_eq!(items.len(), 1);
1353        assert!(
1354            matches!(&items[0], QueryItem::Key(k) if k.as_slice() == contender_id.as_bytes()),
1355            "expected Key with contender ID"
1356        );
1357        assert_eq!(path_query.query.limit, Some(1));
1358
1359        // Subquery path for SingleDocumentByContender should be [vec![0]] (document storage)
1360        assert_eq!(
1361            path_query.query.query.default_subquery_branch.subquery_path,
1362            Some(vec![vec![0]])
1363        );
1364    }
1365
1366    #[test]
1367    fn construct_path_query_has_conditional_subquery_for_stored_info() {
1368        let platform_version = PlatformVersion::latest();
1369        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1370        let contract = dpns.data_contract_owned();
1371
1372        let query = build_resolved_query(
1373            &contract,
1374            ContestedDocumentVotePollDriveQueryResultType::Documents,
1375            None,
1376            Some(5),
1377            None,
1378            false,
1379        );
1380
1381        let path_query = query
1382            .construct_path_query(platform_version)
1383            .expect("should build path query");
1384
1385        // Should always have a conditional subquery for RESOURCE_STORED_INFO_KEY
1386        let conditional = path_query
1387            .query
1388            .query
1389            .conditional_subquery_branches
1390            .as_ref()
1391            .expect("should have conditional branches");
1392        let stored_info_key = QueryItem::Key(RESOURCE_STORED_INFO_KEY_U8_32.to_vec());
1393        assert!(
1394            conditional.contains_key(&stored_info_key),
1395            "should have conditional subquery for stored info key"
1396        );
1397    }
1398
1399    #[test]
1400    fn construct_path_query_with_locked_abstaining_has_conditional_subqueries() {
1401        let platform_version = PlatformVersion::latest();
1402        let dpns = get_dpns_data_contract_fixture(None, 0, platform_version.protocol_version);
1403        let contract = dpns.data_contract_owned();
1404
1405        let query = build_resolved_query(
1406            &contract,
1407            ContestedDocumentVotePollDriveQueryResultType::VoteTally,
1408            None,
1409            Some(5),
1410            None,
1411            true, // allow locked and abstaining
1412        );
1413
1414        let path_query = query
1415            .construct_path_query(platform_version)
1416            .expect("should build path query");
1417
1418        let conditional = path_query
1419            .query
1420            .query
1421            .conditional_subquery_branches
1422            .as_ref()
1423            .expect("should have conditional branches");
1424
1425        // Should have conditional subqueries for lock and abstain keys
1426        let lock_key = QueryItem::Key(RESOURCE_LOCK_VOTE_TREE_KEY_U8_32.to_vec());
1427        let abstain_key = QueryItem::Key(RESOURCE_ABSTAIN_VOTE_TREE_KEY_U8_32.to_vec());
1428        assert!(
1429            conditional.contains_key(&lock_key),
1430            "should have conditional subquery for lock key"
1431        );
1432        assert!(
1433            conditional.contains_key(&abstain_key),
1434            "should have conditional subquery for abstain key"
1435        );
1436    }
1437}