Skip to main content

dpp/data_contract/associated_token/token_configuration/
mod.rs

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    /// V0 plus the per-token shielded pool opt-in. Admitted from protocol version 14
46    /// (`token_versions.token_configuration_format`).
47    #[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    /// The `$formatVersion` this configuration is serialized with.
59    pub fn format_version(&self) -> u16 {
60        match self {
61            TokenConfiguration::V0(_) => 0,
62            TokenConfiguration::V1(_) => 1,
63        }
64    }
65
66    /// Whether the configuration uses anything that needs the unpaid pre-activation gate of
67    /// contract create and update: a format version above 0 or the `Tradeable` trade mode.
68    /// Software that predates either cannot decode the configuration, and the frozen contract
69    /// basic structure generations of protocol versions 9 to 13 do not look at either, so a
70    /// configuration that uses one is checked by `validate_token_configurations` before anything
71    /// else.
72    pub fn needs_activation_check(&self) -> bool {
73        self.format_version() > 0 || self.is_tradeable()
74    }
75
76    /// Whether holders may list the token for sale and make offers for it
77    /// (`marketplaceRules.tradeMode` `Tradeable`).
78    pub fn is_tradeable(&self) -> bool {
79        self.as_cow_v0()
80            .marketplace_rules
81            .trade_mode()
82            .is_tradeable()
83    }
84
85    /// Checks the configuration's format version against the bounds the platform version
86    /// admits (`dpp.contract_versions.token_versions.token_configuration_format`).
87    ///
88    /// A format above the bound is a consensus error, not a decode failure: nodes running
89    /// software that knows the newer variant must still refuse it until the protocol version
90    /// that introduces it activates, so a mixed-version network agrees.
91    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    /// A shielded pool makes freezing and confiscation unenforceable: shielded notes belong to
116    /// no identity account, so a holder who expects a freeze simply shields first. Rather than
117    /// let an issuer advertise controls that only cover transparent balances, a token with
118    /// `hasShieldedPool` must permanently disable `freezeRules`, `unfreezeRules` and
119    /// `destroyFrozenFundsRules`: no one may take the action and no one may administer the
120    /// rule, so no configuration update can ever switch them on. The token must also be
121    /// transferable: a spend inside the pool can pay any identity, so a non-transferable token
122    /// with a pool would change hands through shield and unshield. Checked on contract create
123    /// and update; both flags are immutable.
124    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    /// A tradeable token (`marketplaceRules.tradeMode` `Tradeable`) is admitted from protocol
159    /// version 15 (`TOKEN_MARKETPLACE_INITIAL_PROTOCOL_VERSION`): below it the mode is refused as
160    /// not active, the refusal software that predates the marketplace gives by failing to decode
161    /// it. A tradeable token must also be transferable, since filling a listing or an offer moves
162    /// the token from one identity to another. Checked on contract create and update.
163    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    /// The rules of [`Self::validate_marketplace_rules`] for `trade_mode` as this token's trade
176    /// mode: its own, or the one a `TokenConfigUpdate` proposes (`MarketplaceTradeMode`, the only
177    /// item that changes it; `transferable` is fixed at creation). Borrowing the proposed mode
178    /// spares building the proposed configuration.
179    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    /// The pool's outgoing notes threshold may not exceed
207    /// `max_token_pool_notes_for_outgoing`: a threshold the pool never reaches would refuse
208    /// every outflow and strand every shielded balance, irreversibly on a readonly contract.
209    /// Checked on contract create and update and on the configuration a `TokenConfigUpdate`
210    /// proposes.
211    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
224/// The bound on a token shielded pool's outgoing notes threshold: at most
225/// `max_token_pool_notes_for_outgoing`, refused as `KeyWrongBounds`. Shared by the token
226/// configuration validation and the `TokenConfigUpdate` that changes the threshold.
227pub 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
247/// Validates every token configuration of a contract for `platform_version`: the format
248/// version must be admitted, a pooled token's rules must be compatible with a pool and its
249/// outgoing notes threshold within bounds, and a tradeable token must be admitted and
250/// transferable. Returns the first error. Shared by the contract
251/// create and update basic structure generations and by the pre-activation gate, so the three
252/// cannot drift.
253pub 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        // u64::MAX > JS MAX_SAFE_INTEGER, so it should be serialized as a string
302        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    /// `default_most_restrictive` already populates ~25 inner fields with
325    /// non-default values (decimals=8, base_supply=100_000, etc.) — exactly
326    /// what we want for the round-trip structural check below.
327    fn fixture() -> TokenConfiguration {
328        TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive())
329    }
330
331    /// Tier 3: TokenConfiguration embeds ~25 fields, several of which are
332    /// themselves versioned enums (TokenConfigurationConvention,
333    /// ChangeControlRules x7, TokenKeepsHistoryRules, TokenDistributionRules,
334    /// TokenMarketplaceRules). An inline wire-shape literal would be 200+
335    /// lines and would re-test the nested types' own assertions. Instead we
336    /// assert only the envelope (top-level keys + `$formatVersion`) and trust
337    /// the nested types' tests for inner shape correctness.
338    #[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        // Envelope check: format version + top-level keys present.
344        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        // Same envelope-only check on the platform_value side.
385        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        // Nobody may change a threshold the issuer did not open to change.
577        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        // A token without a pool has no threshold, whoever asks and whatever its other rules.
640        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    /// The control item moves who may set the threshold and the admin item moves who
649    /// administers that, each under the threshold's own admin rule and neither touching the
650    /// other or any rule of the nested V0 configuration.
651    #[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    /// A pooled configuration written before the threshold existed, or by a client that leaves
787    /// it out, reads with no threshold and no one allowed to change it.
788    #[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    /// A V0 configuration's JSON with `hasShieldedPool` bolted on: what a caller writes when
818    /// they ask for a pool but leave the format version at 0.
819    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    /// The same fixture on the Value wire, which is the path a contract is ingested through.
830    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        // The control. Take the pool request back out and the very same configuration decodes,
846        // at format version 0 and without a pool. It is what makes the refusal below evidence
847        // about the pool request rather than about a fixture that stopped building a valid V0.
848        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        // A configuration asking for a pool must not decode as a token that can never have one,
859        // and the refusal has to name the key it refused: any other message means the decoder
860        // tripped over something else and the pool request was never the reason.
861        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    /// Asking for a pool moves the configuration to format version 1, whose V0 fields sit at the
891    /// top level of the wire through `serde(flatten)`. Both wires have to read that shape back.
892    #[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    /// A key no format version carries is refused at format version 1, as it is at 0.
916    ///
917    /// Pinned because the failure it prevents is invisible: a dropped key lets a caller's typo
918    /// reach the chain as a configuration quietly missing whatever they meant to set, and what
919    /// a token can do is fixed when it is created.
920    ///
921    /// The refusal has to hold on both wires. `TokenConfigurationV1` takes version 0's fields
922    /// through `serde(flatten)`, and an unknown key arriving through a flattened field is not
923    /// offered to the inner struct at all, so version 0's own refusal cannot reach it — the
924    /// attribute has to sit on the version that owns the flatten.
925    #[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        // The same configuration without the key still decodes, so the refusal is the key's
959        // doing and not a fixture that stopped building.
960        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    /// Disabling a pool used to downgrade every V1 configuration to V0, which would now drop
1011    /// the non-transferable flag along with the pool.
1012    #[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    /// Removing the pool of a non-transferable token keeps it at V1, so the threshold and its
1048    /// rules, which only apply to a pool, are cleared rather than left on a token without one,
1049    /// whichever of the two flags is set first.
1050    #[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    /// The threshold setter takes a threshold only on a token with a pool and always clears.
1079    #[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    /// A non-transferable token is V1 without a pool, so V1 alone no longer means a pool: the
1097    /// threshold items are refused on it even when its threshold rules name the actor.
1098    #[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    /// A format 1 configuration written without the key, such as a pooled token from before
1187    /// the flag existed, reads as transferable.
1188    #[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    /// A configuration update that makes a non-transferable token tradeable proposes the same
1285    /// configuration a contract update would, and is refused by the same check: whether the
1286    /// trade mode is the token's own or only proposed.
1287    #[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    /// Proposing the mode every token may have is never refused.
1312    #[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}