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