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