drive_proof_verifier/
from_request.rs

1//! Conversions between Drive queries and dapi-grpc requests.
2
3use dapi_grpc::platform::v0::{
4    self as proto,
5    get_contested_resource_vote_state_request::{
6        self, get_contested_resource_vote_state_request_v0,
7    },
8    get_contested_resources_request::{
9        self, get_contested_resources_request_v0, GetContestedResourcesRequestV0,
10    },
11    get_vote_polls_by_end_date_request::{self},
12    GetContestedResourceIdentityVotesRequest, GetContestedResourceVoteStateRequest,
13    GetContestedResourceVotersForIdentityRequest, GetContestedResourcesRequest,
14    GetPrefundedSpecializedBalanceRequest, GetVotePollsByEndDateRequest,
15};
16use dpp::{
17    identifier::Identifier, platform_value::Value,
18    voting::vote_polls::contested_document_resource_vote_poll::ContestedDocumentResourceVotePoll,
19};
20use drive::query::{
21    contested_resource_votes_given_by_identity_query::ContestedResourceVotesGivenByIdentityQuery,
22    vote_poll_contestant_votes_query::ContestedDocumentVotePollVotesDriveQuery,
23    vote_poll_vote_state_query::{
24        ContestedDocumentVotePollDriveQuery, ContestedDocumentVotePollDriveQueryResultType,
25    },
26    vote_polls_by_document_type_query::VotePollsByDocumentTypeQuery,
27    VotePollsByEndDateDriveQuery,
28};
29
30use crate::Error;
31
32const BINCODE_CONFIG: dpp::bincode::config::Configuration = dpp::bincode::config::standard();
33
34/// Convert a gRPC request into a query object.
35///
36/// This trait is implemented on Drive queries that can be created from gRPC requests.
37///
38/// # Generic Type Parameters
39///
40/// * `T`: The type of the gRPC request.
41pub trait TryFromRequest<T>: Sized {
42    /// Create based on some `grpc_request`.
43    fn try_from_request(grpc_request: T) -> Result<Self, Error>;
44
45    /// Try to convert the request into a gRPC query.
46    fn try_to_request(&self) -> Result<T, Error>;
47}
48
49impl TryFromRequest<get_contested_resource_vote_state_request_v0::ResultType>
50    for ContestedDocumentVotePollDriveQueryResultType
51{
52    fn try_from_request(
53        grpc_request: get_contested_resource_vote_state_request_v0::ResultType,
54    ) -> Result<Self, Error> {
55        use get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
56        use ContestedDocumentVotePollDriveQueryResultType as DriveResultType;
57
58        Ok(match grpc_request {
59            GrpcResultType::Documents => DriveResultType::Documents,
60            GrpcResultType::DocumentsAndVoteTally => DriveResultType::DocumentsAndVoteTally,
61            GrpcResultType::VoteTally => DriveResultType::VoteTally,
62        })
63    }
64    fn try_to_request(
65        &self,
66    ) -> Result<get_contested_resource_vote_state_request_v0::ResultType, Error> {
67        use get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
68        use ContestedDocumentVotePollDriveQueryResultType as DriveResultType;
69
70        Ok(match self {
71            DriveResultType::Documents => GrpcResultType::Documents,
72            DriveResultType::DocumentsAndVoteTally => GrpcResultType::DocumentsAndVoteTally,
73            DriveResultType::VoteTally => GrpcResultType::VoteTally,
74            DriveResultType::SingleDocumentByContender(_) => {
75                return Err(Error::RequestError {
76                    error: "can not perform a single document by contender query remotely"
77                        .to_string(),
78                })
79            }
80        })
81    }
82}
83
84impl TryFromRequest<GetContestedResourceVoteStateRequest> for ContestedDocumentVotePollDriveQuery {
85    fn try_from_request(grpc_request: GetContestedResourceVoteStateRequest) -> Result<Self, Error> {
86        let result = match grpc_request.version.ok_or(Error::EmptyVersion)? {
87            get_contested_resource_vote_state_request::Version::V0(v) => {
88                ContestedDocumentVotePollDriveQuery {
89                    limit: v.count.map(|v| v as u16),
90                    vote_poll: ContestedDocumentResourceVotePoll {
91                        contract_id: Identifier::from_bytes(&v.contract_id).map_err(|e| {
92                            Error::RequestError {
93                                error: format!("cannot decode contract id: {}", e),
94                            }
95                        })?,
96                        document_type_name: v.document_type_name.clone(),
97                        index_name: v.index_name.clone(),
98                        index_values: bincode_decode_values(v.index_values.iter())?,
99                    },
100                    result_type:  match v.result_type() {
101                        get_contested_resource_vote_state_request_v0::ResultType::Documents => {
102                            ContestedDocumentVotePollDriveQueryResultType::Documents
103                        }
104                        get_contested_resource_vote_state_request_v0::ResultType::DocumentsAndVoteTally => {
105                            ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally
106                        }
107                        get_contested_resource_vote_state_request_v0::ResultType::VoteTally => {
108                            ContestedDocumentVotePollDriveQueryResultType::VoteTally
109                        }
110                    },
111                    start_at: v
112                        .start_at_identifier_info
113                        .map(|v| to_bytes32(&v.start_identifier).map(|id| (id, v.start_identifier_included)))
114                        .transpose()
115                        .map_err(|e| {
116                            Error::RequestError {
117                                error: format!(
118                                "cannot decode start_at: {}",
119                                e
120                            )}}
121                        )?,
122                    offset: None, // offset is not supported when we use proofs
123                    allow_include_locked_and_abstaining_vote_tally: v
124                        .allow_include_locked_and_abstaining_vote_tally,
125                }
126            }
127        };
128        Ok(result)
129    }
130
131    fn try_to_request(&self) -> Result<GetContestedResourceVoteStateRequest, Error> {
132        use proto::get_contested_resource_vote_state_request::get_contested_resource_vote_state_request_v0 as request_v0;
133        if self.offset.is_some() {
134            return Err(Error::RequestError{error:"ContestedDocumentVotePollDriveQuery.offset field is internal and must be set to None".into()});
135        }
136
137        let start_at_identifier_info = self.start_at.map(|v| request_v0::StartAtIdentifierInfo {
138            start_identifier: v.0.to_vec(),
139            start_identifier_included: v.1,
140        });
141
142        use proto::get_contested_resource_vote_state_request:: get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
143        Ok(proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 {
144            prove:true,
145            contract_id:self.vote_poll.contract_id.to_vec(),
146            count: self.limit.map(|v| v as u32),
147            document_type_name: self.vote_poll.document_type_name.clone(),
148            index_name: self.vote_poll.index_name.clone(),
149            index_values: self.vote_poll.index_values.iter().map(|v|
150                dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e|Error::RequestError { error: e.to_string() } )).collect::<Result<Vec<_>,_>>()?,
151            result_type:match self.result_type {
152                ContestedDocumentVotePollDriveQueryResultType::Documents => GrpcResultType::Documents.into(),
153                ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally => GrpcResultType::DocumentsAndVoteTally.into(),
154                ContestedDocumentVotePollDriveQueryResultType::VoteTally => GrpcResultType::VoteTally.into(),
155                ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(_) => return Err(Error::RequestError {
156                                                                                                                                                                           error: "can not perform a single document by contender query remotely".to_string(),
157                                                                                                                                                                       }),
158            },
159            start_at_identifier_info,
160            allow_include_locked_and_abstaining_vote_tally: self.allow_include_locked_and_abstaining_vote_tally,
161        }
162        .into())
163    }
164}
165
166fn to_bytes32(v: &[u8]) -> Result<[u8; 32], Error> {
167    let result: Result<[u8; 32], std::array::TryFromSliceError> = v.try_into();
168    match result {
169        Ok(id) => Ok(id),
170        Err(e) => Err(Error::RequestError {
171            error: format!("cannot decode id: {}", e),
172        }),
173    }
174}
175
176impl TryFromRequest<GetContestedResourceIdentityVotesRequest>
177    for ContestedResourceVotesGivenByIdentityQuery
178{
179    fn try_from_request(
180        grpc_request: GetContestedResourceIdentityVotesRequest,
181    ) -> Result<Self, Error> {
182        let proto::get_contested_resource_identity_votes_request::Version::V0(value) =
183            grpc_request.version.ok_or(Error::EmptyVersion)?;
184        let start_at = value
185            .start_at_vote_poll_id_info
186            .map(|v| {
187                to_bytes32(&v.start_at_poll_identifier)
188                    .map(|id| (id, v.start_poll_identifier_included))
189            })
190            .transpose()?;
191
192        Ok(Self {
193            identity_id: Identifier::from_vec(value.identity_id.to_vec()).map_err(|e| {
194                Error::RequestError {
195                    error: e.to_string(),
196                }
197            })?,
198            offset: None,
199            limit: value.limit.map(|x| x as u16),
200            start_at,
201            order_ascending: value.order_ascending,
202        })
203    }
204
205    fn try_to_request(&self) -> Result<GetContestedResourceIdentityVotesRequest, Error> {
206        use proto::get_contested_resource_identity_votes_request::get_contested_resource_identity_votes_request_v0 as request_v0;
207        if self.offset.is_some() {
208            return Err(Error::RequestError{error:"ContestedResourceVotesGivenByIdentityQuery.offset field is internal and must be set to None".into()});
209        }
210
211        Ok(proto::get_contested_resource_identity_votes_request::GetContestedResourceIdentityVotesRequestV0 {
212                    prove: true,
213                    identity_id: self.identity_id.to_vec(),
214                    offset: self.offset.map(|x| x as u32),
215                    limit: self.limit.map(|x| x as u32),
216                    start_at_vote_poll_id_info: self.start_at.map(|(id, included)| {
217                        request_v0::StartAtVotePollIdInfo {
218                            start_at_poll_identifier: id.to_vec(),
219                            start_poll_identifier_included: included,
220                        }
221                    }),
222                    order_ascending: self.order_ascending,
223                }.into()
224            )
225    }
226}
227
228use dapi_grpc::platform::v0::get_contested_resource_voters_for_identity_request;
229
230impl TryFromRequest<GetContestedResourceVotersForIdentityRequest>
231    for ContestedDocumentVotePollVotesDriveQuery
232{
233    fn try_from_request(
234        value: GetContestedResourceVotersForIdentityRequest,
235    ) -> Result<Self, Error> {
236        let result = match value.version.ok_or(Error::EmptyVersion)? {
237            get_contested_resource_voters_for_identity_request::Version::V0(v) => {
238                ContestedDocumentVotePollVotesDriveQuery {
239                    vote_poll: ContestedDocumentResourceVotePoll {
240                        contract_id: Identifier::from_bytes(&v.contract_id).map_err(|e| {
241                            Error::RequestError {
242                                error: format!("cannot decode contract id: {}", e),
243                            }
244                        })?,
245                        document_type_name: v.document_type_name.clone(),
246                        index_name: v.index_name.clone(),
247                        index_values: bincode_decode_values(v.index_values.iter())?,
248                    },
249                    contestant_id: Identifier::from_bytes(&v.contestant_id).map_err(|e| {
250                        Error::RequestError {
251                            error: format!("cannot decode contestant_id: {}", e),
252                        }
253                    })?,
254                    limit: v.count.map(|v| v as u16),
255                    offset: None,
256                    start_at: v
257                        .start_at_identifier_info
258                        .map(|v| {
259                            to_bytes32(&v.start_identifier)
260                                .map(|id| (id, v.start_identifier_included))
261                        })
262                        .transpose()
263                        .map_err(|e| Error::RequestError {
264                            error: format!("cannot decode start_at value: {}", e),
265                        })?,
266                    order_ascending: v.order_ascending,
267                }
268            }
269        };
270
271        Ok(result)
272    }
273    fn try_to_request(&self) -> Result<GetContestedResourceVotersForIdentityRequest, Error> {
274        use proto::get_contested_resource_voters_for_identity_request::get_contested_resource_voters_for_identity_request_v0 as request_v0;
275        if self.offset.is_some() {
276            return Err(Error::RequestError{error:"ContestedDocumentVotePollVotesDriveQuery.offset field is internal and must be set to None".into()});
277        }
278
279        Ok(proto::get_contested_resource_voters_for_identity_request::GetContestedResourceVotersForIdentityRequestV0 {
280            prove:true,
281            contract_id: self.vote_poll.contract_id.to_vec(),
282            document_type_name: self.vote_poll.document_type_name.clone(),
283            index_name: self.vote_poll.index_name.clone(),
284            index_values: self.vote_poll.index_values.iter().map(|v|
285                dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e|
286                    Error::RequestError { error: e.to_string()})).collect::<Result<Vec<_>,_>>()?,
287            order_ascending: self.order_ascending,
288            count: self.limit.map(|v| v as u32),
289            contestant_id: self.contestant_id.to_vec(),
290            start_at_identifier_info: self.start_at.map(|v| request_v0::StartAtIdentifierInfo{
291                start_identifier: v.0.to_vec(),
292                start_identifier_included: v.1,
293            }),
294        }
295        .into())
296    }
297}
298
299impl TryFromRequest<GetContestedResourcesRequest> for VotePollsByDocumentTypeQuery {
300    fn try_from_request(value: GetContestedResourcesRequest) -> Result<Self, Error> {
301        let result = match value.version.ok_or(Error::EmptyVersion)? {
302            get_contested_resources_request::Version::V0(req) => VotePollsByDocumentTypeQuery {
303                contract_id: Identifier::from_bytes(&req.contract_id).map_err(|e| {
304                    Error::RequestError {
305                        error: format!("cannot decode contract id: {}", e),
306                    }
307                })?,
308                document_type_name: req.document_type_name.clone(),
309                index_name: req.index_name.clone(),
310                start_at_value: req
311                    .start_at_value_info
312                    .map(|i| {
313                        let (value, _): (Value, _) =
314                            bincode::decode_from_slice(&i.start_value, BINCODE_CONFIG).map_err(
315                                |e| Error::RequestError {
316                                    error: format!("cannot decode start value: {}", e),
317                                },
318                            )?;
319                        Ok::<_, Error>((value, i.start_value_included))
320                    })
321                    .transpose()?,
322                start_index_values: bincode_decode_values(req.start_index_values.iter())?,
323                end_index_values: bincode_decode_values(req.end_index_values.iter())?,
324                limit: req.count.map(|v| v as u16),
325                order_ascending: req.order_ascending,
326            },
327        };
328        Ok(result)
329    }
330
331    fn try_to_request(&self) -> Result<GetContestedResourcesRequest, Error> {
332        Ok(GetContestedResourcesRequestV0 {
333            prove: true,
334            contract_id: self.contract_id.to_vec(),
335            count: self.limit.map(|v| v as u32),
336            document_type_name: self.document_type_name.clone(),
337            end_index_values: bincode_encode_values(&self.end_index_values)?,
338            start_index_values: bincode_encode_values(&self.start_index_values)?,
339            index_name: self.index_name.clone(),
340            order_ascending: self.order_ascending,
341            start_at_value_info: self
342                .start_at_value
343                .as_ref()
344                .map(|(start_value, start_value_included)| {
345                    Ok::<_, Error>(get_contested_resources_request_v0::StartAtValueInfo {
346                        start_value: bincode::encode_to_vec(start_value, BINCODE_CONFIG).map_err(
347                            |e| Error::RequestError {
348                                error: format!("cannot encode start value: {}", e),
349                            },
350                        )?,
351                        start_value_included: *start_value_included,
352                    })
353                })
354                .transpose()?,
355        }
356        .into())
357    }
358}
359
360impl TryFromRequest<GetVotePollsByEndDateRequest> for VotePollsByEndDateDriveQuery {
361    fn try_from_request(value: GetVotePollsByEndDateRequest) -> Result<Self, Error> {
362        let result = match value.version.ok_or(Error::EmptyVersion)? {
363            get_vote_polls_by_end_date_request::Version::V0(v) => VotePollsByEndDateDriveQuery {
364                start_time: v
365                    .start_time_info
366                    .map(|v| (v.start_time_ms, v.start_time_included)),
367                end_time: v
368                    .end_time_info
369                    .map(|v| (v.end_time_ms, v.end_time_included)),
370                limit: v.limit.map(|v| v as u16),
371                offset: v.offset.map(|v| v as u16),
372                order_ascending: v.ascending,
373            },
374        };
375        Ok(result)
376    }
377
378    fn try_to_request(&self) -> Result<GetVotePollsByEndDateRequest, Error> {
379        use proto::get_vote_polls_by_end_date_request::get_vote_polls_by_end_date_request_v0 as request_v0;
380        if self.offset.is_some() {
381            return Err(Error::RequestError {
382                error:
383                    "VotePollsByEndDateDriveQuery.offset field is internal and must be set to None"
384                        .into(),
385            });
386        }
387
388        Ok(
389            proto::get_vote_polls_by_end_date_request::GetVotePollsByEndDateRequestV0 {
390                prove: true,
391                start_time_info: self.start_time.map(|(start_time_ms, start_time_included)| {
392                    request_v0::StartAtTimeInfo {
393                        start_time_ms,
394                        start_time_included,
395                    }
396                }),
397                end_time_info: self.end_time.map(|(end_time_ms, end_time_included)| {
398                    request_v0::EndAtTimeInfo {
399                        end_time_ms,
400                        end_time_included,
401                    }
402                }),
403                limit: self.limit.map(|v| v as u32),
404                offset: self.offset.map(|v| v as u32),
405                ascending: self.order_ascending,
406            }
407            .into(),
408        )
409    }
410}
411
412impl TryFromRequest<GetPrefundedSpecializedBalanceRequest> for Identifier {
413    fn try_to_request(&self) -> Result<GetPrefundedSpecializedBalanceRequest, Error> {
414        Ok(
415            proto::get_prefunded_specialized_balance_request::GetPrefundedSpecializedBalanceRequestV0 {
416                prove:true,
417                id: self.to_vec(),
418            }.into()
419        )
420    }
421
422    fn try_from_request(
423        grpc_request: GetPrefundedSpecializedBalanceRequest,
424    ) -> Result<Self, Error> {
425        match grpc_request.version.ok_or(Error::EmptyVersion)? {
426            proto::get_prefunded_specialized_balance_request::Version::V0(v) => {
427                Identifier::from_bytes(&v.id).map_err(|e| Error::RequestError {
428                    error: format!("cannot decode id: {}", e),
429                })
430            }
431        }
432    }
433}
434
435/// Convert a sequence of byte vectors into a sequence of [values](platform_value::Value).
436///
437/// Small utility function to decode a sequence of byte vectors into a sequence of [values](platform_value::Value).
438fn bincode_decode_values<V: AsRef<[u8]>, T: IntoIterator<Item = V>>(
439    values: T,
440) -> Result<Vec<Value>, Error> {
441    values
442        .into_iter()
443        .map(|v| {
444            dpp::bincode::decode_from_slice(v.as_ref(), BINCODE_CONFIG)
445                .map_err(|e| Error::RequestError {
446                    error: format!("cannot decode value: {}", e),
447                })
448                .map(|(v, _)| v)
449        })
450        .collect()
451}
452
453/// Convert a sequence of [values](platform_value::Value) into a sequence of byte vectors.
454///
455/// Small utility function to encode a sequence of [values](platform_value::Value) into a sequence of byte vectors.
456fn bincode_encode_values<'a, T: IntoIterator<Item = &'a Value>>(
457    values: T,
458) -> Result<Vec<Vec<u8>>, Error> {
459    values
460        .into_iter()
461        .map(|v| {
462            dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e| Error::RequestError {
463                error: format!("cannot encode value: {}", e),
464            })
465        })
466        .collect::<Result<Vec<_>, _>>()
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use dpp::identifier::Identifier;
473    use dpp::platform_value::Value;
474
475    // ---------------------------------------------------------------
476    // Helper: to_bytes32
477    // ---------------------------------------------------------------
478
479    #[test]
480    fn test_to_bytes32_valid() {
481        let input = [0xABu8; 32];
482        let result = to_bytes32(&input).expect("should convert 32-byte slice");
483        assert_eq!(result, input);
484    }
485
486    #[test]
487    fn test_to_bytes32_invalid_length() {
488        // Too short
489        let short = [0u8; 16];
490        assert!(to_bytes32(&short).is_err());
491
492        // Too long
493        let long = [0u8; 33];
494        assert!(to_bytes32(&long).is_err());
495
496        // Empty
497        assert!(to_bytes32(&[]).is_err());
498    }
499
500    // ---------------------------------------------------------------
501    // Helper: bincode encode/decode roundtrip
502    // ---------------------------------------------------------------
503
504    #[test]
505    fn test_bincode_encode_decode_roundtrip() {
506        let values = vec![
507            Value::Text("hello".to_string()),
508            Value::U64(42),
509            Value::Bool(true),
510        ];
511        let encoded = bincode_encode_values(&values).expect("encoding should succeed");
512        assert_eq!(encoded.len(), 3);
513
514        let decoded = bincode_decode_values(encoded.iter()).expect("decoding should succeed");
515        assert_eq!(decoded, values);
516    }
517
518    #[test]
519    fn test_bincode_decode_empty() {
520        let empty: Vec<Vec<u8>> = vec![];
521        let result = bincode_decode_values(empty.iter()).expect("empty input should succeed");
522        assert!(result.is_empty());
523    }
524
525    #[test]
526    fn test_bincode_decode_invalid() {
527        let garbage = vec![vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB]];
528        let result = bincode_decode_values(garbage.iter());
529        assert!(
530            result.is_err(),
531            "invalid bincode bytes should produce an error"
532        );
533    }
534
535    // ---------------------------------------------------------------
536    // TryFromRequest roundtrip: ContestedDocumentVotePollDriveQueryResultType
537    // ---------------------------------------------------------------
538
539    #[test]
540    fn test_contested_document_vote_poll_result_type_roundtrip() {
541        use get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
542
543        let cases = vec![
544            (
545                GrpcResultType::Documents,
546                ContestedDocumentVotePollDriveQueryResultType::Documents,
547            ),
548            (
549                GrpcResultType::VoteTally,
550                ContestedDocumentVotePollDriveQueryResultType::VoteTally,
551            ),
552            (
553                GrpcResultType::DocumentsAndVoteTally,
554                ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally,
555            ),
556        ];
557
558        for (grpc_val, expected_drive) in cases {
559            // grpc -> drive
560            let drive_val =
561                ContestedDocumentVotePollDriveQueryResultType::try_from_request(grpc_val)
562                    .expect("try_from_request should succeed");
563            assert_eq!(drive_val, expected_drive);
564
565            // drive -> grpc
566            let back = drive_val
567                .try_to_request()
568                .expect("try_to_request should succeed");
569            assert_eq!(back, grpc_val);
570        }
571    }
572
573    // ---------------------------------------------------------------
574    // TryFromRequest roundtrip: ContestedDocumentVotePollDriveQuery
575    // ---------------------------------------------------------------
576
577    #[test]
578    fn test_contested_document_vote_poll_query_roundtrip() {
579        let contract_id = Identifier::from_bytes(&[1u8; 32]).unwrap();
580        let index_values = vec![Value::Text("dash".to_string())];
581
582        let query = ContestedDocumentVotePollDriveQuery {
583            vote_poll: ContestedDocumentResourceVotePoll {
584                contract_id,
585                document_type_name: "domain".to_string(),
586                index_name: "parentNameAndLabel".to_string(),
587                index_values: index_values.clone(),
588            },
589            result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally,
590            offset: None,
591            limit: Some(10),
592            start_at: None,
593            allow_include_locked_and_abstaining_vote_tally: true,
594        };
595
596        let grpc_request = query
597            .try_to_request()
598            .expect("try_to_request should succeed");
599
600        let roundtripped = ContestedDocumentVotePollDriveQuery::try_from_request(grpc_request)
601            .expect("try_from_request should succeed");
602
603        assert_eq!(
604            roundtripped.vote_poll.contract_id,
605            query.vote_poll.contract_id
606        );
607        assert_eq!(
608            roundtripped.vote_poll.document_type_name,
609            query.vote_poll.document_type_name
610        );
611        assert_eq!(
612            roundtripped.vote_poll.index_name,
613            query.vote_poll.index_name
614        );
615        assert_eq!(
616            roundtripped.vote_poll.index_values,
617            query.vote_poll.index_values
618        );
619        assert_eq!(roundtripped.result_type, query.result_type);
620        assert_eq!(roundtripped.limit, query.limit);
621        assert_eq!(roundtripped.start_at, query.start_at);
622        assert_eq!(
623            roundtripped.allow_include_locked_and_abstaining_vote_tally,
624            query.allow_include_locked_and_abstaining_vote_tally
625        );
626    }
627
628    // ---------------------------------------------------------------
629    // TryFromRequest roundtrip: Identifier <-> GetPrefundedSpecializedBalanceRequest
630    // ---------------------------------------------------------------
631
632    #[test]
633    fn test_identifier_prefunded_balance_roundtrip() {
634        let id = Identifier::from_bytes(&[7u8; 32]).unwrap();
635
636        let grpc_request: GetPrefundedSpecializedBalanceRequest =
637            id.try_to_request().expect("try_to_request should succeed");
638
639        let roundtripped =
640            Identifier::try_from_request(grpc_request).expect("try_from_request should succeed");
641
642        assert_eq!(roundtripped, id);
643    }
644
645    // ---------------------------------------------------------------
646    // Error path: SingleDocumentByContender is rejected in try_to_request
647    // ---------------------------------------------------------------
648
649    #[test]
650    fn test_contested_result_type_rejects_single_document_by_contender() {
651        let contender_id = Identifier::from_bytes(&[0xCC; 32]).unwrap();
652        let result_type =
653            ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(contender_id);
654
655        let result = result_type.try_to_request();
656        assert!(
657            result.is_err(),
658            "SingleDocumentByContender should not be convertible to a gRPC request"
659        );
660
661        let err_msg = format!("{}", result.unwrap_err());
662        assert!(
663            err_msg.contains("single document by contender"),
664            "error message should mention 'single document by contender', got: {}",
665            err_msg
666        );
667    }
668
669    // ---------------------------------------------------------------
670    // Error path: VotePollsByEndDateDriveQuery rejects offset in try_to_request
671    // ---------------------------------------------------------------
672
673    #[test]
674    fn test_vote_polls_by_end_date_rejects_offset() {
675        let query = VotePollsByEndDateDriveQuery {
676            start_time: Some((1000, true)),
677            end_time: Some((2000, false)),
678            limit: Some(5),
679            offset: Some(10), // This should cause an error
680            order_ascending: true,
681        };
682
683        let result = query.try_to_request();
684        assert!(
685            result.is_err(),
686            "offset must be None for try_to_request to succeed"
687        );
688
689        let err_msg = format!("{}", result.unwrap_err());
690        assert!(
691            err_msg.contains("offset"),
692            "error message should mention 'offset', got: {}",
693            err_msg
694        );
695    }
696}