Skip to main content

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        let offset =
192            value
193                .offset
194                .map(u16::try_from)
195                .transpose()
196                .map_err(|_| Error::RequestError {
197                    error: "offset out of bounds".to_string(),
198                })?;
199        let limit =
200            value
201                .limit
202                .map(u16::try_from)
203                .transpose()
204                .map_err(|_| Error::RequestError {
205                    error: "limit out of bounds".to_string(),
206                })?;
207
208        Ok(Self {
209            identity_id: Identifier::from_vec(value.identity_id.to_vec()).map_err(|e| {
210                Error::RequestError {
211                    error: e.to_string(),
212                }
213            })?,
214            offset,
215            limit,
216            start_at,
217            order_ascending: value.order_ascending,
218        })
219    }
220
221    fn try_to_request(&self) -> Result<GetContestedResourceIdentityVotesRequest, Error> {
222        use proto::get_contested_resource_identity_votes_request::get_contested_resource_identity_votes_request_v0 as request_v0;
223
224        Ok(proto::get_contested_resource_identity_votes_request::GetContestedResourceIdentityVotesRequestV0 {
225                    prove: true,
226                    identity_id: self.identity_id.to_vec(),
227                    offset: self.offset.map(|x| x as u32),
228                    limit: self.limit.map(|x| x as u32),
229                    start_at_vote_poll_id_info: self.start_at.map(|(id, included)| {
230                        request_v0::StartAtVotePollIdInfo {
231                            start_at_poll_identifier: id.to_vec(),
232                            start_poll_identifier_included: included,
233                        }
234                    }),
235                    order_ascending: self.order_ascending,
236                }.into()
237            )
238    }
239}
240
241use dapi_grpc::platform::v0::get_contested_resource_voters_for_identity_request;
242
243impl TryFromRequest<GetContestedResourceVotersForIdentityRequest>
244    for ContestedDocumentVotePollVotesDriveQuery
245{
246    fn try_from_request(
247        value: GetContestedResourceVotersForIdentityRequest,
248    ) -> Result<Self, Error> {
249        let result = match value.version.ok_or(Error::EmptyVersion)? {
250            get_contested_resource_voters_for_identity_request::Version::V0(v) => {
251                ContestedDocumentVotePollVotesDriveQuery {
252                    vote_poll: ContestedDocumentResourceVotePoll {
253                        contract_id: Identifier::from_bytes(&v.contract_id).map_err(|e| {
254                            Error::RequestError {
255                                error: format!("cannot decode contract id: {}", e),
256                            }
257                        })?,
258                        document_type_name: v.document_type_name.clone(),
259                        index_name: v.index_name.clone(),
260                        index_values: bincode_decode_values(v.index_values.iter())?,
261                    },
262                    contestant_id: Identifier::from_bytes(&v.contestant_id).map_err(|e| {
263                        Error::RequestError {
264                            error: format!("cannot decode contestant_id: {}", e),
265                        }
266                    })?,
267                    limit: v.count.map(|v| v as u16),
268                    offset: None,
269                    start_at: v
270                        .start_at_identifier_info
271                        .map(|v| {
272                            to_bytes32(&v.start_identifier)
273                                .map(|id| (id, v.start_identifier_included))
274                        })
275                        .transpose()
276                        .map_err(|e| Error::RequestError {
277                            error: format!("cannot decode start_at value: {}", e),
278                        })?,
279                    order_ascending: v.order_ascending,
280                }
281            }
282        };
283
284        Ok(result)
285    }
286    fn try_to_request(&self) -> Result<GetContestedResourceVotersForIdentityRequest, Error> {
287        use proto::get_contested_resource_voters_for_identity_request::get_contested_resource_voters_for_identity_request_v0 as request_v0;
288        if self.offset.is_some() {
289            return Err(Error::RequestError{error:"ContestedDocumentVotePollVotesDriveQuery.offset field is internal and must be set to None".into()});
290        }
291
292        Ok(proto::get_contested_resource_voters_for_identity_request::GetContestedResourceVotersForIdentityRequestV0 {
293            prove:true,
294            contract_id: self.vote_poll.contract_id.to_vec(),
295            document_type_name: self.vote_poll.document_type_name.clone(),
296            index_name: self.vote_poll.index_name.clone(),
297            index_values: self.vote_poll.index_values.iter().map(|v|
298                dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e|
299                    Error::RequestError { error: e.to_string()})).collect::<Result<Vec<_>,_>>()?,
300            order_ascending: self.order_ascending,
301            count: self.limit.map(|v| v as u32),
302            contestant_id: self.contestant_id.to_vec(),
303            start_at_identifier_info: self.start_at.map(|v| request_v0::StartAtIdentifierInfo{
304                start_identifier: v.0.to_vec(),
305                start_identifier_included: v.1,
306            }),
307        }
308        .into())
309    }
310}
311
312impl TryFromRequest<GetContestedResourcesRequest> for VotePollsByDocumentTypeQuery {
313    fn try_from_request(value: GetContestedResourcesRequest) -> Result<Self, Error> {
314        let result = match value.version.ok_or(Error::EmptyVersion)? {
315            get_contested_resources_request::Version::V0(req) => VotePollsByDocumentTypeQuery {
316                contract_id: Identifier::from_bytes(&req.contract_id).map_err(|e| {
317                    Error::RequestError {
318                        error: format!("cannot decode contract id: {}", e),
319                    }
320                })?,
321                document_type_name: req.document_type_name.clone(),
322                index_name: req.index_name.clone(),
323                start_at_value: req
324                    .start_at_value_info
325                    .map(|i| {
326                        let (value, _): (Value, _) =
327                            bincode::decode_from_slice(&i.start_value, BINCODE_CONFIG).map_err(
328                                |e| Error::RequestError {
329                                    error: format!("cannot decode start value: {}", e),
330                                },
331                            )?;
332                        Ok::<_, Error>((value, i.start_value_included))
333                    })
334                    .transpose()?,
335                start_index_values: bincode_decode_values(req.start_index_values.iter())?,
336                end_index_values: bincode_decode_values(req.end_index_values.iter())?,
337                limit: req.count.map(|v| v as u16),
338                order_ascending: req.order_ascending,
339            },
340        };
341        Ok(result)
342    }
343
344    fn try_to_request(&self) -> Result<GetContestedResourcesRequest, Error> {
345        Ok(GetContestedResourcesRequestV0 {
346            prove: true,
347            contract_id: self.contract_id.to_vec(),
348            count: self.limit.map(|v| v as u32),
349            document_type_name: self.document_type_name.clone(),
350            end_index_values: bincode_encode_values(&self.end_index_values)?,
351            start_index_values: bincode_encode_values(&self.start_index_values)?,
352            index_name: self.index_name.clone(),
353            order_ascending: self.order_ascending,
354            start_at_value_info: self
355                .start_at_value
356                .as_ref()
357                .map(|(start_value, start_value_included)| {
358                    Ok::<_, Error>(get_contested_resources_request_v0::StartAtValueInfo {
359                        start_value: bincode::encode_to_vec(start_value, BINCODE_CONFIG).map_err(
360                            |e| Error::RequestError {
361                                error: format!("cannot encode start value: {}", e),
362                            },
363                        )?,
364                        start_value_included: *start_value_included,
365                    })
366                })
367                .transpose()?,
368        }
369        .into())
370    }
371}
372
373impl TryFromRequest<GetVotePollsByEndDateRequest> for VotePollsByEndDateDriveQuery {
374    fn try_from_request(value: GetVotePollsByEndDateRequest) -> Result<Self, Error> {
375        let result = match value.version.ok_or(Error::EmptyVersion)? {
376            get_vote_polls_by_end_date_request::Version::V0(v) => VotePollsByEndDateDriveQuery {
377                start_time: v
378                    .start_time_info
379                    .map(|v| (v.start_time_ms, v.start_time_included)),
380                end_time: v
381                    .end_time_info
382                    .map(|v| (v.end_time_ms, v.end_time_included)),
383                limit: v.limit.map(|v| v as u16),
384                offset: v.offset.map(|v| v as u16),
385                order_ascending: v.ascending,
386            },
387        };
388        Ok(result)
389    }
390
391    fn try_to_request(&self) -> Result<GetVotePollsByEndDateRequest, Error> {
392        use proto::get_vote_polls_by_end_date_request::get_vote_polls_by_end_date_request_v0 as request_v0;
393        if self.offset.is_some() {
394            return Err(Error::RequestError {
395                error:
396                    "VotePollsByEndDateDriveQuery.offset field is internal and must be set to None"
397                        .into(),
398            });
399        }
400
401        Ok(
402            proto::get_vote_polls_by_end_date_request::GetVotePollsByEndDateRequestV0 {
403                prove: true,
404                start_time_info: self.start_time.map(|(start_time_ms, start_time_included)| {
405                    request_v0::StartAtTimeInfo {
406                        start_time_ms,
407                        start_time_included,
408                    }
409                }),
410                end_time_info: self.end_time.map(|(end_time_ms, end_time_included)| {
411                    request_v0::EndAtTimeInfo {
412                        end_time_ms,
413                        end_time_included,
414                    }
415                }),
416                limit: self.limit.map(|v| v as u32),
417                offset: self.offset.map(|v| v as u32),
418                ascending: self.order_ascending,
419            }
420            .into(),
421        )
422    }
423}
424
425impl TryFromRequest<GetPrefundedSpecializedBalanceRequest> for Identifier {
426    fn try_to_request(&self) -> Result<GetPrefundedSpecializedBalanceRequest, Error> {
427        Ok(
428            proto::get_prefunded_specialized_balance_request::GetPrefundedSpecializedBalanceRequestV0 {
429                prove:true,
430                id: self.to_vec(),
431            }.into()
432        )
433    }
434
435    fn try_from_request(
436        grpc_request: GetPrefundedSpecializedBalanceRequest,
437    ) -> Result<Self, Error> {
438        match grpc_request.version.ok_or(Error::EmptyVersion)? {
439            proto::get_prefunded_specialized_balance_request::Version::V0(v) => {
440                Identifier::from_bytes(&v.id).map_err(|e| Error::RequestError {
441                    error: format!("cannot decode id: {}", e),
442                })
443            }
444        }
445    }
446}
447
448/// Convert a sequence of byte vectors into a sequence of [values](platform_value::Value).
449///
450/// Small utility function to decode a sequence of byte vectors into a sequence of [values](platform_value::Value).
451fn bincode_decode_values<V: AsRef<[u8]>, T: IntoIterator<Item = V>>(
452    values: T,
453) -> Result<Vec<Value>, Error> {
454    values
455        .into_iter()
456        .map(|v| {
457            dpp::bincode::decode_from_slice(v.as_ref(), BINCODE_CONFIG)
458                .map_err(|e| Error::RequestError {
459                    error: format!("cannot decode value: {}", e),
460                })
461                .map(|(v, _)| v)
462        })
463        .collect()
464}
465
466/// Convert a sequence of [values](platform_value::Value) into a sequence of byte vectors.
467///
468/// Small utility function to encode a sequence of [values](platform_value::Value) into a sequence of byte vectors.
469fn bincode_encode_values<'a, T: IntoIterator<Item = &'a Value>>(
470    values: T,
471) -> Result<Vec<Vec<u8>>, Error> {
472    values
473        .into_iter()
474        .map(|v| {
475            dpp::bincode::encode_to_vec(v, BINCODE_CONFIG).map_err(|e| Error::RequestError {
476                error: format!("cannot encode value: {}", e),
477            })
478        })
479        .collect::<Result<Vec<_>, _>>()
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use dpp::identifier::Identifier;
486    use dpp::platform_value::Value;
487
488    // ---------------------------------------------------------------
489    // Helper: to_bytes32
490    // ---------------------------------------------------------------
491
492    #[test]
493    fn test_to_bytes32_valid() {
494        let input = [0xABu8; 32];
495        let result = to_bytes32(&input).expect("should convert 32-byte slice");
496        assert_eq!(result, input);
497    }
498
499    #[test]
500    fn test_to_bytes32_invalid_length() {
501        // Too short
502        let short = [0u8; 16];
503        assert!(to_bytes32(&short).is_err());
504
505        // Too long
506        let long = [0u8; 33];
507        assert!(to_bytes32(&long).is_err());
508
509        // Empty
510        assert!(to_bytes32(&[]).is_err());
511    }
512
513    // ---------------------------------------------------------------
514    // Helper: bincode encode/decode roundtrip
515    // ---------------------------------------------------------------
516
517    #[test]
518    fn test_bincode_encode_decode_roundtrip() {
519        let values = vec![
520            Value::Text("hello".to_string()),
521            Value::U64(42),
522            Value::Bool(true),
523        ];
524        let encoded = bincode_encode_values(&values).expect("encoding should succeed");
525        assert_eq!(encoded.len(), 3);
526
527        let decoded = bincode_decode_values(encoded.iter()).expect("decoding should succeed");
528        assert_eq!(decoded, values);
529    }
530
531    #[test]
532    fn test_bincode_decode_empty() {
533        let empty: Vec<Vec<u8>> = vec![];
534        let result = bincode_decode_values(empty.iter()).expect("empty input should succeed");
535        assert!(result.is_empty());
536    }
537
538    #[test]
539    fn test_bincode_decode_invalid() {
540        let garbage = [vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB]];
541        let result = bincode_decode_values(garbage.iter());
542        assert!(
543            result.is_err(),
544            "invalid bincode bytes should produce an error"
545        );
546    }
547
548    // ---------------------------------------------------------------
549    // TryFromRequest roundtrip: ContestedDocumentVotePollDriveQueryResultType
550    // ---------------------------------------------------------------
551
552    #[test]
553    fn test_contested_document_vote_poll_result_type_roundtrip() {
554        use get_contested_resource_vote_state_request_v0::ResultType as GrpcResultType;
555
556        let cases = vec![
557            (
558                GrpcResultType::Documents,
559                ContestedDocumentVotePollDriveQueryResultType::Documents,
560            ),
561            (
562                GrpcResultType::VoteTally,
563                ContestedDocumentVotePollDriveQueryResultType::VoteTally,
564            ),
565            (
566                GrpcResultType::DocumentsAndVoteTally,
567                ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally,
568            ),
569        ];
570
571        for (grpc_val, expected_drive) in cases {
572            // grpc -> drive
573            let drive_val =
574                ContestedDocumentVotePollDriveQueryResultType::try_from_request(grpc_val)
575                    .expect("try_from_request should succeed");
576            assert_eq!(drive_val, expected_drive);
577
578            // drive -> grpc
579            let back = drive_val
580                .try_to_request()
581                .expect("try_to_request should succeed");
582            assert_eq!(back, grpc_val);
583        }
584    }
585
586    // ---------------------------------------------------------------
587    // TryFromRequest roundtrip: ContestedDocumentVotePollDriveQuery
588    // ---------------------------------------------------------------
589
590    #[test]
591    fn test_contested_document_vote_poll_query_roundtrip() {
592        let contract_id = Identifier::from_bytes(&[1u8; 32]).unwrap();
593        let index_values = vec![Value::Text("dash".to_string())];
594
595        let query = ContestedDocumentVotePollDriveQuery {
596            vote_poll: ContestedDocumentResourceVotePoll {
597                contract_id,
598                document_type_name: "domain".to_string(),
599                index_name: "parentNameAndLabel".to_string(),
600                index_values: index_values.clone(),
601            },
602            result_type: ContestedDocumentVotePollDriveQueryResultType::DocumentsAndVoteTally,
603            offset: None,
604            limit: Some(10),
605            start_at: None,
606            allow_include_locked_and_abstaining_vote_tally: true,
607        };
608
609        let grpc_request = query
610            .try_to_request()
611            .expect("try_to_request should succeed");
612
613        let roundtripped = ContestedDocumentVotePollDriveQuery::try_from_request(grpc_request)
614            .expect("try_from_request should succeed");
615
616        assert_eq!(
617            roundtripped.vote_poll.contract_id,
618            query.vote_poll.contract_id
619        );
620        assert_eq!(
621            roundtripped.vote_poll.document_type_name,
622            query.vote_poll.document_type_name
623        );
624        assert_eq!(
625            roundtripped.vote_poll.index_name,
626            query.vote_poll.index_name
627        );
628        assert_eq!(
629            roundtripped.vote_poll.index_values,
630            query.vote_poll.index_values
631        );
632        assert_eq!(roundtripped.result_type, query.result_type);
633        assert_eq!(roundtripped.limit, query.limit);
634        assert_eq!(roundtripped.start_at, query.start_at);
635        assert_eq!(
636            roundtripped.allow_include_locked_and_abstaining_vote_tally,
637            query.allow_include_locked_and_abstaining_vote_tally
638        );
639    }
640
641    // ---------------------------------------------------------------
642    // TryFromRequest roundtrip: Identifier <-> GetPrefundedSpecializedBalanceRequest
643    // ---------------------------------------------------------------
644
645    #[test]
646    fn test_identifier_prefunded_balance_roundtrip() {
647        let id = Identifier::from_bytes(&[7u8; 32]).unwrap();
648
649        let grpc_request: GetPrefundedSpecializedBalanceRequest =
650            id.try_to_request().expect("try_to_request should succeed");
651
652        let roundtripped =
653            Identifier::try_from_request(grpc_request).expect("try_from_request should succeed");
654
655        assert_eq!(roundtripped, id);
656    }
657
658    // ---------------------------------------------------------------
659    // Error path: SingleDocumentByContender is rejected in try_to_request
660    // ---------------------------------------------------------------
661
662    #[test]
663    fn test_contested_result_type_rejects_single_document_by_contender() {
664        let contender_id = Identifier::from_bytes(&[0xCC; 32]).unwrap();
665        let result_type =
666            ContestedDocumentVotePollDriveQueryResultType::SingleDocumentByContender(contender_id);
667
668        let result = result_type.try_to_request();
669        assert!(
670            result.is_err(),
671            "SingleDocumentByContender should not be convertible to a gRPC request"
672        );
673
674        let err_msg = format!("{}", result.unwrap_err());
675        assert!(
676            err_msg.contains("single document by contender"),
677            "error message should mention 'single document by contender', got: {}",
678            err_msg
679        );
680    }
681
682    // ---------------------------------------------------------------
683    // Error path: VotePollsByEndDateDriveQuery rejects offset in try_to_request
684    // ---------------------------------------------------------------
685
686    // ---------------------------------------------------------------
687    // Error path: ContestedDocumentVotePollDriveQuery try_to_request
688    // rejects offset != None
689    // ---------------------------------------------------------------
690
691    #[test]
692    fn test_contested_document_vote_poll_query_rejects_offset() {
693        let contract_id = Identifier::from_bytes(&[2u8; 32]).unwrap();
694        let query = ContestedDocumentVotePollDriveQuery {
695            vote_poll: ContestedDocumentResourceVotePoll {
696                contract_id,
697                document_type_name: "d".to_string(),
698                index_name: "idx".to_string(),
699                index_values: vec![],
700            },
701            result_type: ContestedDocumentVotePollDriveQueryResultType::Documents,
702            offset: Some(5), // should trigger rejection
703            limit: None,
704            start_at: None,
705            allow_include_locked_and_abstaining_vote_tally: false,
706        };
707
708        let err = query.try_to_request().unwrap_err();
709        let err_msg = format!("{}", err);
710        assert!(
711            err_msg.contains("offset"),
712            "error should mention offset, got: {err_msg}"
713        );
714    }
715
716    // ---------------------------------------------------------------
717    // ContestedResourceVotesGivenByIdentityQuery preserves proof-critical
718    // pagination fields in both conversion directions.
719    // ---------------------------------------------------------------
720
721    #[test]
722    fn test_contested_resource_votes_given_by_identity_preserves_offset() {
723        let id = Identifier::from_bytes(&[3u8; 32]).unwrap();
724        let query = ContestedResourceVotesGivenByIdentityQuery {
725            identity_id: id,
726            offset: Some(10),
727            limit: Some(20),
728            start_at: None,
729            order_ascending: true,
730        };
731        let request = query.try_to_request().expect("request conversion");
732        let converted = ContestedResourceVotesGivenByIdentityQuery::try_from_request(request)
733            .expect("query conversion");
734        assert_eq!(converted, query);
735    }
736
737    #[test]
738    fn test_contested_resource_votes_given_by_identity_rejects_wide_pagination_values() {
739        use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::{
740            GetContestedResourceIdentityVotesRequestV0, Version as ReqVersion,
741        };
742
743        for (limit, offset, expected_field) in [
744            (Some(u16::MAX as u32 + 1), None, "limit"),
745            (None, Some(u16::MAX as u32 + 1), "offset"),
746        ] {
747            let request = GetContestedResourceIdentityVotesRequest {
748                version: Some(ReqVersion::V0(GetContestedResourceIdentityVotesRequestV0 {
749                    identity_id: vec![0u8; 32],
750                    start_at_vote_poll_id_info: None,
751                    limit,
752                    offset,
753                    order_ascending: true,
754                    prove: true,
755                })),
756            };
757            let err =
758                ContestedResourceVotesGivenByIdentityQuery::try_from_request(request).unwrap_err();
759            assert!(
760                format!("{err}").contains(expected_field),
761                "unexpected error: {err}"
762            );
763        }
764    }
765
766    #[test]
767    fn test_contested_resource_votes_given_by_identity_accepts_u16_pagination_boundaries() {
768        use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::{
769            GetContestedResourceIdentityVotesRequestV0, Version as ReqVersion,
770        };
771
772        for value in [0, 1, u16::MAX as u32] {
773            let request = GetContestedResourceIdentityVotesRequest {
774                version: Some(ReqVersion::V0(GetContestedResourceIdentityVotesRequestV0 {
775                    identity_id: vec![0u8; 32],
776                    start_at_vote_poll_id_info: None,
777                    limit: Some(value),
778                    offset: Some(value),
779                    order_ascending: true,
780                    prove: true,
781                })),
782            };
783            let query = ContestedResourceVotesGivenByIdentityQuery::try_from_request(request)
784                .expect("pagination value should fit");
785            assert_eq!(query.limit, Some(value as u16));
786            assert_eq!(query.offset, Some(value as u16));
787        }
788    }
789
790    #[test]
791    fn test_contested_resource_votes_given_by_identity_from_request_bad_identity() {
792        // identity_id must be exactly 32 bytes; 10 bytes must fail.
793        use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::{
794            GetContestedResourceIdentityVotesRequestV0, Version as ReqVersion,
795        };
796        let request = GetContestedResourceIdentityVotesRequest {
797            version: Some(ReqVersion::V0(GetContestedResourceIdentityVotesRequestV0 {
798                identity_id: vec![0u8; 10],
799                start_at_vote_poll_id_info: None,
800                limit: None,
801                offset: None,
802                order_ascending: true,
803                prove: true,
804            })),
805        };
806        let err =
807            ContestedResourceVotesGivenByIdentityQuery::try_from_request(request).unwrap_err();
808        assert!(matches!(err, Error::RequestError { .. }), "got: {err:?}");
809    }
810
811    #[test]
812    fn test_contested_resource_votes_given_by_identity_from_request_bad_start_at() {
813        // start_at_poll_identifier must be 32 bytes.
814        use dapi_grpc::platform::v0::get_contested_resource_identity_votes_request::{
815            get_contested_resource_identity_votes_request_v0::StartAtVotePollIdInfo,
816            GetContestedResourceIdentityVotesRequestV0, Version as ReqVersion,
817        };
818        let request = GetContestedResourceIdentityVotesRequest {
819            version: Some(ReqVersion::V0(GetContestedResourceIdentityVotesRequestV0 {
820                identity_id: vec![0u8; 32],
821                start_at_vote_poll_id_info: Some(StartAtVotePollIdInfo {
822                    start_at_poll_identifier: vec![1u8; 9], // bad length
823                    start_poll_identifier_included: true,
824                }),
825                limit: None,
826                offset: None,
827                order_ascending: true,
828                prove: true,
829            })),
830        };
831        let err =
832            ContestedResourceVotesGivenByIdentityQuery::try_from_request(request).unwrap_err();
833        assert!(matches!(err, Error::RequestError { .. }), "got: {err:?}");
834    }
835
836    #[test]
837    fn test_contested_resource_votes_given_by_identity_missing_version() {
838        let request = GetContestedResourceIdentityVotesRequest { version: None };
839        let err =
840            ContestedResourceVotesGivenByIdentityQuery::try_from_request(request).unwrap_err();
841        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
842    }
843
844    // ---------------------------------------------------------------
845    // ContestedDocumentVotePollVotesDriveQuery tests
846    // ---------------------------------------------------------------
847
848    #[test]
849    fn test_contested_document_vote_poll_votes_missing_version() {
850        let request = GetContestedResourceVotersForIdentityRequest { version: None };
851        let err = ContestedDocumentVotePollVotesDriveQuery::try_from_request(request).unwrap_err();
852        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
853    }
854
855    #[test]
856    fn test_contested_document_vote_poll_votes_from_request_bad_contract_id() {
857        use dapi_grpc::platform::v0::get_contested_resource_voters_for_identity_request::{
858            GetContestedResourceVotersForIdentityRequestV0, Version as ReqVersion,
859        };
860        let request = GetContestedResourceVotersForIdentityRequest {
861            version: Some(ReqVersion::V0(
862                GetContestedResourceVotersForIdentityRequestV0 {
863                    contract_id: vec![0u8; 7], // bad
864                    document_type_name: "d".to_string(),
865                    index_name: "i".to_string(),
866                    index_values: vec![],
867                    contestant_id: vec![0u8; 32],
868                    start_at_identifier_info: None,
869                    order_ascending: true,
870                    count: None,
871                    prove: true,
872                },
873            )),
874        };
875        let err = ContestedDocumentVotePollVotesDriveQuery::try_from_request(request).unwrap_err();
876        match err {
877            Error::RequestError { error } => assert!(error.contains("contract id"), "got: {error}"),
878            other => panic!("expected RequestError, got: {other:?}"),
879        }
880    }
881
882    #[test]
883    fn test_contested_document_vote_poll_votes_from_request_bad_contestant_id() {
884        use dapi_grpc::platform::v0::get_contested_resource_voters_for_identity_request::{
885            GetContestedResourceVotersForIdentityRequestV0, Version as ReqVersion,
886        };
887        let request = GetContestedResourceVotersForIdentityRequest {
888            version: Some(ReqVersion::V0(
889                GetContestedResourceVotersForIdentityRequestV0 {
890                    contract_id: vec![0u8; 32],
891                    document_type_name: "d".to_string(),
892                    index_name: "i".to_string(),
893                    index_values: vec![],
894                    contestant_id: vec![0u8; 5], // bad
895                    start_at_identifier_info: None,
896                    order_ascending: true,
897                    count: None,
898                    prove: true,
899                },
900            )),
901        };
902        let err = ContestedDocumentVotePollVotesDriveQuery::try_from_request(request).unwrap_err();
903        match err {
904            Error::RequestError { error } => {
905                assert!(error.contains("contestant_id"), "got: {error}")
906            }
907            other => panic!("expected RequestError, got: {other:?}"),
908        }
909    }
910
911    #[test]
912    fn test_contested_document_vote_poll_votes_rejects_offset() {
913        let contract_id = Identifier::from_bytes(&[0u8; 32]).unwrap();
914        let contestant_id = Identifier::from_bytes(&[1u8; 32]).unwrap();
915        let q = ContestedDocumentVotePollVotesDriveQuery {
916            vote_poll: ContestedDocumentResourceVotePoll {
917                contract_id,
918                document_type_name: "d".to_string(),
919                index_name: "i".to_string(),
920                index_values: vec![],
921            },
922            contestant_id,
923            limit: None,
924            offset: Some(7),
925            start_at: None,
926            order_ascending: true,
927        };
928        let err = q.try_to_request().unwrap_err();
929        assert!(format!("{err}").contains("offset"));
930    }
931
932    // ---------------------------------------------------------------
933    // VotePollsByDocumentTypeQuery tests
934    // ---------------------------------------------------------------
935
936    #[test]
937    fn test_vote_polls_by_document_type_missing_version() {
938        let request = GetContestedResourcesRequest { version: None };
939        let err = VotePollsByDocumentTypeQuery::try_from_request(request).unwrap_err();
940        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
941    }
942
943    #[test]
944    fn test_vote_polls_by_document_type_from_request_bad_contract_id() {
945        let request = GetContestedResourcesRequest {
946            version: Some(get_contested_resources_request::Version::V0(
947                GetContestedResourcesRequestV0 {
948                    contract_id: vec![0u8; 6],
949                    document_type_name: "d".to_string(),
950                    index_name: "i".to_string(),
951                    start_at_value_info: None,
952                    start_index_values: vec![],
953                    end_index_values: vec![],
954                    count: None,
955                    order_ascending: true,
956                    prove: true,
957                },
958            )),
959        };
960        let err = VotePollsByDocumentTypeQuery::try_from_request(request).unwrap_err();
961        match err {
962            Error::RequestError { error } => assert!(error.contains("contract id"), "got: {error}"),
963            other => panic!("expected RequestError, got: {other:?}"),
964        }
965    }
966
967    #[test]
968    fn test_vote_polls_by_document_type_from_request_bad_start_value() {
969        let request = GetContestedResourcesRequest {
970            version: Some(get_contested_resources_request::Version::V0(
971                GetContestedResourcesRequestV0 {
972                    contract_id: vec![0u8; 32],
973                    document_type_name: "d".to_string(),
974                    index_name: "i".to_string(),
975                    start_at_value_info: Some(
976                        get_contested_resources_request_v0::StartAtValueInfo {
977                            start_value: vec![0xFFu8, 0xFE, 0xFD], // not valid bincode
978                            start_value_included: true,
979                        },
980                    ),
981                    start_index_values: vec![],
982                    end_index_values: vec![],
983                    count: None,
984                    order_ascending: true,
985                    prove: true,
986                },
987            )),
988        };
989        let err = VotePollsByDocumentTypeQuery::try_from_request(request).unwrap_err();
990        match err {
991            Error::RequestError { error } => {
992                assert!(error.contains("decode start value"), "got: {error}")
993            }
994            other => panic!("expected RequestError, got: {other:?}"),
995        }
996    }
997
998    #[test]
999    fn test_vote_polls_by_document_type_roundtrip_with_start_at_value() {
1000        let contract_id = Identifier::from_bytes(&[9u8; 32]).unwrap();
1001        let query = VotePollsByDocumentTypeQuery {
1002            contract_id,
1003            document_type_name: "domain".to_string(),
1004            index_name: "parent".to_string(),
1005            start_at_value: Some((Value::Text("dash".to_string()), true)),
1006            start_index_values: vec![Value::Text("a".to_string())],
1007            end_index_values: vec![Value::Text("z".to_string())],
1008            limit: Some(20),
1009            order_ascending: false,
1010        };
1011
1012        let grpc = query.try_to_request().expect("try_to_request succeeds");
1013        let back = VotePollsByDocumentTypeQuery::try_from_request(grpc)
1014            .expect("try_from_request succeeds");
1015
1016        assert_eq!(back.contract_id, query.contract_id);
1017        assert_eq!(back.document_type_name, query.document_type_name);
1018        assert_eq!(back.index_name, query.index_name);
1019        assert_eq!(back.start_at_value, query.start_at_value);
1020        assert_eq!(back.start_index_values, query.start_index_values);
1021        assert_eq!(back.end_index_values, query.end_index_values);
1022        assert_eq!(back.limit, query.limit);
1023        assert_eq!(back.order_ascending, query.order_ascending);
1024    }
1025
1026    // ---------------------------------------------------------------
1027    // VotePollsByEndDateDriveQuery happy-path roundtrip
1028    // ---------------------------------------------------------------
1029
1030    #[test]
1031    fn test_vote_polls_by_end_date_roundtrip() {
1032        let q = VotePollsByEndDateDriveQuery {
1033            start_time: Some((1, false)),
1034            end_time: Some((10_000, true)),
1035            limit: Some(10),
1036            offset: None,
1037            order_ascending: false,
1038        };
1039        let grpc = q.try_to_request().expect("try_to_request ok");
1040        let back =
1041            VotePollsByEndDateDriveQuery::try_from_request(grpc).expect("try_from_request ok");
1042        assert_eq!(back.start_time, q.start_time);
1043        assert_eq!(back.end_time, q.end_time);
1044        assert_eq!(back.limit, q.limit);
1045        assert_eq!(back.order_ascending, q.order_ascending);
1046    }
1047
1048    #[test]
1049    fn test_vote_polls_by_end_date_missing_version() {
1050        let request = GetVotePollsByEndDateRequest { version: None };
1051        let err = VotePollsByEndDateDriveQuery::try_from_request(request).unwrap_err();
1052        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
1053    }
1054
1055    // ---------------------------------------------------------------
1056    // Identifier / GetPrefundedSpecializedBalanceRequest error paths
1057    // ---------------------------------------------------------------
1058
1059    #[test]
1060    fn test_identifier_prefunded_balance_missing_version() {
1061        let request = GetPrefundedSpecializedBalanceRequest { version: None };
1062        let err = Identifier::try_from_request(request).unwrap_err();
1063        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
1064    }
1065
1066    #[test]
1067    fn test_identifier_prefunded_balance_bad_id_length() {
1068        let request = GetPrefundedSpecializedBalanceRequest {
1069            version: Some(
1070                proto::get_prefunded_specialized_balance_request::Version::V0(
1071                    proto::get_prefunded_specialized_balance_request::GetPrefundedSpecializedBalanceRequestV0 {
1072                        id: vec![0u8; 10], // bad
1073                        prove: true,
1074                    },
1075                ),
1076            ),
1077        };
1078        let err = Identifier::try_from_request(request).unwrap_err();
1079        match err {
1080            Error::RequestError { error } => assert!(error.contains("decode id"), "got: {error}"),
1081            other => panic!("expected RequestError, got: {other:?}"),
1082        }
1083    }
1084
1085    // ---------------------------------------------------------------
1086    // ContestedDocumentVotePollDriveQuery error paths
1087    // ---------------------------------------------------------------
1088
1089    #[test]
1090    fn test_contested_document_vote_poll_query_missing_version() {
1091        let request = GetContestedResourceVoteStateRequest { version: None };
1092        let err = ContestedDocumentVotePollDriveQuery::try_from_request(request).unwrap_err();
1093        assert!(matches!(err, Error::EmptyVersion), "got: {err:?}");
1094    }
1095
1096    #[test]
1097    fn test_contested_document_vote_poll_query_from_request_bad_contract_id() {
1098        let request = GetContestedResourceVoteStateRequest {
1099            version: Some(get_contested_resource_vote_state_request::Version::V0(
1100                proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 {
1101                    contract_id: vec![0u8; 9], // bad
1102                    document_type_name: "d".to_string(),
1103                    index_name: "i".to_string(),
1104                    index_values: vec![],
1105                    result_type: 0,
1106                    start_at_identifier_info: None,
1107                    allow_include_locked_and_abstaining_vote_tally: true,
1108                    count: None,
1109                    prove: true,
1110                },
1111            )),
1112        };
1113        let err = ContestedDocumentVotePollDriveQuery::try_from_request(request).unwrap_err();
1114        match err {
1115            Error::RequestError { error } => assert!(error.contains("contract id"), "got: {error}"),
1116            other => panic!("expected RequestError, got: {other:?}"),
1117        }
1118    }
1119
1120    #[test]
1121    fn test_contested_document_vote_poll_query_from_request_bad_start_at_identifier() {
1122        let request = GetContestedResourceVoteStateRequest {
1123            version: Some(get_contested_resource_vote_state_request::Version::V0(
1124                proto::get_contested_resource_vote_state_request::GetContestedResourceVoteStateRequestV0 {
1125                    contract_id: vec![0u8; 32],
1126                    document_type_name: "d".to_string(),
1127                    index_name: "i".to_string(),
1128                    index_values: vec![],
1129                    result_type: 0,
1130                    start_at_identifier_info: Some(
1131                        get_contested_resource_vote_state_request_v0::StartAtIdentifierInfo {
1132                            start_identifier: vec![0u8; 10], // bad
1133                            start_identifier_included: true,
1134                        },
1135                    ),
1136                    allow_include_locked_and_abstaining_vote_tally: true,
1137                    count: None,
1138                    prove: true,
1139                },
1140            )),
1141        };
1142        let err = ContestedDocumentVotePollDriveQuery::try_from_request(request).unwrap_err();
1143        match err {
1144            Error::RequestError { error } => assert!(error.contains("start_at"), "got: {error}"),
1145            other => panic!("expected RequestError, got: {other:?}"),
1146        }
1147    }
1148
1149    // ---------------------------------------------------------------
1150    // bincode_encode_values: error path
1151    // ---------------------------------------------------------------
1152
1153    #[test]
1154    fn test_bincode_decode_mixed_valid_and_invalid() {
1155        let mut encoded_valid = bincode_encode_values(&[Value::Text("x".to_string())]).unwrap();
1156        // Put a corrupted record after a valid one.
1157        encoded_valid.push(vec![0xFF, 0xFE, 0xFD]);
1158        let result = bincode_decode_values(encoded_valid.iter());
1159        assert!(result.is_err(), "mixed input must fail");
1160    }
1161
1162    // ---------------------------------------------------------------
1163    // Original test below (kept for completeness)
1164    // ---------------------------------------------------------------
1165
1166    #[test]
1167    fn test_vote_polls_by_end_date_rejects_offset() {
1168        let query = VotePollsByEndDateDriveQuery {
1169            start_time: Some((1000, true)),
1170            end_time: Some((2000, false)),
1171            limit: Some(5),
1172            offset: Some(10), // This should cause an error
1173            order_ascending: true,
1174        };
1175
1176        let result = query.try_to_request();
1177        assert!(
1178            result.is_err(),
1179            "offset must be None for try_to_request to succeed"
1180        );
1181
1182        let err_msg = format!("{}", result.unwrap_err());
1183        assert!(
1184            err_msg.contains("offset"),
1185            "error message should mention 'offset', got: {}",
1186            err_msg
1187        );
1188    }
1189}