1#[cfg(feature = "json-conversion")]
2use crate::serialization::json_safe_fields;
3#[cfg(feature = "json-conversion")]
4use crate::serialization::JsonConvertible;
5use crate::serialization::PlatformSerializable;
6#[cfg(feature = "value-conversion")]
7use crate::serialization::ValueConvertible;
8use crate::util::hash::hash_double;
9use crate::ProtocolError;
10use bincode::{Decode, DecodeUntrusted, Encode};
11use platform_serialization_derive::{
12 PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
13};
14use platform_value::Identifier;
15use platform_version::version::PlatformVersion;
16#[cfg(feature = "serde-conversion")]
17use serde::{Deserialize, Serialize};
18use std::fmt;
19
20pub type VotingPower = u32;
23
24const YES_NO_VOTE_POLL_ID_DOMAIN: &[u8] = b"dash platform yes/no vote poll id";
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
32#[cfg_attr(
33 feature = "serde-conversion",
34 derive(Serialize, Deserialize),
35 serde(rename_all = "camelCase")
36)]
37pub enum VotingPowerRounding {
38 Down,
40 Up,
42 DownPlusOne,
45 UpMinusOne,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, DecodeUntrusted)]
55#[cfg_attr(
56 feature = "serde-conversion",
57 derive(Serialize, Deserialize),
58 serde(tag = "$type", rename_all = "camelCase")
59)]
60pub enum YesNoMinimumVotingPower {
61 #[cfg_attr(feature = "serde-conversion", serde(rename_all = "camelCase"))]
63 Absolute { voting_power: VotingPower },
64 FractionOfTotal {
68 numerator: u8,
69 denominator: u8,
70 rounding: VotingPowerRounding,
71 },
72}
73
74impl YesNoMinimumVotingPower {
75 fn resolve_v0(&self, total_voting_power: VotingPower) -> VotingPower {
80 match *self {
81 YesNoMinimumVotingPower::Absolute { voting_power } => voting_power,
82 YesNoMinimumVotingPower::FractionOfTotal {
83 numerator,
84 denominator,
85 rounding,
86 } => {
87 if denominator == 0 {
88 return VotingPower::MAX;
89 }
90 let share = total_voting_power as u64 * numerator as u64;
91 let denominator = denominator as u64;
92 let resolved = match rounding {
93 VotingPowerRounding::Down => share / denominator,
94 VotingPowerRounding::Up => share.div_ceil(denominator),
95 VotingPowerRounding::DownPlusOne => share / denominator + 1,
96 VotingPowerRounding::UpMinusOne => {
97 share.div_ceil(denominator).saturating_sub(1)
98 }
99 };
100 VotingPower::try_from(resolved).unwrap_or(VotingPower::MAX)
101 }
102 }
103 }
104}
105
106impl fmt::Display for YesNoMinimumVotingPower {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 YesNoMinimumVotingPower::Absolute { voting_power } => write!(f, "{}", voting_power),
110 YesNoMinimumVotingPower::FractionOfTotal {
111 numerator,
112 denominator,
113 rounding,
114 } => write!(
115 f,
116 "{}/{} of the total, rounded {}",
117 numerator,
118 denominator,
119 match rounding {
120 VotingPowerRounding::Down => "down",
121 VotingPowerRounding::Up => "up",
122 VotingPowerRounding::DownPlusOne => "down plus one",
123 VotingPowerRounding::UpMinusOne => "up minus one",
124 }
125 ),
126 }
127 }
128}
129
130#[derive(
144 Debug,
145 Clone,
146 Copy,
147 PartialEq,
148 Eq,
149 Encode,
150 Decode,
151 DecodeUntrusted,
152 PlatformSerialize,
153 PlatformDeserializeTrusted,
154 PlatformDeserializeUntrusted,
155)]
156#[cfg_attr(
157 feature = "serde-conversion",
158 derive(Serialize, Deserialize),
159 serde(tag = "$type", rename_all = "camelCase")
160)]
161#[platform_serialize(unversioned)]
162pub enum YesNoVotePollPurpose {
163 #[cfg_attr(feature = "serde-conversion", serde(rename_all = "camelCase"))]
167 DecisionProposal { document_id: Identifier },
168}
169
170impl fmt::Display for YesNoVotePollPurpose {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 match self {
173 YesNoVotePollPurpose::DecisionProposal { document_id } => {
174 write!(f, "decision proposal {}", document_id)
175 }
176 }
177 }
178}
179
180#[cfg_attr(feature = "json-conversion", json_safe_fields)]
196#[cfg_attr(
197 all(feature = "json-conversion", feature = "serde-conversion"),
198 derive(JsonConvertible)
199)]
200#[derive(
201 Debug,
202 Clone,
203 Encode,
204 Decode,
205 PlatformSerialize,
206 PlatformDeserializeTrusted,
207 PlatformDeserializeUntrusted,
208 PartialEq,
209 Eq,
210 DecodeUntrusted,
211)]
212#[cfg_attr(
213 feature = "serde-conversion",
214 derive(Serialize, Deserialize),
215 serde(rename_all = "camelCase")
216)]
217#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
218#[platform_serialize(limit = 100000, unversioned)]
220pub struct YesNoVotePoll {
221 pub purpose: YesNoVotePollPurpose,
223 pub supermajority_numerator: u8,
225 pub supermajority_denominator: u8,
227 pub minimum_voting_power: YesNoMinimumVotingPower,
229}
230
231impl fmt::Display for YesNoVotePoll {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 write!(
234 f,
235 "YesNoVotePoll {{ purpose: {}, supermajority: {}/{}, minimum_voting_power: {} }}",
236 self.purpose,
237 self.supermajority_numerator,
238 self.supermajority_denominator,
239 self.minimum_voting_power
240 )
241 }
242}
243
244impl YesNoVotePoll {
245 pub fn validate_parameters(
254 &self,
255 platform_version: &PlatformVersion,
256 ) -> Result<(), ProtocolError> {
257 match platform_version
258 .dpp
259 .voting_versions
260 .yes_no_vote_poll_parameters
261 {
262 Some(0) => self.validate_parameters_v0(platform_version),
263 Some(version) => Err(ProtocolError::UnknownVersionMismatch {
264 method: "YesNoVotePoll::validate_parameters".to_string(),
265 known_versions: vec![0],
266 received: version,
267 }),
268 None => Err(ProtocolError::UnknownVersionError(
269 "YesNoVotePoll::validate_parameters is not active before protocol version 15"
270 .to_string(),
271 )),
272 }
273 }
274
275 fn validate_parameters_v0(
281 &self,
282 platform_version: &PlatformVersion,
283 ) -> Result<(), ProtocolError> {
284 if self.supermajority_denominator == 0 {
285 return Err(ProtocolError::VoteError(
286 "a yes/no vote poll supermajority denominator must not be zero".to_string(),
287 ));
288 }
289 if self.supermajority_numerator == 0 {
290 return Err(ProtocolError::VoteError(
291 "a yes/no vote poll supermajority numerator must not be zero".to_string(),
292 ));
293 }
294 if self.supermajority_numerator >= self.supermajority_denominator {
295 return Err(ProtocolError::VoteError(format!(
296 "a yes/no vote poll supermajority numerator ({}) must be below its denominator ({})",
297 self.supermajority_numerator, self.supermajority_denominator
298 )));
299 }
300 match self.minimum_voting_power {
301 YesNoMinimumVotingPower::Absolute { voting_power } => {
302 let max_voting_power = platform_version
303 .system_limits
304 .max_yes_no_vote_poll_minimum_voting_power;
305 if voting_power > max_voting_power {
306 return Err(ProtocolError::VoteError(format!(
307 "a yes/no vote poll minimum voting power is at most {}, got {}",
308 max_voting_power, voting_power
309 )));
310 }
311 }
312 YesNoMinimumVotingPower::FractionOfTotal {
313 numerator,
314 denominator,
315 ..
316 } => {
317 if denominator == 0 {
318 return Err(ProtocolError::VoteError(
319 "a yes/no vote poll minimum share denominator must not be zero".to_string(),
320 ));
321 }
322 let max_percent = platform_version
323 .system_limits
324 .max_yes_no_vote_poll_minimum_voting_power_percent_of_total;
325 if numerator as u32 * 100 > denominator as u32 * max_percent as u32 {
326 return Err(ProtocolError::VoteError(format!(
327 "a yes/no vote poll minimum share is at most {}% of the total voting power, got {}/{}",
328 max_percent, numerator, denominator
329 )));
330 }
331 }
332 }
333 Ok(())
334 }
335
336 pub fn required_voting_power(
347 &self,
348 total_voting_power: VotingPower,
349 platform_version: &PlatformVersion,
350 ) -> Result<VotingPower, ProtocolError> {
351 match platform_version
352 .dpp
353 .voting_versions
354 .yes_no_vote_poll_decision
355 {
356 Some(0) => Ok(self.required_voting_power_v0(total_voting_power)),
357 Some(version) => Err(ProtocolError::UnknownVersionMismatch {
358 method: "YesNoVotePoll::required_voting_power".to_string(),
359 known_versions: vec![0],
360 received: version,
361 }),
362 None => Err(ProtocolError::UnknownVersionError(
363 "YesNoVotePoll::required_voting_power is not active before protocol version 15"
364 .to_string(),
365 )),
366 }
367 }
368
369 pub fn passes(
382 &self,
383 yes_voting_power: VotingPower,
384 no_voting_power: VotingPower,
385 total_voting_power: VotingPower,
386 platform_version: &PlatformVersion,
387 ) -> Result<bool, ProtocolError> {
388 match platform_version
389 .dpp
390 .voting_versions
391 .yes_no_vote_poll_decision
392 {
393 Some(0) => Ok(self.passes_v0(yes_voting_power, no_voting_power, total_voting_power)),
394 Some(version) => Err(ProtocolError::UnknownVersionMismatch {
395 method: "YesNoVotePoll::passes".to_string(),
396 known_versions: vec![0],
397 received: version,
398 }),
399 None => Err(ProtocolError::UnknownVersionError(
400 "YesNoVotePoll::passes is not active before protocol version 15".to_string(),
401 )),
402 }
403 }
404
405 fn required_voting_power_v0(&self, total_voting_power: VotingPower) -> VotingPower {
407 self.minimum_voting_power.resolve_v0(total_voting_power)
408 }
409
410 fn passes_v0(
413 &self,
414 yes_voting_power: VotingPower,
415 no_voting_power: VotingPower,
416 total_voting_power: VotingPower,
417 ) -> bool {
418 let yes = yes_voting_power as u64;
419 let cast = yes + no_voting_power as u64;
420 if cast < self.required_voting_power_v0(total_voting_power) as u64 {
421 return false;
422 }
423 yes * self.supermajority_denominator as u64 > cast * self.supermajority_numerator as u64
424 }
425
426 pub fn unique_id(
436 &self,
437 platform_version: &PlatformVersion,
438 ) -> Result<Identifier, ProtocolError> {
439 match platform_version.dpp.voting_versions.yes_no_vote_poll_id {
440 Some(0) => self.unique_id_v0(),
441 Some(version) => Err(ProtocolError::UnknownVersionMismatch {
442 method: "YesNoVotePoll::unique_id".to_string(),
443 known_versions: vec![0],
444 received: version,
445 }),
446 None => Err(ProtocolError::UnknownVersionError(
447 "YesNoVotePoll::unique_id is not active before protocol version 15".to_string(),
448 )),
449 }
450 }
451
452 pub fn specialized_balance_id(
460 &self,
461 platform_version: &PlatformVersion,
462 ) -> Result<Identifier, ProtocolError> {
463 self.unique_id(platform_version)
464 }
465
466 fn unique_id_v0(&self) -> Result<Identifier, ProtocolError> {
470 let mut preimage = YES_NO_VOTE_POLL_ID_DOMAIN.to_vec();
471 preimage.extend(self.purpose.serialize_to_bytes()?);
472 Ok(Identifier::new(hash_double(preimage)))
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 fn poll() -> YesNoVotePoll {
481 YesNoVotePoll {
482 purpose: YesNoVotePollPurpose::DecisionProposal {
483 document_id: Identifier::new([0xc1; 32]),
484 },
485 supermajority_numerator: 2,
486 supermajority_denominator: 3,
487 minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 },
488 }
489 }
490
491 #[test]
492 fn should_pass_strictly_above_two_thirds_and_fail_at_or_below_it() {
493 let poll = YesNoVotePoll {
495 minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 300 },
496 ..poll()
497 };
498 assert!(poll.passes_v0(201, 99, 0));
500 assert!(!poll.passes_v0(200, 100, 0));
501 assert!(poll.passes_v0(201, 100, 0));
503 assert!(!poll.passes_v0(200, 101, 0));
504 }
505
506 #[test]
507 fn should_fail_below_the_minimum_voting_power_even_when_unanimous() {
508 let poll = poll();
509 assert!(!poll.passes_v0(399, 0, 0));
510 assert!(poll.passes_v0(400, 0, 0));
511 }
512
513 #[test]
514 fn should_resolve_a_share_of_the_total_rounded_as_asked() {
515 let third = |rounding| YesNoMinimumVotingPower::FractionOfTotal {
516 numerator: 1,
517 denominator: 3,
518 rounding,
519 };
520 assert_eq!(third(VotingPowerRounding::Down).resolve_v0(10), 3);
521 assert_eq!(third(VotingPowerRounding::Up).resolve_v0(10), 4);
522 assert_eq!(third(VotingPowerRounding::Down).resolve_v0(9), 3);
524 assert_eq!(third(VotingPowerRounding::Up).resolve_v0(9), 3);
525 assert_eq!(third(VotingPowerRounding::Up).resolve_v0(0), 0);
526 assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve_v0(10), 4);
528 assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve_v0(9), 4);
529 assert_eq!(third(VotingPowerRounding::DownPlusOne).resolve_v0(0), 1);
530 assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve_v0(10), 3);
531 assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve_v0(9), 2);
532 assert_eq!(third(VotingPowerRounding::UpMinusOne).resolve_v0(0), 0);
533 let majority = YesNoMinimumVotingPower::FractionOfTotal {
535 numerator: 1,
536 denominator: 2,
537 rounding: VotingPowerRounding::DownPlusOne,
538 };
539 assert_eq!(majority.resolve_v0(10), 6);
540 assert_eq!(majority.resolve_v0(9), 5);
541 assert_eq!(majority.resolve_v0(u32::MAX), u32::MAX / 2 + 1);
542 assert_eq!(
543 YesNoMinimumVotingPower::Absolute { voting_power: 400 }.resolve_v0(10),
544 400
545 );
546 let half = YesNoMinimumVotingPower::FractionOfTotal {
548 numerator: 1,
549 denominator: 2,
550 rounding: VotingPowerRounding::Up,
551 };
552 assert_eq!(half.resolve_v0(u32::MAX), u32::MAX / 2 + 1);
553 }
554
555 #[test]
556 fn should_judge_a_share_minimum_against_the_total_at_close() {
557 let poll = YesNoVotePoll {
559 minimum_voting_power: YesNoMinimumVotingPower::FractionOfTotal {
560 numerator: 1,
561 denominator: 10,
562 rounding: VotingPowerRounding::Up,
563 },
564 ..poll()
565 };
566 assert_eq!(poll.required_voting_power_v0(3_801), 381);
568 assert!(!poll.passes_v0(380, 0, 3_801));
569 assert!(poll.passes_v0(381, 0, 3_801));
570 assert!(!poll.passes_v0(381, 0, 3_811));
572 }
573
574 #[test]
575 fn should_bound_the_minimum() {
576 let platform_version = PlatformVersion::latest();
577 let max_voting_power = platform_version
578 .system_limits
579 .max_yes_no_vote_poll_minimum_voting_power;
580 let with_minimum = |minimum_voting_power| YesNoVotePoll {
581 minimum_voting_power,
582 ..poll()
583 };
584 let share = |numerator, denominator| YesNoMinimumVotingPower::FractionOfTotal {
585 numerator,
586 denominator,
587 rounding: VotingPowerRounding::Up,
588 };
589 for accepted in [
590 YesNoMinimumVotingPower::Absolute { voting_power: 0 },
591 YesNoMinimumVotingPower::Absolute {
592 voting_power: max_voting_power,
593 },
594 share(0, 1),
595 share(1, 3),
596 share(1, 2),
597 share(50, 100),
598 ] {
599 assert!(
600 with_minimum(accepted)
601 .validate_parameters(platform_version)
602 .is_ok(),
603 "{accepted} should be accepted"
604 );
605 }
606 for refused in [
607 YesNoMinimumVotingPower::Absolute {
608 voting_power: max_voting_power + 1,
609 },
610 share(1, 0),
611 share(51, 100),
612 share(2, 3),
613 ] {
614 assert!(
615 with_minimum(refused)
616 .validate_parameters(platform_version)
617 .is_err(),
618 "{refused} should be refused"
619 );
620 }
621 }
622
623 #[test]
624 fn should_not_overflow_on_the_largest_tallies() {
625 let poll = YesNoVotePoll {
626 supermajority_numerator: 254,
627 supermajority_denominator: 255,
628 ..poll()
629 };
630 assert!(poll.passes_v0(u32::MAX, 0, 0));
631 assert!(!poll.passes_v0(u32::MAX, u32::MAX, 0));
632 }
633
634 #[test]
635 fn should_refuse_parameters_no_poll_can_run_with() {
636 let platform_version = PlatformVersion::latest();
637 assert!(poll().validate_parameters(platform_version).is_ok());
638 assert!(YesNoVotePoll {
639 supermajority_denominator: 0,
640 ..poll()
641 }
642 .validate_parameters(platform_version)
643 .is_err());
644 assert!(YesNoVotePoll {
645 supermajority_numerator: 3,
646 supermajority_denominator: 3,
647 ..poll()
648 }
649 .validate_parameters(platform_version)
650 .is_err());
651 assert!(YesNoVotePoll {
653 supermajority_numerator: 0,
654 ..poll()
655 }
656 .validate_parameters(platform_version)
657 .is_err());
658 assert!(YesNoVotePoll {
660 supermajority_numerator: 1,
661 supermajority_denominator: 2,
662 ..poll()
663 }
664 .validate_parameters(platform_version)
665 .is_ok());
666 }
667
668 #[test]
669 fn should_derive_the_id_from_the_purpose_alone() {
670 let platform_version = PlatformVersion::latest();
671 let base = poll().unique_id(platform_version).expect("id");
672 let other_purpose = YesNoVotePoll {
673 purpose: YesNoVotePollPurpose::DecisionProposal {
674 document_id: Identifier::new([0xc2; 32]),
675 },
676 ..poll()
677 };
678 assert_ne!(other_purpose.unique_id(platform_version).expect("id"), base);
679 let other_rule = YesNoVotePoll {
682 supermajority_numerator: 1,
683 supermajority_denominator: 2,
684 ..poll()
685 };
686 let other_minimum = YesNoVotePoll {
687 minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 401 },
688 ..poll()
689 };
690 for same in [other_rule, other_minimum] {
691 assert_eq!(same.unique_id(platform_version).expect("id"), base);
692 }
693 assert_eq!(
694 poll()
695 .specialized_balance_id(platform_version)
696 .expect("balance id"),
697 base
698 );
699 }
700
701 #[test]
702 fn should_hash_the_id_under_the_yes_no_domain_tag() {
703 let encoded = poll()
704 .purpose
705 .serialize_to_bytes()
706 .expect("encoded purpose");
707 assert_eq!(encoded, [[0u8].as_slice(), &[0xc1; 32]].concat());
710 let untagged = Identifier::new(hash_double(&encoded));
711 let tagged = Identifier::new(hash_double(
712 [YES_NO_VOTE_POLL_ID_DOMAIN, encoded.as_slice()].concat(),
713 ));
714 let id = poll().unique_id(PlatformVersion::latest()).expect("id");
715 assert_eq!(id, tagged);
716 assert_ne!(id, untagged);
717 }
718
719 #[test]
720 fn should_report_the_poll_rules_not_active_before_protocol_version_15() {
721 let platform_version = PlatformVersion::get(14).expect("expected protocol version 14");
722 let not_active = |result: Result<(), ProtocolError>, method: &str| {
723 assert!(
724 matches!(result, Err(ProtocolError::UnknownVersionError(_))),
725 "expected {method} to be not active at protocol version 14, got {result:?}"
726 );
727 };
728 not_active(
729 poll().validate_parameters(platform_version),
730 "validate_parameters",
731 );
732 not_active(
733 poll()
734 .required_voting_power(100, platform_version)
735 .map(|_| ()),
736 "required_voting_power",
737 );
738 not_active(
739 poll().passes(10, 0, 100, platform_version).map(|_| ()),
740 "passes",
741 );
742 not_active(poll().unique_id(platform_version).map(|_| ()), "unique_id");
743 not_active(
744 poll().specialized_balance_id(platform_version).map(|_| ()),
745 "specialized_balance_id",
746 );
747 }
748}
749
750#[cfg(all(
751 test,
752 feature = "json-conversion",
753 feature = "value-conversion",
754 feature = "serde-conversion"
755))]
756mod json_convertible_tests {
757 use super::*;
758 use platform_value::platform_value;
759 use serde_json::json;
760
761 fn fixture() -> YesNoVotePoll {
762 YesNoVotePoll {
763 purpose: YesNoVotePollPurpose::DecisionProposal {
764 document_id: Identifier::new([0xc1; 32]),
765 },
766 supermajority_numerator: 2,
767 supermajority_denominator: 3,
768 minimum_voting_power: YesNoMinimumVotingPower::Absolute { voting_power: 400 },
769 }
770 }
771
772 #[test]
773 fn json_round_trip_with_full_wire_shape() {
774 use crate::serialization::JsonConvertible;
775 let original = fixture();
776 let json = original.to_json().expect("to_json");
777 assert_eq!(
779 json,
780 json!({
781 "purpose": {
782 "$type": "decisionProposal",
783 "documentId": Identifier::new([0xc1; 32])
784 .to_string(platform_value::string_encoding::Encoding::Base58),
785 },
786 "supermajorityNumerator": 2,
787 "supermajorityDenominator": 3,
788 "minimumVotingPower": { "$type": "absolute", "votingPower": 400 },
789 })
790 );
791 let recovered = YesNoVotePoll::from_json(json).expect("from_json");
792 assert_eq!(original, recovered);
793 }
794
795 #[test]
796 fn should_round_trip_a_share_minimum_through_json() {
797 use crate::serialization::JsonConvertible;
798 let original = YesNoVotePoll {
799 minimum_voting_power: YesNoMinimumVotingPower::FractionOfTotal {
800 numerator: 1,
801 denominator: 3,
802 rounding: VotingPowerRounding::Up,
803 },
804 ..fixture()
805 };
806 let json = original.to_json().expect("to_json");
807 assert_eq!(
808 json["minimumVotingPower"],
809 json!({ "$type": "fractionOfTotal", "numerator": 1, "denominator": 3, "rounding": "up" })
810 );
811 let recovered = YesNoVotePoll::from_json(json).expect("from_json");
812 assert_eq!(original, recovered);
813 }
814
815 #[test]
816 fn should_name_every_rounding_in_camel_case() {
817 use crate::serialization::JsonConvertible;
818 for (rounding, name) in [
819 (VotingPowerRounding::Down, "down"),
820 (VotingPowerRounding::Up, "up"),
821 (VotingPowerRounding::DownPlusOne, "downPlusOne"),
822 (VotingPowerRounding::UpMinusOne, "upMinusOne"),
823 ] {
824 let original = YesNoVotePoll {
825 minimum_voting_power: YesNoMinimumVotingPower::FractionOfTotal {
826 numerator: 1,
827 denominator: 2,
828 rounding,
829 },
830 ..fixture()
831 };
832 let json = original.to_json().expect("to_json");
833 assert_eq!(json["minimumVotingPower"]["rounding"], json!(name));
834 assert_eq!(YesNoVotePoll::from_json(json).expect("from_json"), original);
835 }
836 }
837
838 #[test]
839 fn value_round_trip_with_full_wire_shape() {
840 use crate::serialization::ValueConvertible;
841 let original = fixture();
842 let value = original.to_object().expect("to_object");
843 assert_eq!(
844 value,
845 platform_value!({
846 "purpose": {
847 "$type": "decisionProposal",
848 "documentId": Identifier::new([0xc1; 32]),
849 },
850 "supermajorityNumerator": 2u8,
851 "supermajorityDenominator": 3u8,
852 "minimumVotingPower": { "$type": "absolute", "votingPower": 400u32 },
853 })
854 );
855 let recovered = YesNoVotePoll::from_object(value).expect("from_object");
856 assert_eq!(original, recovered);
857 }
858}