1use crate::consensus::basic::data_contract::{
2 NonTransferableTokenShieldedPoolError, NonTransferableTokenTradeableError,
3 TokenShieldedPoolIncompatibleRulesError,
4};
5use crate::consensus::basic::state_transition::StateTransitionNotActiveError;
6use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError;
7use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters;
8use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters;
9use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0;
10use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1;
11use crate::data_contract::associated_token::token_marketplace_rules::accessors::v0::TokenMarketplaceRulesV0Getters;
12use crate::data_contract::associated_token::token_marketplace_rules::v0::TokenTradeMode;
13use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers;
14use crate::data_contract::change_control_rules::ChangeControlRules;
15use crate::data_contract::errors::DataContractError;
16use crate::data_contract::TokenContractPosition;
17#[cfg(feature = "json-conversion")]
18use crate::serialization::JsonConvertible;
19#[cfg(feature = "value-conversion")]
20use crate::serialization::ValueConvertible;
21use crate::validation::SimpleConsensusValidationResult;
22use bincode::{Decode, DecodeUntrusted, Encode};
23use derive_more::From;
24use platform_version::version::feature_initial_protocol_versions::TOKEN_MARKETPLACE_INITIAL_PROTOCOL_VERSION;
25use platform_version::version::PlatformVersion;
26use serde::{Deserialize, Serialize};
27use std::borrow::Cow;
28use std::collections::BTreeMap;
29use std::fmt;
30
31pub mod accessors;
32mod methods;
33pub mod v0;
34pub mod v1;
35
36#[cfg_attr(feature = "json-conversion", derive(JsonConvertible))]
37#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
38#[derive(
39 Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq, Eq, From, DecodeUntrusted,
40)]
41#[serde(tag = "$formatVersion")]
42pub enum TokenConfiguration {
43 #[serde(rename = "0")]
44 V0(TokenConfigurationV0),
45 #[serde(rename = "1")]
48 V1(TokenConfigurationV1),
49}
50impl TokenConfiguration {
51 pub fn as_cow_v0(&self) -> Cow<'_, TokenConfigurationV0> {
52 match self {
53 TokenConfiguration::V0(v0) => Cow::Borrowed(v0),
54 TokenConfiguration::V1(v1) => Cow::Borrowed(&v1.base),
55 }
56 }
57
58 pub fn format_version(&self) -> u16 {
60 match self {
61 TokenConfiguration::V0(_) => 0,
62 TokenConfiguration::V1(_) => 1,
63 }
64 }
65
66 pub fn needs_activation_check(&self) -> bool {
73 self.format_version() > 0 || self.is_tradeable()
74 }
75
76 pub fn is_tradeable(&self) -> bool {
79 self.as_cow_v0()
80 .marketplace_rules
81 .trade_mode()
82 .is_tradeable()
83 }
84
85 pub fn validate_format_version(
92 &self,
93 platform_version: &PlatformVersion,
94 ) -> SimpleConsensusValidationResult {
95 let bounds = &platform_version
96 .dpp
97 .contract_versions
98 .token_versions
99 .token_configuration_format;
100 let format_version = self.format_version();
101 if format_version < bounds.min_version || format_version > bounds.max_version {
102 SimpleConsensusValidationResult::new_with_error(
103 UnsupportedVersionError::new(
104 format_version,
105 bounds.min_version,
106 bounds.max_version,
107 )
108 .into(),
109 )
110 } else {
111 SimpleConsensusValidationResult::new()
112 }
113 }
114
115 pub fn validate_shielded_pool_rules(
125 &self,
126 token_contract_position: TokenContractPosition,
127 ) -> SimpleConsensusValidationResult {
128 if !self.has_shielded_pool() {
129 return SimpleConsensusValidationResult::new();
130 }
131 if !self.is_transferable() {
132 return SimpleConsensusValidationResult::new_with_error(
133 NonTransferableTokenShieldedPoolError::new(token_contract_position).into(),
134 );
135 }
136 let rules: [(&ChangeControlRules, &str); 3] = [
137 (self.freeze_rules(), "freezeRules"),
138 (self.unfreeze_rules(), "unfreezeRules"),
139 (self.destroy_frozen_funds_rules(), "destroyFrozenFundsRules"),
140 ];
141 for (rule, name) in rules {
142 let disabled = *rule.authorized_to_make_change_action_takers()
143 == AuthorizedActionTakers::NoOne
144 && *rule.admin_action_takers() == AuthorizedActionTakers::NoOne;
145 if !disabled {
146 return SimpleConsensusValidationResult::new_with_error(
147 TokenShieldedPoolIncompatibleRulesError::new(
148 token_contract_position,
149 name.to_string(),
150 )
151 .into(),
152 );
153 }
154 }
155 SimpleConsensusValidationResult::new()
156 }
157
158 pub fn validate_marketplace_rules(
164 &self,
165 token_contract_position: TokenContractPosition,
166 platform_version: &PlatformVersion,
167 ) -> SimpleConsensusValidationResult {
168 self.validate_trade_mode(
169 self.as_cow_v0().marketplace_rules.trade_mode(),
170 token_contract_position,
171 platform_version,
172 )
173 }
174
175 pub fn validate_trade_mode(
180 &self,
181 trade_mode: &TokenTradeMode,
182 token_contract_position: TokenContractPosition,
183 platform_version: &PlatformVersion,
184 ) -> SimpleConsensusValidationResult {
185 if !trade_mode.is_tradeable() {
186 return SimpleConsensusValidationResult::new();
187 }
188 if platform_version.protocol_version < TOKEN_MARKETPLACE_INITIAL_PROTOCOL_VERSION {
189 return SimpleConsensusValidationResult::new_with_error(
190 StateTransitionNotActiveError::new(
191 "TokenTradeModeTradeable",
192 platform_version.protocol_version,
193 TOKEN_MARKETPLACE_INITIAL_PROTOCOL_VERSION,
194 )
195 .into(),
196 );
197 }
198 if !self.is_transferable() {
199 return SimpleConsensusValidationResult::new_with_error(
200 NonTransferableTokenTradeableError::new(token_contract_position).into(),
201 );
202 }
203 SimpleConsensusValidationResult::new()
204 }
205
206 pub fn validate_minimum_pool_notes_for_outgoing(
212 &self,
213 token_contract_position: TokenContractPosition,
214 platform_version: &PlatformVersion,
215 ) -> SimpleConsensusValidationResult {
216 validate_minimum_pool_notes_for_outgoing_bound(
217 self.minimum_pool_notes_for_outgoing(),
218 token_contract_position,
219 platform_version,
220 )
221 }
222}
223
224pub fn validate_minimum_pool_notes_for_outgoing_bound(
228 minimum_pool_notes: u64,
229 token_contract_position: TokenContractPosition,
230 platform_version: &PlatformVersion,
231) -> SimpleConsensusValidationResult {
232 let max_minimum_pool_notes = platform_version
233 .system_limits
234 .max_token_pool_notes_for_outgoing;
235 if minimum_pool_notes > max_minimum_pool_notes {
236 return SimpleConsensusValidationResult::new_with_error(
237 DataContractError::KeyWrongBounds(format!(
238 "token at position {token_contract_position}: minimumPoolNotesForOutgoing \
239 {minimum_pool_notes} is above the maximum {max_minimum_pool_notes}"
240 ))
241 .into(),
242 );
243 }
244 SimpleConsensusValidationResult::new()
245}
246
247pub fn validate_token_configurations(
254 tokens: &BTreeMap<TokenContractPosition, TokenConfiguration>,
255 platform_version: &PlatformVersion,
256) -> SimpleConsensusValidationResult {
257 for (position, configuration) in tokens {
258 let result = configuration.validate_format_version(platform_version);
259 if !result.is_valid() {
260 return result;
261 }
262 let result = configuration.validate_shielded_pool_rules(*position);
263 if !result.is_valid() {
264 return result;
265 }
266 let result =
267 configuration.validate_minimum_pool_notes_for_outgoing(*position, platform_version);
268 if !result.is_valid() {
269 return result;
270 }
271 let result = configuration.validate_marketplace_rules(*position, platform_version);
272 if !result.is_valid() {
273 return result;
274 }
275 }
276 SimpleConsensusValidationResult::new()
277}
278
279impl fmt::Display for TokenConfiguration {
280 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281 match self {
282 TokenConfiguration::V0(v0) => write!(f, "{}", v0),
283 TokenConfiguration::V1(v1) => write!(f, "{}", v1),
284 }
285 }
286}
287
288#[cfg(all(test, feature = "json-conversion"))]
289mod tests {
290 use super::*;
291 use crate::serialization::JsonConvertible;
292
293 #[test]
294 fn token_configuration_large_supply_json_round_trip() {
295 let mut config = TokenConfigurationV0::default_most_restrictive();
296 config.base_supply = u64::MAX;
297 let config = TokenConfiguration::V0(config);
298
299 let json = config.to_json().expect("to_json should succeed");
300
301 assert!(
303 json["baseSupply"].is_string(),
304 "baseSupply should be a string for large values, got: {:?}",
305 json["baseSupply"]
306 );
307 assert_eq!(json["baseSupply"].as_str().unwrap(), u64::MAX.to_string());
308
309 let restored = TokenConfiguration::from_json(json).expect("from_json should succeed");
310 assert_eq!(config, restored);
311 }
312}
313
314#[cfg(all(
315 test,
316 feature = "json-conversion",
317 feature = "value-conversion",
318 feature = "serde-conversion"
319))]
320mod json_convertible_tests {
321 use super::*;
322 use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0;
323
324 fn fixture() -> TokenConfiguration {
328 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
329 }
330
331 #[test]
339 fn json_round_trip_with_envelope_shape() {
340 use crate::serialization::JsonConvertible;
341 let original = fixture();
342 let json = original.to_json().expect("to_json");
343 assert_eq!(
345 json.get("$formatVersion").and_then(|v| v.as_str()),
346 Some("0")
347 );
348 for key in [
349 "conventions",
350 "conventionsChangeRules",
351 "baseSupply",
352 "maxSupply",
353 "keepsHistory",
354 "startAsPaused",
355 "allowTransferToFrozenBalance",
356 "maxSupplyChangeRules",
357 "distributionRules",
358 "marketplaceRules",
359 "manualMintingRules",
360 "manualBurningRules",
361 "freezeRules",
362 "unfreezeRules",
363 "destroyFrozenFundsRules",
364 "emergencyActionRules",
365 "mainControlGroup",
366 "mainControlGroupCanBeModified",
367 "description",
368 ] {
369 assert!(
370 json.get(key).is_some(),
371 "expected top-level key {:?} in JSON envelope",
372 key
373 );
374 }
375 let recovered = TokenConfiguration::from_json(json).expect("from_json");
376 assert_eq!(original, recovered);
377 }
378
379 #[test]
380 fn value_round_trip_with_envelope_shape() {
381 use crate::serialization::ValueConvertible;
382 let original = fixture();
383 let value = original.to_object().expect("to_object");
384 let map = value.as_map().expect("value is a Map");
386 let has_key = |k: &str| {
387 map.iter()
388 .any(|(key, _)| matches!(key, platform_value::Value::Text(t) if t == k))
389 };
390 assert!(has_key("$formatVersion"));
391 for key in [
392 "conventions",
393 "conventionsChangeRules",
394 "baseSupply",
395 "maxSupply",
396 "keepsHistory",
397 "startAsPaused",
398 "allowTransferToFrozenBalance",
399 "maxSupplyChangeRules",
400 "distributionRules",
401 "marketplaceRules",
402 "manualMintingRules",
403 "manualBurningRules",
404 "freezeRules",
405 "unfreezeRules",
406 "destroyFrozenFundsRules",
407 "emergencyActionRules",
408 "mainControlGroup",
409 "mainControlGroupCanBeModified",
410 "description",
411 ] {
412 assert!(
413 has_key(key),
414 "expected top-level key {:?} in Value envelope",
415 key
416 );
417 }
418 let recovered = TokenConfiguration::from_object(value).expect("from_object");
419 assert_eq!(original, recovered);
420 }
421
422 mod shielded_pool_rules {
423 use super::*;
424 use crate::consensus::basic::BasicError;
425 use crate::consensus::ConsensusError;
426 use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Setters;
427 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
428 use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0;
429 use crate::prelude::Identifier;
430
431 fn rules(
432 authorized: AuthorizedActionTakers,
433 admin: AuthorizedActionTakers,
434 ) -> ChangeControlRules {
435 ChangeControlRules::V0(ChangeControlRulesV0 {
436 authorized_to_make_change: authorized,
437 admin_action_takers: admin,
438 changing_authorized_action_takers_to_no_one_allowed: false,
439 changing_admin_action_takers_to_no_one_allowed: false,
440 self_changing_admin_action_takers_allowed: false,
441 })
442 }
443
444 fn pooled_configuration() -> TokenConfiguration {
445 let mut configuration =
446 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
447 configuration.set_has_shielded_pool(true);
448 configuration
449 }
450
451 fn rejected_rule(configuration: &TokenConfiguration) -> Option<String> {
452 let result = configuration.validate_shielded_pool_rules(3);
453 match result.errors.as_slice() {
454 [] => None,
455 [ConsensusError::BasicError(
456 BasicError::TokenShieldedPoolIncompatibleRulesError(error),
457 )] => {
458 assert_eq!(error.token_contract_position(), 3);
459 Some(error.rule().to_string())
460 }
461 other => panic!("unexpected errors: {other:?}"),
462 }
463 }
464
465 #[test]
466 fn most_restrictive_defaults_are_compatible_with_a_pool() {
467 assert_eq!(rejected_rule(&pooled_configuration()), None);
468 }
469
470 #[test]
471 fn a_token_without_a_pool_may_keep_freeze_rules() {
472 let mut configuration =
473 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
474 configuration.set_freeze_rules(rules(
475 AuthorizedActionTakers::ContractOwner,
476 AuthorizedActionTakers::ContractOwner,
477 ));
478 assert_eq!(rejected_rule(&configuration), None);
479 }
480
481 #[test]
482 fn a_pooled_token_rejects_an_authorized_freezer() {
483 let mut configuration = pooled_configuration();
484 configuration.set_freeze_rules(rules(
485 AuthorizedActionTakers::ContractOwner,
486 AuthorizedActionTakers::NoOne,
487 ));
488 assert_eq!(
489 rejected_rule(&configuration),
490 Some("freezeRules".to_string())
491 );
492 }
493
494 #[test]
495 fn a_pooled_token_rejects_an_admin_who_could_enable_unfreezing_later() {
496 let mut configuration = pooled_configuration();
497 configuration.set_unfreeze_rules(rules(
498 AuthorizedActionTakers::NoOne,
499 AuthorizedActionTakers::MainGroup,
500 ));
501 assert_eq!(
502 rejected_rule(&configuration),
503 Some("unfreezeRules".to_string())
504 );
505 }
506
507 #[test]
508 fn a_pooled_token_rejects_frozen_funds_destruction() {
509 let mut configuration = pooled_configuration();
510 configuration.set_destroy_frozen_funds_rules(rules(
511 AuthorizedActionTakers::Identity(Identifier::from([1u8; 32])),
512 AuthorizedActionTakers::NoOne,
513 ));
514 assert_eq!(
515 rejected_rule(&configuration),
516 Some("destroyFrozenFundsRules".to_string())
517 );
518 }
519 }
520}
521
522#[cfg(test)]
523mod minimum_pool_notes_tests {
524 use super::*;
525 use crate::consensus::basic::BasicError;
526 use crate::consensus::codes::ErrorWithCode;
527 use crate::consensus::ConsensusError;
528 use crate::data_contract::associated_token::token_configuration::accessors::v0::{
529 TokenConfigurationV0Getters, TokenConfigurationV0Setters,
530 };
531 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
532 use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
533 use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0;
534 use crate::group::action_taker::{ActionGoal, ActionTaker};
535 use crate::prelude::Identifier;
536
537 fn pooled() -> TokenConfiguration {
538 let mut configuration =
539 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
540 configuration.set_has_shielded_pool(true);
541 configuration
542 }
543
544 fn pooled_with_threshold(minimum_pool_notes: u64) -> TokenConfiguration {
545 let mut configuration = pooled();
546 let TokenConfiguration::V1(v1) = &mut configuration else {
547 panic!("a pooled configuration is V1");
548 };
549 v1.minimum_pool_notes_for_outgoing = Some(minimum_pool_notes);
550 configuration
551 }
552
553 fn rules(authorized: AuthorizedActionTakers) -> ChangeControlRules {
554 ChangeControlRules::V0(ChangeControlRulesV0 {
555 authorized_to_make_change: authorized,
556 admin_action_takers: authorized,
557 changing_authorized_action_takers_to_no_one_allowed: false,
558 changing_admin_action_takers_to_no_one_allowed: false,
559 self_changing_admin_action_takers_allowed: false,
560 })
561 }
562
563 #[test]
564 fn should_read_a_configuration_without_a_threshold_as_zero() {
565 assert_eq!(
566 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
567 .minimum_pool_notes_for_outgoing(),
568 0
569 );
570 let configuration = pooled();
571 let TokenConfiguration::V1(v1) = &configuration else {
572 panic!("a pooled configuration is V1");
573 };
574 assert_eq!(v1.minimum_pool_notes_for_outgoing, None);
575 assert_eq!(configuration.minimum_pool_notes_for_outgoing(), 0);
576 assert_eq!(
578 configuration.authorized_action_takers_for_configuration_item(
579 &TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(1)
580 ),
581 AuthorizedActionTakers::NoOne
582 );
583 }
584
585 #[test]
586 fn should_bound_the_threshold_by_the_system_limit_with_error_10241() {
587 let platform_version = PlatformVersion::latest();
588 let max = platform_version
589 .system_limits
590 .max_token_pool_notes_for_outgoing;
591 assert_eq!(max, 250);
592
593 let at_the_limit = BTreeMap::from([(0, pooled_with_threshold(max))]);
594 let result = validate_token_configurations(&at_the_limit, platform_version);
595 assert!(result.is_valid(), "unexpected errors: {:?}", result.errors);
596
597 let over_the_limit = BTreeMap::from([(3, pooled_with_threshold(max + 1))]);
598 let result = validate_token_configurations(&over_the_limit, platform_version);
599 assert_matches::assert_matches!(
600 result.errors.as_slice(),
601 [error @ ConsensusError::BasicError(BasicError::ContractError(
602 DataContractError::KeyWrongBounds(message)
603 ))] if error.code() == 10241 && message.contains("position 3")
604 );
605 }
606
607 #[test]
608 fn should_govern_the_threshold_by_its_own_rules_on_a_pooled_token_only() {
609 let owner = Identifier::from([1; 32]);
610 let other = Identifier::from([2; 32]);
611 let groups = BTreeMap::new();
612 let change = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(12);
613 let can_apply = |configuration: &TokenConfiguration, action_taker: Identifier| {
614 configuration.can_apply_token_configuration_item(
615 &change,
616 &owner,
617 None,
618 &groups,
619 &ActionTaker::SingleIdentity(action_taker),
620 ActionGoal::ActionCompletion,
621 )
622 };
623
624 let mut governed = pooled();
625 let TokenConfiguration::V1(v1) = &mut governed else {
626 panic!("a pooled configuration is V1");
627 };
628 v1.minimum_pool_notes_for_outgoing_change_rules =
629 rules(AuthorizedActionTakers::ContractOwner);
630 assert!(can_apply(&governed, owner));
631 assert!(!can_apply(&governed, other));
632 assert_eq!(
633 governed.controlling_action_takers_for_configuration_item(&change),
634 AuthorizedActionTakers::ContractOwner
635 );
636 governed.apply_token_configuration_item(change.clone());
637 assert_eq!(governed.minimum_pool_notes_for_outgoing(), 12);
638
639 let mut unpooled = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
641 unpooled.set_max_supply_change_rules(rules(AuthorizedActionTakers::ContractOwner));
642 assert!(!can_apply(&unpooled, owner));
643 let before = unpooled.clone();
644 unpooled.apply_token_configuration_item(change);
645 assert_eq!(unpooled, before);
646 }
647
648 #[test]
652 fn should_route_the_threshold_control_and_admin_items_to_their_own_rules() {
653 let owner = Identifier::from([1; 32]);
654 let heir = Identifier::from([3; 32]);
655 let groups = BTreeMap::new();
656 let can_apply = |configuration: &TokenConfiguration,
657 change: &TokenConfigurationChangeItem,
658 action_taker: Identifier| {
659 configuration.can_apply_token_configuration_item(
660 change,
661 &owner,
662 None,
663 &groups,
664 &ActionTaker::SingleIdentity(action_taker),
665 ActionGoal::ActionCompletion,
666 )
667 };
668 let threshold_rules = |configuration: &TokenConfiguration| {
669 let TokenConfiguration::V1(v1) = configuration else {
670 panic!("a pooled configuration is V1");
671 };
672 v1.minimum_pool_notes_for_outgoing_change_rules.clone()
673 };
674
675 let mut configuration = pooled();
676 let TokenConfiguration::V1(v1) = &mut configuration else {
677 panic!("a pooled configuration is V1");
678 };
679 v1.minimum_pool_notes_for_outgoing_change_rules =
680 ChangeControlRules::V0(ChangeControlRulesV0 {
681 authorized_to_make_change: AuthorizedActionTakers::NoOne,
682 admin_action_takers: AuthorizedActionTakers::ContractOwner,
683 changing_authorized_action_takers_to_no_one_allowed: false,
684 changing_admin_action_takers_to_no_one_allowed: false,
685 self_changing_admin_action_takers_allowed: true,
686 });
687 let max_supply_rules_before = configuration.max_supply_change_rules().clone();
688 let set_threshold = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(8);
689 let control = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(
690 AuthorizedActionTakers::Identity(heir),
691 );
692 let admin = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(
693 AuthorizedActionTakers::Identity(heir),
694 );
695
696 for item in [&control, &admin] {
697 assert_eq!(
698 configuration.authorized_action_takers_for_configuration_item(item),
699 AuthorizedActionTakers::ContractOwner
700 );
701 assert!(can_apply(&configuration, item, owner));
702 assert!(!can_apply(&configuration, item, heir));
703 }
704 assert!(!can_apply(&configuration, &set_threshold, owner));
705
706 configuration.apply_token_configuration_item(control);
707 let rules = threshold_rules(&configuration);
708 assert_eq!(
709 *rules.authorized_to_make_change_action_takers(),
710 AuthorizedActionTakers::Identity(heir)
711 );
712 assert_eq!(
713 *rules.admin_action_takers(),
714 AuthorizedActionTakers::ContractOwner
715 );
716 assert!(can_apply(&configuration, &set_threshold, heir));
717 assert!(!can_apply(&configuration, &set_threshold, owner));
718
719 configuration.apply_token_configuration_item(admin);
720 let rules = threshold_rules(&configuration);
721 assert_eq!(
722 *rules.authorized_to_make_change_action_takers(),
723 AuthorizedActionTakers::Identity(heir)
724 );
725 assert_eq!(
726 *rules.admin_action_takers(),
727 AuthorizedActionTakers::Identity(heir)
728 );
729 assert_eq!(
730 configuration.max_supply_change_rules(),
731 &max_supply_rules_before
732 );
733 }
734
735 #[test]
736 fn should_hand_every_other_item_of_a_pooled_token_to_its_v0_rules() {
737 let owner = Identifier::from([1; 32]);
738 let groups = BTreeMap::new();
739 let mut configuration = pooled();
740 configuration.set_max_supply_change_rules(rules(AuthorizedActionTakers::ContractOwner));
741 let change = TokenConfigurationChangeItem::MaxSupply(Some(5_000));
742 assert!(configuration.can_apply_token_configuration_item(
743 &change,
744 &owner,
745 None,
746 &groups,
747 &ActionTaker::SingleIdentity(owner),
748 ActionGoal::ActionCompletion,
749 ));
750 configuration.apply_token_configuration_item(change);
751 assert_eq!(configuration.max_supply(), Some(5_000));
752 assert!(configuration.has_shielded_pool());
753 }
754
755 #[test]
756 fn should_count_the_threshold_rules_among_the_tokens_rules_and_groups() {
757 let mut configuration = pooled();
758 let TokenConfiguration::V1(v1) = &mut configuration else {
759 panic!("a pooled configuration is V1");
760 };
761 v1.minimum_pool_notes_for_outgoing_change_rules = rules(AuthorizedActionTakers::Group(4));
762 let (group_positions, _) = configuration.all_used_group_positions();
763 assert!(group_positions.contains(&4));
764 assert!(configuration
765 .all_change_control_rules()
766 .iter()
767 .any(
768 |(name, rules)| *name == "minimum_pool_notes_for_outgoing_change_rules"
769 && *rules.authorized_to_make_change_action_takers()
770 == AuthorizedActionTakers::Group(4)
771 ));
772 }
773}
774
775#[cfg(all(
776 test,
777 feature = "json-conversion",
778 feature = "value-conversion",
779 feature = "serde-conversion"
780))]
781mod minimum_pool_notes_json_tests {
782 use super::*;
783 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
784 use crate::serialization::JsonConvertible;
785
786 #[test]
789 fn should_read_a_pooled_configuration_without_the_threshold_keys_as_none() {
790 let mut configuration =
791 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
792 configuration.set_has_shielded_pool(true);
793 let mut json = configuration.to_json().expect("to_json");
794 let object = json.as_object_mut().expect("configuration object");
795 assert!(object.remove("minimumPoolNotesForOutgoing").is_some());
796 assert!(object
797 .remove("minimumPoolNotesForOutgoingChangeRules")
798 .is_some());
799
800 let decoded = TokenConfiguration::from_json(json).expect("from_json");
801 assert_eq!(decoded, configuration);
802 assert_eq!(decoded.minimum_pool_notes_for_outgoing(), 0);
803 }
804}
805
806#[cfg(all(
807 test,
808 feature = "json-conversion",
809 feature = "value-conversion",
810 feature = "serde-conversion"
811))]
812mod unknown_configuration_key_tests {
813 use super::*;
814 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
815 use crate::serialization::{JsonConvertible, ValueConvertible};
816
817 fn v0_json_asking_for_a_pool() -> serde_json::Value {
820 let mut json = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
821 .to_json()
822 .expect("to_json");
823 json.as_object_mut()
824 .expect("configuration object")
825 .insert("hasShieldedPool".to_string(), serde_json::Value::Bool(true));
826 json
827 }
828
829 fn v0_value_asking_for_a_pool() -> platform_value::Value {
831 let mut value = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
832 .to_object()
833 .expect("to_object");
834 value.as_map_mut().expect("configuration map").push((
835 platform_value::Value::Text("hasShieldedPool".to_string()),
836 platform_value::Value::Bool(true),
837 ));
838 value
839 }
840
841 #[test]
842 fn should_refuse_a_pool_asked_for_at_format_version_0_rather_than_drop_it() {
843 let json = v0_json_asking_for_a_pool();
844
845 let mut without_the_request = json.clone();
849 without_the_request
850 .as_object_mut()
851 .expect("configuration object")
852 .remove("hasShieldedPool");
853 let control = TokenConfiguration::from_json(without_the_request)
854 .expect("the configuration decodes once the pool request is taken out");
855 assert_eq!(control.format_version(), 0);
856 assert!(!control.has_shielded_pool());
857
858 let error = TokenConfiguration::from_json(json).expect_err("the pool request is refused");
862 assert!(
863 error.to_string().contains("hasShieldedPool"),
864 "the refusal must name the key it refused, got {error}"
865 );
866 }
867
868 #[test]
869 fn should_refuse_a_pool_asked_for_at_format_version_0_on_the_value_wire_too() {
870 let value = v0_value_asking_for_a_pool();
871
872 let mut without_the_request = value.clone();
873 without_the_request
874 .as_map_mut()
875 .expect("configuration map")
876 .retain(|(key, _)| key.as_text() != Some("hasShieldedPool"));
877 let control = TokenConfiguration::from_object(without_the_request)
878 .expect("the configuration decodes once the pool request is taken out");
879 assert_eq!(control.format_version(), 0);
880 assert!(!control.has_shielded_pool());
881
882 let error =
883 TokenConfiguration::from_object(value).expect_err("the pool request is refused");
884 assert!(
885 error.to_string().contains("hasShieldedPool"),
886 "the refusal must name the key it refused, got {error}"
887 );
888 }
889
890 #[test]
893 fn should_still_decode_a_pooled_configuration_whose_v0_fields_are_flattened() {
894 let mut configuration =
895 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
896 configuration.set_has_shielded_pool(true);
897
898 let json = configuration.to_json().expect("to_json");
899 assert_eq!(
900 json.get("$formatVersion").and_then(|v| v.as_str()),
901 Some("1")
902 );
903 assert_eq!(
904 TokenConfiguration::from_json(json).expect("from_json"),
905 configuration
906 );
907
908 let value = configuration.to_object().expect("to_object");
909 assert_eq!(
910 TokenConfiguration::from_object(value).expect("from_object"),
911 configuration
912 );
913 }
914
915 #[test]
926 fn should_refuse_an_unrecognized_key_at_format_version_1() {
927 let mut configuration =
928 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
929 configuration.set_has_shielded_pool(true);
930
931 const UNRECOGNIZED_KEY: &str = "thisKeyIsInNoFormatVersion";
932
933 let mut json = configuration.to_json().expect("to_json");
934 json.as_object_mut()
935 .expect("configuration object")
936 .insert(UNRECOGNIZED_KEY.to_string(), serde_json::Value::Bool(true));
937 let error = TokenConfiguration::from_json(json)
938 .expect_err("an unrecognized key must be refused, not dropped")
939 .to_string();
940 assert!(
941 error.contains(UNRECOGNIZED_KEY),
942 "the refusal must name the key it refused, got {error}"
943 );
944
945 let mut value = configuration.to_object().expect("to_object");
946 value.as_map_mut().expect("configuration map").push((
947 platform_value::Value::Text(UNRECOGNIZED_KEY.to_string()),
948 platform_value::Value::Bool(true),
949 ));
950 let error = TokenConfiguration::from_object(value)
951 .expect_err("the contract ingest path must refuse it too")
952 .to_string();
953 assert!(
954 error.contains(UNRECOGNIZED_KEY),
955 "the refusal must name the key it refused, got {error}"
956 );
957
958 let clean = TokenConfiguration::from_json(configuration.to_json().expect("to_json"))
961 .expect("the configuration itself still decodes");
962 assert_eq!(clean, configuration);
963 assert_eq!(clean.format_version(), 1);
964 assert!(clean.has_shielded_pool());
965 }
966}
967
968#[cfg(test)]
969mod non_transferable_tests {
970 use super::*;
971 use crate::consensus::basic::BasicError;
972 use crate::consensus::codes::ErrorWithCode;
973 use crate::consensus::ConsensusError;
974 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
975 use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
976 use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0;
977 use crate::group::action_taker::{ActionGoal, ActionTaker};
978 use crate::prelude::Identifier;
979
980 fn transferable() -> TokenConfiguration {
981 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
982 }
983
984 pub(super) fn non_transferable() -> TokenConfiguration {
985 let mut configuration = transferable();
986 configuration.set_transferable(false);
987 configuration
988 }
989
990 #[test]
991 fn should_read_every_format_version_0_configuration_as_transferable() {
992 assert!(transferable().is_transferable());
993 }
994
995 #[test]
996 fn should_make_a_non_transferable_token_a_format_version_1_configuration_without_a_pool() {
997 let configuration = non_transferable();
998 assert_eq!(configuration.format_version(), 1);
999 assert!(!configuration.is_transferable());
1000 assert!(!configuration.has_shielded_pool());
1001 }
1002
1003 #[test]
1004 fn should_return_to_format_version_0_when_made_transferable_again() {
1005 let mut configuration = non_transferable();
1006 configuration.set_transferable(true);
1007 assert_eq!(configuration, transferable());
1008 }
1009
1010 #[test]
1013 fn should_keep_a_non_transferable_token_non_transferable_when_its_pool_is_removed() {
1014 let mut configuration = non_transferable();
1015 configuration.set_has_shielded_pool(true);
1016 configuration.set_has_shielded_pool(false);
1017 assert_eq!(configuration, non_transferable());
1018 }
1019
1020 #[test]
1021 fn should_refuse_a_shielded_pool_on_a_non_transferable_token_with_error_10279() {
1022 let mut pooled = transferable();
1023 pooled.set_has_shielded_pool(true);
1024 assert!(pooled.validate_shielded_pool_rules(2).is_valid());
1025
1026 pooled.set_transferable(false);
1027 let result = pooled.validate_shielded_pool_rules(2);
1028 match result.errors.as_slice() {
1029 [error @ ConsensusError::BasicError(
1030 BasicError::NonTransferableTokenShieldedPoolError(inner),
1031 )] => {
1032 assert_eq!(inner.token_contract_position(), 2);
1033 assert_eq!(error.code(), 10279);
1034 }
1035 other => panic!("expected NonTransferableTokenShieldedPoolError, got {other:?}"),
1036 }
1037 }
1038
1039 #[test]
1040 fn should_accept_a_non_transferable_token_without_a_pool() {
1041 let configurations = BTreeMap::from([(0, non_transferable())]);
1042 assert!(
1043 validate_token_configurations(&configurations, PlatformVersion::latest()).is_valid()
1044 );
1045 }
1046
1047 #[test]
1051 fn should_clear_the_pool_threshold_when_a_non_transferable_token_loses_its_pool() {
1052 let pooled_with_threshold = || {
1053 let mut configuration = transferable();
1054 configuration.set_has_shielded_pool(true);
1055 assert!(configuration.set_minimum_pool_notes_for_outgoing(Some(9)));
1056 let TokenConfiguration::V1(v1) = &mut configuration else {
1057 panic!("a pooled configuration is V1");
1058 };
1059 v1.minimum_pool_notes_for_outgoing_change_rules
1060 .set_authorized_to_make_change_action_takers(AuthorizedActionTakers::ContractOwner);
1061 configuration
1062 };
1063
1064 let mut transferable_first = pooled_with_threshold();
1065 transferable_first.set_transferable(false);
1066 transferable_first.set_has_shielded_pool(false);
1067
1068 let mut pool_first = pooled_with_threshold();
1069 pool_first.set_has_shielded_pool(false);
1070 pool_first.set_transferable(false);
1071
1072 for configuration in [transferable_first, pool_first] {
1073 assert_eq!(configuration, non_transferable());
1074 assert_eq!(configuration.minimum_pool_notes_for_outgoing(), 0);
1075 }
1076 }
1077
1078 #[test]
1080 fn should_set_a_pool_threshold_only_on_a_token_with_a_pool() {
1081 let mut configuration = non_transferable();
1082 assert!(!configuration.set_minimum_pool_notes_for_outgoing(Some(5)));
1083 assert_eq!(configuration, non_transferable());
1084 assert!(configuration.set_minimum_pool_notes_for_outgoing(None));
1085
1086 let mut unpooled = transferable();
1087 assert!(!unpooled.set_minimum_pool_notes_for_outgoing(Some(5)));
1088 assert!(unpooled.set_minimum_pool_notes_for_outgoing(None));
1089 assert_eq!(unpooled, transferable());
1090
1091 configuration.set_has_shielded_pool(true);
1092 assert!(configuration.set_minimum_pool_notes_for_outgoing(Some(5)));
1093 assert_eq!(configuration.minimum_pool_notes_for_outgoing(), 5);
1094 }
1095
1096 #[test]
1099 fn should_refuse_the_pool_threshold_items_on_a_token_without_a_pool() {
1100 let owner = Identifier::from([1; 32]);
1101 let mut configuration = non_transferable();
1102 let TokenConfiguration::V1(v1) = &mut configuration else {
1103 panic!("a non-transferable configuration is V1");
1104 };
1105 v1.minimum_pool_notes_for_outgoing_change_rules =
1106 ChangeControlRules::V0(ChangeControlRulesV0 {
1107 authorized_to_make_change: AuthorizedActionTakers::ContractOwner,
1108 admin_action_takers: AuthorizedActionTakers::ContractOwner,
1109 changing_authorized_action_takers_to_no_one_allowed: true,
1110 changing_admin_action_takers_to_no_one_allowed: true,
1111 self_changing_admin_action_takers_allowed: true,
1112 });
1113
1114 for change in [
1115 TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(10),
1116 TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(
1117 AuthorizedActionTakers::NoOne,
1118 ),
1119 TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(
1120 AuthorizedActionTakers::NoOne,
1121 ),
1122 ] {
1123 assert!(
1124 !configuration.can_apply_token_configuration_item(
1125 &change,
1126 &owner,
1127 None,
1128 &BTreeMap::new(),
1129 &ActionTaker::SingleIdentity(owner),
1130 ActionGoal::ActionCompletion,
1131 ),
1132 "{change} must be refused on a token without a pool"
1133 );
1134 }
1135 }
1136
1137 #[test]
1138 fn should_round_trip_a_non_transferable_configuration_through_bincode() {
1139 let configuration = non_transferable();
1140 let bytes =
1141 bincode::encode_to_vec(&configuration, bincode::config::standard()).expect("encode");
1142 let (decoded, read): (TokenConfiguration, usize) =
1143 bincode::decode_from_slice(&bytes, bincode::config::standard()).expect("decode");
1144 assert_eq!(read, bytes.len());
1145 assert_eq!(decoded, configuration);
1146 }
1147}
1148
1149#[cfg(all(
1150 test,
1151 feature = "json-conversion",
1152 feature = "value-conversion",
1153 feature = "serde-conversion"
1154))]
1155mod non_transferable_json_tests {
1156 use super::non_transferable_tests::non_transferable;
1157 use super::*;
1158 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
1159 use crate::serialization::{JsonConvertible, ValueConvertible};
1160
1161 #[test]
1162 fn should_write_transferable_false_and_read_it_back_on_both_wires() {
1163 let configuration = non_transferable();
1164
1165 let json = configuration.to_json().expect("to_json");
1166 assert_eq!(
1167 json.get("$formatVersion").and_then(|v| v.as_str()),
1168 Some("1")
1169 );
1170 assert_eq!(
1171 json.get("transferable"),
1172 Some(&serde_json::Value::Bool(false))
1173 );
1174 assert_eq!(
1175 TokenConfiguration::from_json(json).expect("from_json"),
1176 configuration
1177 );
1178
1179 let value = configuration.to_object().expect("to_object");
1180 assert_eq!(
1181 TokenConfiguration::from_object(value).expect("from_object"),
1182 configuration
1183 );
1184 }
1185
1186 #[test]
1189 fn should_read_a_format_version_1_configuration_without_the_key_as_transferable() {
1190 let mut pooled = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
1191 pooled.set_has_shielded_pool(true);
1192 let mut json = pooled.to_json().expect("to_json");
1193 assert!(json
1194 .as_object_mut()
1195 .expect("configuration object")
1196 .remove("transferable")
1197 .is_some());
1198
1199 let decoded = TokenConfiguration::from_json(json).expect("from_json");
1200 assert!(decoded.is_transferable());
1201 assert_eq!(decoded, pooled);
1202 }
1203}
1204
1205#[cfg(test)]
1206mod marketplace_rules_tests {
1207 use super::*;
1208 use crate::consensus::basic::BasicError;
1209 use crate::consensus::codes::ErrorWithCode;
1210 use crate::consensus::ConsensusError;
1211 use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters;
1212 use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
1213 use crate::data_contract::associated_token::token_marketplace_rules::accessors::v0::TokenMarketplaceRulesV0Setters;
1214 use crate::data_contract::associated_token::token_marketplace_rules::v0::TokenTradeMode;
1215
1216 fn not_tradeable() -> TokenConfiguration {
1217 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
1218 }
1219
1220 fn tradeable() -> TokenConfiguration {
1221 let mut configuration = TokenConfigurationV0::default_most_restrictive();
1222 configuration
1223 .marketplace_rules
1224 .set_trade_mode(TokenTradeMode::Tradeable);
1225 TokenConfiguration::V0(configuration)
1226 }
1227
1228 fn protocol_version_14() -> &'static PlatformVersion {
1229 PlatformVersion::get(14).expect("expected protocol version 14")
1230 }
1231
1232 #[test]
1233 fn should_accept_a_token_that_is_not_tradeable_at_every_protocol_version() {
1234 let configurations = BTreeMap::from([(0, not_tradeable())]);
1235 for platform_version in [protocol_version_14(), PlatformVersion::latest()] {
1236 assert!(validate_token_configurations(&configurations, platform_version).is_valid());
1237 }
1238 }
1239
1240 #[test]
1241 fn should_refuse_a_tradeable_token_as_not_active_before_protocol_version_15() {
1242 let configurations = BTreeMap::from([(1, tradeable())]);
1243 let result = validate_token_configurations(&configurations, protocol_version_14());
1244 match result.errors.as_slice() {
1245 [error
1246 @ ConsensusError::BasicError(BasicError::StateTransitionNotActiveError(inner))] => {
1247 assert_eq!(inner.state_transition_type(), "TokenTradeModeTradeable");
1248 assert_eq!(inner.current_protocol_version(), 14);
1249 assert_eq!(
1250 inner.required_protocol_version(),
1251 TOKEN_MARKETPLACE_INITIAL_PROTOCOL_VERSION
1252 );
1253 assert_eq!(error.code(), 10603);
1254 }
1255 other => panic!("expected StateTransitionNotActiveError, got {other:?}"),
1256 }
1257 }
1258
1259 #[test]
1260 fn should_accept_a_tradeable_transferable_token_from_protocol_version_15() {
1261 let configurations = BTreeMap::from([(0, tradeable())]);
1262 assert!(
1263 validate_token_configurations(&configurations, PlatformVersion::latest()).is_valid()
1264 );
1265 }
1266
1267 #[test]
1268 fn should_refuse_a_tradeable_non_transferable_token_with_error_10281() {
1269 let mut configuration = tradeable();
1270 configuration.set_transferable(false);
1271 let configurations = BTreeMap::from([(3, configuration)]);
1272 let result = validate_token_configurations(&configurations, PlatformVersion::latest());
1273 match result.errors.as_slice() {
1274 [error @ ConsensusError::BasicError(BasicError::NonTransferableTokenTradeableError(
1275 inner,
1276 ))] => {
1277 assert_eq!(inner.token_contract_position(), 3);
1278 assert_eq!(error.code(), 10281);
1279 }
1280 other => panic!("expected NonTransferableTokenTradeableError, got {other:?}"),
1281 }
1282 }
1283
1284 #[test]
1288 fn should_refuse_making_a_non_transferable_token_tradeable_through_its_configuration_item() {
1289 let mut configuration = not_tradeable();
1290 configuration.set_transferable(false);
1291
1292 let proposed = configuration.validate_trade_mode(
1293 &TokenTradeMode::Tradeable,
1294 0,
1295 PlatformVersion::latest(),
1296 );
1297 assert!(matches!(
1298 proposed.errors.as_slice(),
1299 [ConsensusError::BasicError(
1300 BasicError::NonTransferableTokenTradeableError(_)
1301 )]
1302 ));
1303
1304 configuration.apply_token_configuration_item(
1305 TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::Tradeable),
1306 );
1307 let applied = configuration.validate_marketplace_rules(0, PlatformVersion::latest());
1308 assert_eq!(applied.errors, proposed.errors);
1309 }
1310
1311 #[test]
1313 fn should_accept_proposing_not_tradeable_for_a_non_transferable_token() {
1314 let mut configuration = not_tradeable();
1315 configuration.set_transferable(false);
1316 assert!(configuration
1317 .validate_trade_mode(&TokenTradeMode::NotTradeable, 0, protocol_version_14())
1318 .is_valid());
1319 }
1320}