1use 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
34pub trait TryFromRequest<T>: Sized {
42 fn try_from_request(grpc_request: T) -> Result<Self, Error>;
44
45 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, 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
447fn 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
465fn 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 #[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 let short = [0u8; 16];
502 assert!(to_bytes32(&short).is_err());
503
504 let long = [0u8; 33];
506 assert!(to_bytes32(&long).is_err());
507
508 assert!(to_bytes32(&[]).is_err());
510 }
511
512 #[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 #[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 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 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 #[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 #[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 #[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 #[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), 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 #[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 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 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], 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 #[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], 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], 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 #[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], 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 #[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 #[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], 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 #[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], 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], 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 #[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 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 #[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), 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}