1use crate::consensus::basic::data_contract::TokenShieldedPoolIncompatibleRulesError;
2use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError;
3use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters;
4use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters;
5use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0;
6use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1;
7use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers;
8use crate::data_contract::change_control_rules::ChangeControlRules;
9use crate::data_contract::errors::DataContractError;
10use crate::data_contract::TokenContractPosition;
11#[cfg(feature = "json-conversion")]
12use crate::serialization::JsonConvertible;
13#[cfg(feature = "value-conversion")]
14use crate::serialization::ValueConvertible;
15use crate::validation::SimpleConsensusValidationResult;
16use bincode::{Decode, DecodeUntrusted, Encode};
17use derive_more::From;
18use platform_version::version::PlatformVersion;
19use serde::{Deserialize, Serialize};
20use std::borrow::Cow;
21use std::collections::BTreeMap;
22use std::fmt;
23
24pub mod accessors;
25mod methods;
26pub mod v0;
27pub mod v1;
28
29#[cfg_attr(feature = "json-conversion", derive(JsonConvertible))]
30#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
31#[derive(
32 Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq, Eq, From, DecodeUntrusted,
33)]
34#[serde(tag = "$formatVersion")]
35pub enum TokenConfiguration {
36 #[serde(rename = "0")]
37 V0(TokenConfigurationV0),
38 #[serde(rename = "1")]
41 V1(TokenConfigurationV1),
42}
43impl TokenConfiguration {
44 pub fn as_cow_v0(&self) -> Cow<'_, TokenConfigurationV0> {
45 match self {
46 TokenConfiguration::V0(v0) => Cow::Borrowed(v0),
47 TokenConfiguration::V1(v1) => Cow::Borrowed(&v1.base),
48 }
49 }
50
51 pub fn format_version(&self) -> u16 {
53 match self {
54 TokenConfiguration::V0(_) => 0,
55 TokenConfiguration::V1(_) => 1,
56 }
57 }
58
59 pub fn validate_format_version(
66 &self,
67 platform_version: &PlatformVersion,
68 ) -> SimpleConsensusValidationResult {
69 let bounds = &platform_version
70 .dpp
71 .contract_versions
72 .token_versions
73 .token_configuration_format;
74 let format_version = self.format_version();
75 if format_version < bounds.min_version || format_version > bounds.max_version {
76 SimpleConsensusValidationResult::new_with_error(
77 UnsupportedVersionError::new(
78 format_version,
79 bounds.min_version,
80 bounds.max_version,
81 )
82 .into(),
83 )
84 } else {
85 SimpleConsensusValidationResult::new()
86 }
87 }
88
89 pub fn validate_shielded_pool_rules(
97 &self,
98 token_contract_position: TokenContractPosition,
99 ) -> SimpleConsensusValidationResult {
100 if !self.has_shielded_pool() {
101 return SimpleConsensusValidationResult::new();
102 }
103 let rules: [(&ChangeControlRules, &str); 3] = [
104 (self.freeze_rules(), "freezeRules"),
105 (self.unfreeze_rules(), "unfreezeRules"),
106 (self.destroy_frozen_funds_rules(), "destroyFrozenFundsRules"),
107 ];
108 for (rule, name) in rules {
109 let disabled = *rule.authorized_to_make_change_action_takers()
110 == AuthorizedActionTakers::NoOne
111 && *rule.admin_action_takers() == AuthorizedActionTakers::NoOne;
112 if !disabled {
113 return SimpleConsensusValidationResult::new_with_error(
114 TokenShieldedPoolIncompatibleRulesError::new(
115 token_contract_position,
116 name.to_string(),
117 )
118 .into(),
119 );
120 }
121 }
122 SimpleConsensusValidationResult::new()
123 }
124
125 pub fn validate_minimum_pool_notes_for_outgoing(
131 &self,
132 token_contract_position: TokenContractPosition,
133 platform_version: &PlatformVersion,
134 ) -> SimpleConsensusValidationResult {
135 validate_minimum_pool_notes_for_outgoing_bound(
136 self.minimum_pool_notes_for_outgoing(),
137 token_contract_position,
138 platform_version,
139 )
140 }
141}
142
143pub fn validate_minimum_pool_notes_for_outgoing_bound(
147 minimum_pool_notes: u64,
148 token_contract_position: TokenContractPosition,
149 platform_version: &PlatformVersion,
150) -> SimpleConsensusValidationResult {
151 let max_minimum_pool_notes = platform_version
152 .system_limits
153 .max_token_pool_notes_for_outgoing;
154 if minimum_pool_notes > max_minimum_pool_notes {
155 return SimpleConsensusValidationResult::new_with_error(
156 DataContractError::KeyWrongBounds(format!(
157 "token at position {token_contract_position}: minimumPoolNotesForOutgoing \
158 {minimum_pool_notes} is above the maximum {max_minimum_pool_notes}"
159 ))
160 .into(),
161 );
162 }
163 SimpleConsensusValidationResult::new()
164}
165
166pub fn validate_token_configurations(
172 tokens: &BTreeMap<TokenContractPosition, TokenConfiguration>,
173 platform_version: &PlatformVersion,
174) -> SimpleConsensusValidationResult {
175 for (position, configuration) in tokens {
176 let result = configuration.validate_format_version(platform_version);
177 if !result.is_valid() {
178 return result;
179 }
180 let result = configuration.validate_shielded_pool_rules(*position);
181 if !result.is_valid() {
182 return result;
183 }
184 let result =
185 configuration.validate_minimum_pool_notes_for_outgoing(*position, platform_version);
186 if !result.is_valid() {
187 return result;
188 }
189 }
190 SimpleConsensusValidationResult::new()
191}
192
193impl fmt::Display for TokenConfiguration {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 TokenConfiguration::V0(v0) => write!(f, "{}", v0),
197 TokenConfiguration::V1(v1) => write!(f, "{}", v1),
198 }
199 }
200}
201
202#[cfg(all(test, feature = "json-conversion"))]
203mod tests {
204 use super::*;
205 use crate::serialization::JsonConvertible;
206
207 #[test]
208 fn token_configuration_large_supply_json_round_trip() {
209 let mut config = TokenConfigurationV0::default_most_restrictive();
210 config.base_supply = u64::MAX;
211 let config = TokenConfiguration::V0(config);
212
213 let json = config.to_json().expect("to_json should succeed");
214
215 assert!(
217 json["baseSupply"].is_string(),
218 "baseSupply should be a string for large values, got: {:?}",
219 json["baseSupply"]
220 );
221 assert_eq!(json["baseSupply"].as_str().unwrap(), u64::MAX.to_string());
222
223 let restored = TokenConfiguration::from_json(json).expect("from_json should succeed");
224 assert_eq!(config, restored);
225 }
226}
227
228#[cfg(all(
229 test,
230 feature = "json-conversion",
231 feature = "value-conversion",
232 feature = "serde-conversion"
233))]
234mod json_convertible_tests {
235 use super::*;
236 use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0;
237
238 fn fixture() -> TokenConfiguration {
242 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
243 }
244
245 #[test]
253 fn json_round_trip_with_envelope_shape() {
254 use crate::serialization::JsonConvertible;
255 let original = fixture();
256 let json = original.to_json().expect("to_json");
257 assert_eq!(
259 json.get("$formatVersion").and_then(|v| v.as_str()),
260 Some("0")
261 );
262 for key in [
263 "conventions",
264 "conventionsChangeRules",
265 "baseSupply",
266 "maxSupply",
267 "keepsHistory",
268 "startAsPaused",
269 "allowTransferToFrozenBalance",
270 "maxSupplyChangeRules",
271 "distributionRules",
272 "marketplaceRules",
273 "manualMintingRules",
274 "manualBurningRules",
275 "freezeRules",
276 "unfreezeRules",
277 "destroyFrozenFundsRules",
278 "emergencyActionRules",
279 "mainControlGroup",
280 "mainControlGroupCanBeModified",
281 "description",
282 ] {
283 assert!(
284 json.get(key).is_some(),
285 "expected top-level key {:?} in JSON envelope",
286 key
287 );
288 }
289 let recovered = TokenConfiguration::from_json(json).expect("from_json");
290 assert_eq!(original, recovered);
291 }
292
293 #[test]
294 fn value_round_trip_with_envelope_shape() {
295 use crate::serialization::ValueConvertible;
296 let original = fixture();
297 let value = original.to_object().expect("to_object");
298 let map = value.as_map().expect("value is a Map");
300 let has_key = |k: &str| {
301 map.iter()
302 .any(|(key, _)| matches!(key, platform_value::Value::Text(t) if t == k))
303 };
304 assert!(has_key("$formatVersion"));
305 for key in [
306 "conventions",
307 "conventionsChangeRules",
308 "baseSupply",
309 "maxSupply",
310 "keepsHistory",
311 "startAsPaused",
312 "allowTransferToFrozenBalance",
313 "maxSupplyChangeRules",
314 "distributionRules",
315 "marketplaceRules",
316 "manualMintingRules",
317 "manualBurningRules",
318 "freezeRules",
319 "unfreezeRules",
320 "destroyFrozenFundsRules",
321 "emergencyActionRules",
322 "mainControlGroup",
323 "mainControlGroupCanBeModified",
324 "description",
325 ] {
326 assert!(
327 has_key(key),
328 "expected top-level key {:?} in Value envelope",
329 key
330 );
331 }
332 let recovered = TokenConfiguration::from_object(value).expect("from_object");
333 assert_eq!(original, recovered);
334 }
335
336 mod shielded_pool_rules {
337 use super::*;
338 use crate::consensus::basic::BasicError;
339 use crate::consensus::ConsensusError;
340 use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Setters;
341 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
342 use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0;
343 use crate::prelude::Identifier;
344
345 fn rules(
346 authorized: AuthorizedActionTakers,
347 admin: AuthorizedActionTakers,
348 ) -> ChangeControlRules {
349 ChangeControlRules::V0(ChangeControlRulesV0 {
350 authorized_to_make_change: authorized,
351 admin_action_takers: admin,
352 changing_authorized_action_takers_to_no_one_allowed: false,
353 changing_admin_action_takers_to_no_one_allowed: false,
354 self_changing_admin_action_takers_allowed: false,
355 })
356 }
357
358 fn pooled_configuration() -> TokenConfiguration {
359 let mut configuration =
360 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
361 configuration.set_has_shielded_pool(true);
362 configuration
363 }
364
365 fn rejected_rule(configuration: &TokenConfiguration) -> Option<String> {
366 let result = configuration.validate_shielded_pool_rules(3);
367 match result.errors.as_slice() {
368 [] => None,
369 [ConsensusError::BasicError(
370 BasicError::TokenShieldedPoolIncompatibleRulesError(error),
371 )] => {
372 assert_eq!(error.token_contract_position(), 3);
373 Some(error.rule().to_string())
374 }
375 other => panic!("unexpected errors: {other:?}"),
376 }
377 }
378
379 #[test]
380 fn most_restrictive_defaults_are_compatible_with_a_pool() {
381 assert_eq!(rejected_rule(&pooled_configuration()), None);
382 }
383
384 #[test]
385 fn a_token_without_a_pool_may_keep_freeze_rules() {
386 let mut configuration =
387 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
388 configuration.set_freeze_rules(rules(
389 AuthorizedActionTakers::ContractOwner,
390 AuthorizedActionTakers::ContractOwner,
391 ));
392 assert_eq!(rejected_rule(&configuration), None);
393 }
394
395 #[test]
396 fn a_pooled_token_rejects_an_authorized_freezer() {
397 let mut configuration = pooled_configuration();
398 configuration.set_freeze_rules(rules(
399 AuthorizedActionTakers::ContractOwner,
400 AuthorizedActionTakers::NoOne,
401 ));
402 assert_eq!(
403 rejected_rule(&configuration),
404 Some("freezeRules".to_string())
405 );
406 }
407
408 #[test]
409 fn a_pooled_token_rejects_an_admin_who_could_enable_unfreezing_later() {
410 let mut configuration = pooled_configuration();
411 configuration.set_unfreeze_rules(rules(
412 AuthorizedActionTakers::NoOne,
413 AuthorizedActionTakers::MainGroup,
414 ));
415 assert_eq!(
416 rejected_rule(&configuration),
417 Some("unfreezeRules".to_string())
418 );
419 }
420
421 #[test]
422 fn a_pooled_token_rejects_frozen_funds_destruction() {
423 let mut configuration = pooled_configuration();
424 configuration.set_destroy_frozen_funds_rules(rules(
425 AuthorizedActionTakers::Identity(Identifier::from([1u8; 32])),
426 AuthorizedActionTakers::NoOne,
427 ));
428 assert_eq!(
429 rejected_rule(&configuration),
430 Some("destroyFrozenFundsRules".to_string())
431 );
432 }
433 }
434}
435
436#[cfg(test)]
437mod minimum_pool_notes_tests {
438 use super::*;
439 use crate::consensus::basic::BasicError;
440 use crate::consensus::codes::ErrorWithCode;
441 use crate::consensus::ConsensusError;
442 use crate::data_contract::associated_token::token_configuration::accessors::v0::{
443 TokenConfigurationV0Getters, TokenConfigurationV0Setters,
444 };
445 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
446 use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
447 use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0;
448 use crate::group::action_taker::{ActionGoal, ActionTaker};
449 use crate::prelude::Identifier;
450
451 fn pooled() -> TokenConfiguration {
452 let mut configuration =
453 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
454 configuration.set_has_shielded_pool(true);
455 configuration
456 }
457
458 fn pooled_with_threshold(minimum_pool_notes: u64) -> TokenConfiguration {
459 let mut configuration = pooled();
460 let TokenConfiguration::V1(v1) = &mut configuration else {
461 panic!("a pooled configuration is V1");
462 };
463 v1.minimum_pool_notes_for_outgoing = Some(minimum_pool_notes);
464 configuration
465 }
466
467 fn rules(authorized: AuthorizedActionTakers) -> ChangeControlRules {
468 ChangeControlRules::V0(ChangeControlRulesV0 {
469 authorized_to_make_change: authorized,
470 admin_action_takers: authorized,
471 changing_authorized_action_takers_to_no_one_allowed: false,
472 changing_admin_action_takers_to_no_one_allowed: false,
473 self_changing_admin_action_takers_allowed: false,
474 })
475 }
476
477 #[test]
478 fn should_read_a_configuration_without_a_threshold_as_zero() {
479 assert_eq!(
480 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
481 .minimum_pool_notes_for_outgoing(),
482 0
483 );
484 let configuration = pooled();
485 let TokenConfiguration::V1(v1) = &configuration else {
486 panic!("a pooled configuration is V1");
487 };
488 assert_eq!(v1.minimum_pool_notes_for_outgoing, None);
489 assert_eq!(configuration.minimum_pool_notes_for_outgoing(), 0);
490 assert_eq!(
492 configuration.authorized_action_takers_for_configuration_item(
493 &TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(1)
494 ),
495 AuthorizedActionTakers::NoOne
496 );
497 }
498
499 #[test]
500 fn should_bound_the_threshold_by_the_system_limit_with_error_10241() {
501 let platform_version = PlatformVersion::latest();
502 let max = platform_version
503 .system_limits
504 .max_token_pool_notes_for_outgoing;
505 assert_eq!(max, 250);
506
507 let at_the_limit = BTreeMap::from([(0, pooled_with_threshold(max))]);
508 let result = validate_token_configurations(&at_the_limit, platform_version);
509 assert!(result.is_valid(), "unexpected errors: {:?}", result.errors);
510
511 let over_the_limit = BTreeMap::from([(3, pooled_with_threshold(max + 1))]);
512 let result = validate_token_configurations(&over_the_limit, platform_version);
513 assert_matches::assert_matches!(
514 result.errors.as_slice(),
515 [error @ ConsensusError::BasicError(BasicError::ContractError(
516 DataContractError::KeyWrongBounds(message)
517 ))] if error.code() == 10241 && message.contains("position 3")
518 );
519 }
520
521 #[test]
522 fn should_govern_the_threshold_by_its_own_rules_on_a_pooled_token_only() {
523 let owner = Identifier::from([1; 32]);
524 let other = Identifier::from([2; 32]);
525 let groups = BTreeMap::new();
526 let change = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(12);
527 let can_apply = |configuration: &TokenConfiguration, action_taker: Identifier| {
528 configuration.can_apply_token_configuration_item(
529 &change,
530 &owner,
531 None,
532 &groups,
533 &ActionTaker::SingleIdentity(action_taker),
534 ActionGoal::ActionCompletion,
535 )
536 };
537
538 let mut governed = pooled();
539 let TokenConfiguration::V1(v1) = &mut governed else {
540 panic!("a pooled configuration is V1");
541 };
542 v1.minimum_pool_notes_for_outgoing_change_rules =
543 rules(AuthorizedActionTakers::ContractOwner);
544 assert!(can_apply(&governed, owner));
545 assert!(!can_apply(&governed, other));
546 assert_eq!(
547 governed.controlling_action_takers_for_configuration_item(&change),
548 AuthorizedActionTakers::ContractOwner
549 );
550 governed.apply_token_configuration_item(change.clone());
551 assert_eq!(governed.minimum_pool_notes_for_outgoing(), 12);
552
553 let mut unpooled = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
555 unpooled.set_max_supply_change_rules(rules(AuthorizedActionTakers::ContractOwner));
556 assert!(!can_apply(&unpooled, owner));
557 let before = unpooled.clone();
558 unpooled.apply_token_configuration_item(change);
559 assert_eq!(unpooled, before);
560 }
561
562 #[test]
566 fn should_route_the_threshold_control_and_admin_items_to_their_own_rules() {
567 let owner = Identifier::from([1; 32]);
568 let heir = Identifier::from([3; 32]);
569 let groups = BTreeMap::new();
570 let can_apply = |configuration: &TokenConfiguration,
571 change: &TokenConfigurationChangeItem,
572 action_taker: Identifier| {
573 configuration.can_apply_token_configuration_item(
574 change,
575 &owner,
576 None,
577 &groups,
578 &ActionTaker::SingleIdentity(action_taker),
579 ActionGoal::ActionCompletion,
580 )
581 };
582 let threshold_rules = |configuration: &TokenConfiguration| {
583 let TokenConfiguration::V1(v1) = configuration else {
584 panic!("a pooled configuration is V1");
585 };
586 v1.minimum_pool_notes_for_outgoing_change_rules.clone()
587 };
588
589 let mut configuration = pooled();
590 let TokenConfiguration::V1(v1) = &mut configuration else {
591 panic!("a pooled configuration is V1");
592 };
593 v1.minimum_pool_notes_for_outgoing_change_rules =
594 ChangeControlRules::V0(ChangeControlRulesV0 {
595 authorized_to_make_change: AuthorizedActionTakers::NoOne,
596 admin_action_takers: AuthorizedActionTakers::ContractOwner,
597 changing_authorized_action_takers_to_no_one_allowed: false,
598 changing_admin_action_takers_to_no_one_allowed: false,
599 self_changing_admin_action_takers_allowed: true,
600 });
601 let max_supply_rules_before = configuration.max_supply_change_rules().clone();
602 let set_threshold = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(8);
603 let control = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(
604 AuthorizedActionTakers::Identity(heir),
605 );
606 let admin = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(
607 AuthorizedActionTakers::Identity(heir),
608 );
609
610 for item in [&control, &admin] {
611 assert_eq!(
612 configuration.authorized_action_takers_for_configuration_item(item),
613 AuthorizedActionTakers::ContractOwner
614 );
615 assert!(can_apply(&configuration, item, owner));
616 assert!(!can_apply(&configuration, item, heir));
617 }
618 assert!(!can_apply(&configuration, &set_threshold, owner));
619
620 configuration.apply_token_configuration_item(control);
621 let rules = threshold_rules(&configuration);
622 assert_eq!(
623 *rules.authorized_to_make_change_action_takers(),
624 AuthorizedActionTakers::Identity(heir)
625 );
626 assert_eq!(
627 *rules.admin_action_takers(),
628 AuthorizedActionTakers::ContractOwner
629 );
630 assert!(can_apply(&configuration, &set_threshold, heir));
631 assert!(!can_apply(&configuration, &set_threshold, owner));
632
633 configuration.apply_token_configuration_item(admin);
634 let rules = threshold_rules(&configuration);
635 assert_eq!(
636 *rules.authorized_to_make_change_action_takers(),
637 AuthorizedActionTakers::Identity(heir)
638 );
639 assert_eq!(
640 *rules.admin_action_takers(),
641 AuthorizedActionTakers::Identity(heir)
642 );
643 assert_eq!(
644 configuration.max_supply_change_rules(),
645 &max_supply_rules_before
646 );
647 }
648
649 #[test]
650 fn should_hand_every_other_item_of_a_pooled_token_to_its_v0_rules() {
651 let owner = Identifier::from([1; 32]);
652 let groups = BTreeMap::new();
653 let mut configuration = pooled();
654 configuration.set_max_supply_change_rules(rules(AuthorizedActionTakers::ContractOwner));
655 let change = TokenConfigurationChangeItem::MaxSupply(Some(5_000));
656 assert!(configuration.can_apply_token_configuration_item(
657 &change,
658 &owner,
659 None,
660 &groups,
661 &ActionTaker::SingleIdentity(owner),
662 ActionGoal::ActionCompletion,
663 ));
664 configuration.apply_token_configuration_item(change);
665 assert_eq!(configuration.max_supply(), Some(5_000));
666 assert!(configuration.has_shielded_pool());
667 }
668
669 #[test]
670 fn should_count_the_threshold_rules_among_the_tokens_rules_and_groups() {
671 let mut configuration = pooled();
672 let TokenConfiguration::V1(v1) = &mut configuration else {
673 panic!("a pooled configuration is V1");
674 };
675 v1.minimum_pool_notes_for_outgoing_change_rules = rules(AuthorizedActionTakers::Group(4));
676 let (group_positions, _) = configuration.all_used_group_positions();
677 assert!(group_positions.contains(&4));
678 assert!(configuration
679 .all_change_control_rules()
680 .iter()
681 .any(
682 |(name, rules)| *name == "minimum_pool_notes_for_outgoing_change_rules"
683 && *rules.authorized_to_make_change_action_takers()
684 == AuthorizedActionTakers::Group(4)
685 ));
686 }
687}
688
689#[cfg(all(
690 test,
691 feature = "json-conversion",
692 feature = "value-conversion",
693 feature = "serde-conversion"
694))]
695mod minimum_pool_notes_json_tests {
696 use super::*;
697 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
698 use crate::serialization::JsonConvertible;
699
700 #[test]
703 fn should_read_a_pooled_configuration_without_the_threshold_keys_as_none() {
704 let mut configuration =
705 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
706 configuration.set_has_shielded_pool(true);
707 let mut json = configuration.to_json().expect("to_json");
708 let object = json.as_object_mut().expect("configuration object");
709 assert!(object.remove("minimumPoolNotesForOutgoing").is_some());
710 assert!(object
711 .remove("minimumPoolNotesForOutgoingChangeRules")
712 .is_some());
713
714 let decoded = TokenConfiguration::from_json(json).expect("from_json");
715 assert_eq!(decoded, configuration);
716 assert_eq!(decoded.minimum_pool_notes_for_outgoing(), 0);
717 }
718}
719
720#[cfg(all(
721 test,
722 feature = "json-conversion",
723 feature = "value-conversion",
724 feature = "serde-conversion"
725))]
726mod unknown_configuration_key_tests {
727 use super::*;
728 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
729 use crate::serialization::{JsonConvertible, ValueConvertible};
730
731 fn v0_json_asking_for_a_pool() -> serde_json::Value {
734 let mut json = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
735 .to_json()
736 .expect("to_json");
737 json.as_object_mut()
738 .expect("configuration object")
739 .insert("hasShieldedPool".to_string(), serde_json::Value::Bool(true));
740 json
741 }
742
743 fn v0_value_asking_for_a_pool() -> platform_value::Value {
745 let mut value = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
746 .to_object()
747 .expect("to_object");
748 value.as_map_mut().expect("configuration map").push((
749 platform_value::Value::Text("hasShieldedPool".to_string()),
750 platform_value::Value::Bool(true),
751 ));
752 value
753 }
754
755 #[test]
756 fn should_refuse_a_pool_asked_for_at_format_version_0_rather_than_drop_it() {
757 let json = v0_json_asking_for_a_pool();
758
759 let mut without_the_request = json.clone();
763 without_the_request
764 .as_object_mut()
765 .expect("configuration object")
766 .remove("hasShieldedPool");
767 let control = TokenConfiguration::from_json(without_the_request)
768 .expect("the configuration decodes once the pool request is taken out");
769 assert_eq!(control.format_version(), 0);
770 assert!(!control.has_shielded_pool());
771
772 let error = TokenConfiguration::from_json(json).expect_err("the pool request is refused");
776 assert!(
777 error.to_string().contains("hasShieldedPool"),
778 "the refusal must name the key it refused, got {error}"
779 );
780 }
781
782 #[test]
783 fn should_refuse_a_pool_asked_for_at_format_version_0_on_the_value_wire_too() {
784 let value = v0_value_asking_for_a_pool();
785
786 let mut without_the_request = value.clone();
787 without_the_request
788 .as_map_mut()
789 .expect("configuration map")
790 .retain(|(key, _)| key.as_text() != Some("hasShieldedPool"));
791 let control = TokenConfiguration::from_object(without_the_request)
792 .expect("the configuration decodes once the pool request is taken out");
793 assert_eq!(control.format_version(), 0);
794 assert!(!control.has_shielded_pool());
795
796 let error =
797 TokenConfiguration::from_object(value).expect_err("the pool request is refused");
798 assert!(
799 error.to_string().contains("hasShieldedPool"),
800 "the refusal must name the key it refused, got {error}"
801 );
802 }
803
804 #[test]
807 fn should_still_decode_a_pooled_configuration_whose_v0_fields_are_flattened() {
808 let mut configuration =
809 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
810 configuration.set_has_shielded_pool(true);
811
812 let json = configuration.to_json().expect("to_json");
813 assert_eq!(
814 json.get("$formatVersion").and_then(|v| v.as_str()),
815 Some("1")
816 );
817 assert_eq!(
818 TokenConfiguration::from_json(json).expect("from_json"),
819 configuration
820 );
821
822 let value = configuration.to_object().expect("to_object");
823 assert_eq!(
824 TokenConfiguration::from_object(value).expect("from_object"),
825 configuration
826 );
827 }
828
829 #[test]
840 fn should_refuse_an_unrecognized_key_at_format_version_1() {
841 let mut configuration =
842 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
843 configuration.set_has_shielded_pool(true);
844
845 const UNRECOGNIZED_KEY: &str = "thisKeyIsInNoFormatVersion";
846
847 let mut json = configuration.to_json().expect("to_json");
848 json.as_object_mut()
849 .expect("configuration object")
850 .insert(UNRECOGNIZED_KEY.to_string(), serde_json::Value::Bool(true));
851 let error = TokenConfiguration::from_json(json)
852 .expect_err("an unrecognized key must be refused, not dropped")
853 .to_string();
854 assert!(
855 error.contains(UNRECOGNIZED_KEY),
856 "the refusal must name the key it refused, got {error}"
857 );
858
859 let mut value = configuration.to_object().expect("to_object");
860 value.as_map_mut().expect("configuration map").push((
861 platform_value::Value::Text(UNRECOGNIZED_KEY.to_string()),
862 platform_value::Value::Bool(true),
863 ));
864 let error = TokenConfiguration::from_object(value)
865 .expect_err("the contract ingest path must refuse it too")
866 .to_string();
867 assert!(
868 error.contains(UNRECOGNIZED_KEY),
869 "the refusal must name the key it refused, got {error}"
870 );
871
872 let clean = TokenConfiguration::from_json(configuration.to_json().expect("to_json"))
875 .expect("the configuration itself still decodes");
876 assert_eq!(clean, configuration);
877 assert_eq!(clean.format_version(), 1);
878 assert!(clean.has_shielded_pool());
879 }
880}