Skip to main content

dpp/data_contract/associated_token/
token_configuration_item.rs

1use crate::balances::credits::TokenAmount;
2use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention;
3use crate::data_contract::associated_token::token_marketplace_rules::v0::TokenTradeMode;
4use crate::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution;
5use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers;
6use crate::data_contract::GroupContractPosition;
7use crate::ProtocolError;
8use bincode::Encode;
9use platform_serialization::de::Decode;
10use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize};
11use platform_value::Identifier;
12#[cfg(feature = "serde-conversion")]
13use serde::{Deserialize, Serialize};
14use std::fmt;
15
16#[derive(
17    Debug,
18    Clone,
19    Default,
20    PartialOrd,
21    Encode,
22    Decode,
23    PlatformSerialize,
24    PlatformDeserialize,
25    PartialEq,
26    Eq,
27)]
28#[cfg_attr(
29    feature = "serde-conversion",
30    derive(Serialize, Deserialize),
31    serde(
32        into = "TokenConfigurationChangeItemRepr",
33        from = "TokenConfigurationChangeItemRepr"
34    )
35)]
36pub enum TokenConfigurationChangeItem {
37    #[default]
38    TokenConfigurationNoChange,
39    Conventions(TokenConfigurationConvention),
40    ConventionsControlGroup(AuthorizedActionTakers),
41    ConventionsAdminGroup(AuthorizedActionTakers),
42    MaxSupply(Option<TokenAmount>),
43    MaxSupplyControlGroup(AuthorizedActionTakers),
44    MaxSupplyAdminGroup(AuthorizedActionTakers),
45    PerpetualDistribution(Option<TokenPerpetualDistribution>),
46    PerpetualDistributionControlGroup(AuthorizedActionTakers),
47    PerpetualDistributionAdminGroup(AuthorizedActionTakers),
48    NewTokensDestinationIdentity(Option<Identifier>),
49    NewTokensDestinationIdentityControlGroup(AuthorizedActionTakers),
50    NewTokensDestinationIdentityAdminGroup(AuthorizedActionTakers),
51    MintingAllowChoosingDestination(bool),
52    MintingAllowChoosingDestinationControlGroup(AuthorizedActionTakers),
53    MintingAllowChoosingDestinationAdminGroup(AuthorizedActionTakers),
54    ManualMinting(AuthorizedActionTakers),
55    ManualMintingAdminGroup(AuthorizedActionTakers),
56    ManualBurning(AuthorizedActionTakers),
57    ManualBurningAdminGroup(AuthorizedActionTakers),
58    Freeze(AuthorizedActionTakers),
59    FreezeAdminGroup(AuthorizedActionTakers),
60    Unfreeze(AuthorizedActionTakers),
61    UnfreezeAdminGroup(AuthorizedActionTakers),
62    DestroyFrozenFunds(AuthorizedActionTakers),
63    DestroyFrozenFundsAdminGroup(AuthorizedActionTakers),
64    EmergencyAction(AuthorizedActionTakers),
65    EmergencyActionAdminGroup(AuthorizedActionTakers),
66    MarketplaceTradeMode(TokenTradeMode),
67    MarketplaceTradeModeControlGroup(AuthorizedActionTakers),
68    MarketplaceTradeModeAdminGroup(AuthorizedActionTakers),
69    MainControlGroup(Option<GroupContractPosition>),
70}
71
72// Internal-`$type` serde shape via a struct-variant Repr (the outer enum mixes
73// AuthorizedActionTakers/struct variants with primitive/Option/unit variants
74// that serde cannot auto-internal-tag). Unit -> `{"$type":"..."}`; data ->
75// `{"$type":"...","value":...}`. `MaxSupply` gains the json_safe protection it
76// previously lacked (Option<u64> above MAX_SAFE_INTEGER -> string in HR JSON;
77// Content-safe — never emits u128). `MainControlGroup` is u16 (always JS-safe).
78// The macro keeps the Repr and both `From` impls in lockstep with one variant
79// list — avoiding the "update Serialize AND Deserialize" maintenance trap.
80#[cfg(feature = "serde-conversion")]
81macro_rules! token_configuration_change_item_repr {
82    (
83        unit: $unit:ident,
84        $( $variant:ident : $ty:ty $(, with = $with:literal)? );* $(;)?
85    ) => {
86        #[derive(Serialize, Deserialize)]
87        #[serde(tag = "$type", rename_all = "camelCase")]
88        enum TokenConfigurationChangeItemRepr {
89            $unit,
90            $( $variant {
91                $(#[serde(with = $with)])?
92                value: $ty,
93            } ),*
94        }
95
96        impl From<TokenConfigurationChangeItem> for TokenConfigurationChangeItemRepr {
97            fn from(item: TokenConfigurationChangeItem) -> Self {
98                match item {
99                    TokenConfigurationChangeItem::$unit => Self::$unit,
100                    $( TokenConfigurationChangeItem::$variant(value) => Self::$variant { value }, )*
101                }
102            }
103        }
104
105        impl From<TokenConfigurationChangeItemRepr> for TokenConfigurationChangeItem {
106            fn from(repr: TokenConfigurationChangeItemRepr) -> Self {
107                match repr {
108                    TokenConfigurationChangeItemRepr::$unit => Self::$unit,
109                    $( TokenConfigurationChangeItemRepr::$variant { value } => Self::$variant(value), )*
110                }
111            }
112        }
113    };
114}
115
116#[cfg(feature = "serde-conversion")]
117token_configuration_change_item_repr! {
118    unit: TokenConfigurationNoChange,
119    Conventions: TokenConfigurationConvention;
120    ConventionsControlGroup: AuthorizedActionTakers;
121    ConventionsAdminGroup: AuthorizedActionTakers;
122    MaxSupply: Option<TokenAmount>, with = "crate::serialization::json_safe_option_u64";
123    MaxSupplyControlGroup: AuthorizedActionTakers;
124    MaxSupplyAdminGroup: AuthorizedActionTakers;
125    PerpetualDistribution: Option<TokenPerpetualDistribution>;
126    PerpetualDistributionControlGroup: AuthorizedActionTakers;
127    PerpetualDistributionAdminGroup: AuthorizedActionTakers;
128    NewTokensDestinationIdentity: Option<Identifier>;
129    NewTokensDestinationIdentityControlGroup: AuthorizedActionTakers;
130    NewTokensDestinationIdentityAdminGroup: AuthorizedActionTakers;
131    MintingAllowChoosingDestination: bool;
132    MintingAllowChoosingDestinationControlGroup: AuthorizedActionTakers;
133    MintingAllowChoosingDestinationAdminGroup: AuthorizedActionTakers;
134    ManualMinting: AuthorizedActionTakers;
135    ManualMintingAdminGroup: AuthorizedActionTakers;
136    ManualBurning: AuthorizedActionTakers;
137    ManualBurningAdminGroup: AuthorizedActionTakers;
138    Freeze: AuthorizedActionTakers;
139    FreezeAdminGroup: AuthorizedActionTakers;
140    Unfreeze: AuthorizedActionTakers;
141    UnfreezeAdminGroup: AuthorizedActionTakers;
142    DestroyFrozenFunds: AuthorizedActionTakers;
143    DestroyFrozenFundsAdminGroup: AuthorizedActionTakers;
144    EmergencyAction: AuthorizedActionTakers;
145    EmergencyActionAdminGroup: AuthorizedActionTakers;
146    MarketplaceTradeMode: TokenTradeMode;
147    MarketplaceTradeModeControlGroup: AuthorizedActionTakers;
148    MarketplaceTradeModeAdminGroup: AuthorizedActionTakers;
149    MainControlGroup: Option<GroupContractPosition>;
150}
151
152impl TokenConfigurationChangeItem {
153    pub fn payload_serialization(&self) -> Result<Option<Vec<u8>>, ProtocolError> {
154        Ok(match self {
155            TokenConfigurationChangeItem::TokenConfigurationNoChange => None,
156            TokenConfigurationChangeItem::Conventions(convention) => Some(
157                bincode::encode_to_vec(convention, bincode::config::standard())
158                    .map_err(|e| ProtocolError::EncodingError(e.to_string()))?,
159            ),
160            TokenConfigurationChangeItem::ConventionsControlGroup(a)
161            | TokenConfigurationChangeItem::ConventionsAdminGroup(a)
162            | TokenConfigurationChangeItem::MaxSupplyControlGroup(a)
163            | TokenConfigurationChangeItem::MaxSupplyAdminGroup(a)
164            | TokenConfigurationChangeItem::PerpetualDistributionControlGroup(a)
165            | TokenConfigurationChangeItem::PerpetualDistributionAdminGroup(a)
166            | TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup(a)
167            | TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup(a)
168            | TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup(a)
169            | TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup(a)
170            | TokenConfigurationChangeItem::ManualMinting(a)
171            | TokenConfigurationChangeItem::ManualMintingAdminGroup(a)
172            | TokenConfigurationChangeItem::ManualBurning(a)
173            | TokenConfigurationChangeItem::ManualBurningAdminGroup(a)
174            | TokenConfigurationChangeItem::Freeze(a)
175            | TokenConfigurationChangeItem::FreezeAdminGroup(a)
176            | TokenConfigurationChangeItem::Unfreeze(a)
177            | TokenConfigurationChangeItem::UnfreezeAdminGroup(a)
178            | TokenConfigurationChangeItem::DestroyFrozenFunds(a)
179            | TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup(a)
180            | TokenConfigurationChangeItem::EmergencyAction(a)
181            | TokenConfigurationChangeItem::EmergencyActionAdminGroup(a)
182            | TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(a)
183            | TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(a) => Some(a.to_bytes()),
184            TokenConfigurationChangeItem::MaxSupply(max_supply) => {
185                max_supply.map(|amount| amount.to_be_bytes().to_vec())
186            }
187            TokenConfigurationChangeItem::PerpetualDistribution(distribution) => distribution
188                .as_ref()
189                .map(|dist| {
190                    bincode::encode_to_vec(dist, bincode::config::standard())
191                        .map_err(|e| ProtocolError::EncodingError(e.to_string()))
192                })
193                .transpose()?,
194            TokenConfigurationChangeItem::NewTokensDestinationIdentity(identity) => {
195                identity.map(|id| id.to_vec())
196            }
197            TokenConfigurationChangeItem::MintingAllowChoosingDestination(allow) => {
198                Some(vec![*allow as u8])
199            }
200            TokenConfigurationChangeItem::MarketplaceTradeMode(mode) => Some(
201                bincode::encode_to_vec(mode, bincode::config::standard())
202                    .map_err(|e| ProtocolError::EncodingError(e.to_string()))?,
203            ),
204            TokenConfigurationChangeItem::MainControlGroup(position) => {
205                position.map(|pos| pos.to_be_bytes().to_vec())
206            }
207        })
208    }
209    pub fn u8_item_index(&self) -> u8 {
210        match self {
211            TokenConfigurationChangeItem::TokenConfigurationNoChange => 0,
212            TokenConfigurationChangeItem::Conventions(_) => 1,
213            TokenConfigurationChangeItem::ConventionsControlGroup(_) => 2,
214            TokenConfigurationChangeItem::ConventionsAdminGroup(_) => 3,
215            TokenConfigurationChangeItem::MaxSupply(_) => 4,
216            TokenConfigurationChangeItem::MaxSupplyControlGroup(_) => 5,
217            TokenConfigurationChangeItem::MaxSupplyAdminGroup(_) => 6,
218            TokenConfigurationChangeItem::PerpetualDistribution(_) => 7,
219            TokenConfigurationChangeItem::PerpetualDistributionControlGroup(_) => 8,
220            TokenConfigurationChangeItem::PerpetualDistributionAdminGroup(_) => 9,
221            TokenConfigurationChangeItem::NewTokensDestinationIdentity(_) => 10,
222            TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup(_) => 11,
223            TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup(_) => 12,
224            TokenConfigurationChangeItem::MintingAllowChoosingDestination(_) => 13,
225            TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup(_) => 14,
226            TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup(_) => 15,
227            TokenConfigurationChangeItem::ManualMinting(_) => 16,
228            TokenConfigurationChangeItem::ManualMintingAdminGroup(_) => 17,
229            TokenConfigurationChangeItem::ManualBurning(_) => 18,
230            TokenConfigurationChangeItem::ManualBurningAdminGroup(_) => 19,
231            TokenConfigurationChangeItem::Freeze(_) => 20,
232            TokenConfigurationChangeItem::FreezeAdminGroup(_) => 21,
233            TokenConfigurationChangeItem::Unfreeze(_) => 22,
234            TokenConfigurationChangeItem::UnfreezeAdminGroup(_) => 23,
235            TokenConfigurationChangeItem::DestroyFrozenFunds(_) => 24,
236            TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup(_) => 25,
237            TokenConfigurationChangeItem::EmergencyAction(_) => 26,
238            TokenConfigurationChangeItem::EmergencyActionAdminGroup(_) => 27,
239            TokenConfigurationChangeItem::MarketplaceTradeMode(_) => 28,
240            TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(_) => 29,
241            TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(_) => 30,
242            TokenConfigurationChangeItem::MainControlGroup(_) => 31,
243        }
244    }
245}
246
247impl fmt::Display for TokenConfigurationChangeItem {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        match self {
250            TokenConfigurationChangeItem::TokenConfigurationNoChange => {
251                write!(f, "No Change in Token Configuration")
252            }
253            TokenConfigurationChangeItem::Conventions(convention) => {
254                write!(f, "Conventions: {}", convention)
255            }
256            TokenConfigurationChangeItem::ConventionsControlGroup(control_group) => {
257                write!(f, "Conventions Control Group: {}", control_group)
258            }
259            TokenConfigurationChangeItem::ConventionsAdminGroup(admin_group) => {
260                write!(f, "Conventions Admin Group: {}", admin_group)
261            }
262            TokenConfigurationChangeItem::MaxSupply(max_supply) => match max_supply {
263                Some(amount) => write!(f, "Max Supply: {}", amount),
264                None => write!(f, "Max Supply: No Limit"),
265            },
266            TokenConfigurationChangeItem::MaxSupplyControlGroup(control_group) => {
267                write!(f, "Max Supply Control Group: {}", control_group)
268            }
269            TokenConfigurationChangeItem::MaxSupplyAdminGroup(admin_group) => {
270                write!(f, "Max Supply Admin Group: {}", admin_group)
271            }
272            TokenConfigurationChangeItem::PerpetualDistribution(distribution) => match distribution
273            {
274                Some(dist) => write!(f, "Perpetual Distribution: {}", dist),
275                None => write!(f, "Perpetual Distribution: None"),
276            },
277            TokenConfigurationChangeItem::PerpetualDistributionControlGroup(control_group) => {
278                write!(f, "Perpetual Distribution Control Group: {}", control_group)
279            }
280            TokenConfigurationChangeItem::PerpetualDistributionAdminGroup(admin_group) => {
281                write!(f, "Perpetual Distribution Admin Group: {}", admin_group)
282            }
283            TokenConfigurationChangeItem::NewTokensDestinationIdentity(identity) => {
284                match identity {
285                    Some(id) => write!(f, "New Tokens Destination Identity: {}", id),
286                    None => write!(f, "New Tokens Destination Identity: None"),
287                }
288            }
289            TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup(
290                control_group,
291            ) => {
292                write!(
293                    f,
294                    "New Tokens Destination Identity Control Group: {}",
295                    control_group
296                )
297            }
298            TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup(admin_group) => {
299                write!(
300                    f,
301                    "New Tokens Destination Identity Admin Group: {}",
302                    admin_group
303                )
304            }
305            TokenConfigurationChangeItem::MintingAllowChoosingDestination(allow) => {
306                write!(f, "Minting Allow Choosing Destination: {}", allow)
307            }
308            TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup(
309                control_group,
310            ) => {
311                write!(
312                    f,
313                    "Minting Allow Choosing Destination Control Group: {}",
314                    control_group
315                )
316            }
317            TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup(
318                admin_group,
319            ) => {
320                write!(
321                    f,
322                    "Minting Allow Choosing Destination Admin Group: {}",
323                    admin_group
324                )
325            }
326            TokenConfigurationChangeItem::ManualMinting(control_group) => {
327                write!(f, "Manual Minting: {}", control_group)
328            }
329            TokenConfigurationChangeItem::ManualMintingAdminGroup(admin_group) => {
330                write!(f, "Manual Minting Admin Group: {}", admin_group)
331            }
332            TokenConfigurationChangeItem::ManualBurning(control_group) => {
333                write!(f, "Manual Burning: {}", control_group)
334            }
335            TokenConfigurationChangeItem::ManualBurningAdminGroup(admin_group) => {
336                write!(f, "Manual Burning Admin Group: {}", admin_group)
337            }
338            TokenConfigurationChangeItem::Freeze(control_group) => {
339                write!(f, "Freeze: {}", control_group)
340            }
341            TokenConfigurationChangeItem::FreezeAdminGroup(admin_group) => {
342                write!(f, "Freeze Admin Group: {}", admin_group)
343            }
344            TokenConfigurationChangeItem::Unfreeze(control_group) => {
345                write!(f, "Unfreeze: {}", control_group)
346            }
347            TokenConfigurationChangeItem::UnfreezeAdminGroup(admin_group) => {
348                write!(f, "Unfreeze Admin Group: {}", admin_group)
349            }
350            TokenConfigurationChangeItem::DestroyFrozenFunds(control_group) => {
351                write!(f, "Destroy Frozen Funds: {}", control_group)
352            }
353            TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup(admin_group) => {
354                write!(f, "Destroy Frozen Funds Admin Group: {}", admin_group)
355            }
356            TokenConfigurationChangeItem::EmergencyAction(control_group) => {
357                write!(f, "Emergency Action: {}", control_group)
358            }
359            TokenConfigurationChangeItem::EmergencyActionAdminGroup(admin_group) => {
360                write!(f, "Emergency Action Admin Group: {}", admin_group)
361            }
362            TokenConfigurationChangeItem::MainControlGroup(position) => match position {
363                Some(pos) => write!(f, "Main Control Group: {}", pos),
364                None => write!(f, "Main Control Group: None"),
365            },
366            TokenConfigurationChangeItem::MarketplaceTradeMode(mode) => {
367                write!(f, "Marketplace Trade Mode: {:?}", mode)
368            }
369            TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(control_group) => {
370                write!(f, "Marketplace Trade Mode Control Group: {}", control_group)
371            }
372            TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(admin_group) => {
373                write!(f, "Marketplace Trade Mode Admin Group: {}", admin_group)
374            }
375        }
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use std::collections::BTreeSet;
383
384    /// Helper: build one instance of every variant using default inner values.
385    fn all_variants() -> Vec<TokenConfigurationChangeItem> {
386        let aat = AuthorizedActionTakers::NoOne;
387        vec![
388            TokenConfigurationChangeItem::TokenConfigurationNoChange,
389            TokenConfigurationChangeItem::Conventions(
390                TokenConfigurationConvention::V0(
391                    crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0::default(),
392                ),
393            ),
394            TokenConfigurationChangeItem::ConventionsControlGroup(aat),
395            TokenConfigurationChangeItem::ConventionsAdminGroup(aat),
396            TokenConfigurationChangeItem::MaxSupply(None),
397            TokenConfigurationChangeItem::MaxSupplyControlGroup(aat),
398            TokenConfigurationChangeItem::MaxSupplyAdminGroup(aat),
399            TokenConfigurationChangeItem::PerpetualDistribution(None),
400            TokenConfigurationChangeItem::PerpetualDistributionControlGroup(aat),
401            TokenConfigurationChangeItem::PerpetualDistributionAdminGroup(aat),
402            TokenConfigurationChangeItem::NewTokensDestinationIdentity(None),
403            TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup(aat),
404            TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup(aat),
405            TokenConfigurationChangeItem::MintingAllowChoosingDestination(false),
406            TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup(aat),
407            TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup(aat),
408            TokenConfigurationChangeItem::ManualMinting(aat),
409            TokenConfigurationChangeItem::ManualMintingAdminGroup(aat),
410            TokenConfigurationChangeItem::ManualBurning(aat),
411            TokenConfigurationChangeItem::ManualBurningAdminGroup(aat),
412            TokenConfigurationChangeItem::Freeze(aat),
413            TokenConfigurationChangeItem::FreezeAdminGroup(aat),
414            TokenConfigurationChangeItem::Unfreeze(aat),
415            TokenConfigurationChangeItem::UnfreezeAdminGroup(aat),
416            TokenConfigurationChangeItem::DestroyFrozenFunds(aat),
417            TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup(aat),
418            TokenConfigurationChangeItem::EmergencyAction(aat),
419            TokenConfigurationChangeItem::EmergencyActionAdminGroup(aat),
420            TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::default()),
421            TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(aat),
422            TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(aat),
423            TokenConfigurationChangeItem::MainControlGroup(None),
424        ]
425    }
426
427    // ---- u8_item_index returns unique values 0..=31 ----
428
429    #[test]
430    fn u8_item_index_values_are_unique() {
431        let variants = all_variants();
432        let indices: Vec<u8> = variants.iter().map(|v| v.u8_item_index()).collect();
433        let unique: BTreeSet<u8> = indices.iter().cloned().collect();
434        assert_eq!(
435            indices.len(),
436            unique.len(),
437            "Duplicate u8_item_index values found: {:?}",
438            indices
439        );
440    }
441
442    #[test]
443    fn u8_item_index_covers_0_through_31() {
444        let variants = all_variants();
445        let indices: BTreeSet<u8> = variants.iter().map(|v| v.u8_item_index()).collect();
446        for i in 0u8..=31 {
447            assert!(indices.contains(&i), "Missing u8_item_index value: {}", i);
448        }
449    }
450
451    #[test]
452    fn u8_item_index_all_within_range() {
453        let variants = all_variants();
454        for v in &variants {
455            let idx = v.u8_item_index();
456            assert!(idx <= 31, "Index {} exceeds expected max of 31", idx);
457        }
458    }
459
460    #[test]
461    fn u8_item_index_specific_known_values() {
462        assert_eq!(
463            TokenConfigurationChangeItem::TokenConfigurationNoChange.u8_item_index(),
464            0
465        );
466        assert_eq!(
467            TokenConfigurationChangeItem::MaxSupply(Some(100)).u8_item_index(),
468            4
469        );
470        assert_eq!(
471            TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne)
472                .u8_item_index(),
473            16
474        );
475        assert_eq!(
476            TokenConfigurationChangeItem::MainControlGroup(Some(5)).u8_item_index(),
477            31
478        );
479    }
480
481    #[test]
482    fn u8_item_index_variant_count() {
483        // We expect exactly 32 variants (indices 0..=31)
484        let variants = all_variants();
485        assert_eq!(variants.len(), 32);
486    }
487
488    // --- payload_serialization ---
489
490    #[test]
491    fn payload_serialization_no_change_is_none() {
492        let item = TokenConfigurationChangeItem::TokenConfigurationNoChange;
493        assert!(item.payload_serialization().unwrap().is_none());
494    }
495
496    #[test]
497    fn payload_serialization_max_supply_none_is_none() {
498        let item = TokenConfigurationChangeItem::MaxSupply(None);
499        assert!(item.payload_serialization().unwrap().is_none());
500    }
501
502    #[test]
503    fn payload_serialization_max_supply_some_encodes_be_bytes() {
504        let amount: u64 = 0x0102_0304_0506_0708;
505        let item = TokenConfigurationChangeItem::MaxSupply(Some(amount));
506        let bytes = item.payload_serialization().unwrap().unwrap();
507        assert_eq!(bytes, amount.to_be_bytes().to_vec());
508        assert_eq!(bytes.len(), 8);
509    }
510
511    #[test]
512    fn payload_serialization_new_tokens_destination_identity_none_is_none() {
513        let item = TokenConfigurationChangeItem::NewTokensDestinationIdentity(None);
514        assert!(item.payload_serialization().unwrap().is_none());
515    }
516
517    #[test]
518    fn payload_serialization_new_tokens_destination_identity_some_is_32_bytes() {
519        let id = Identifier::from([0x77u8; 32]);
520        let item = TokenConfigurationChangeItem::NewTokensDestinationIdentity(Some(id));
521        let bytes = item.payload_serialization().unwrap().unwrap();
522        assert_eq!(bytes.len(), 32);
523        assert_eq!(bytes, vec![0x77u8; 32]);
524    }
525
526    #[test]
527    fn payload_serialization_perpetual_distribution_none_is_none() {
528        let item = TokenConfigurationChangeItem::PerpetualDistribution(None);
529        assert!(item.payload_serialization().unwrap().is_none());
530    }
531
532    #[test]
533    fn payload_serialization_main_control_group_none_is_none() {
534        let item = TokenConfigurationChangeItem::MainControlGroup(None);
535        assert!(item.payload_serialization().unwrap().is_none());
536    }
537
538    #[test]
539    fn payload_serialization_main_control_group_some_is_two_be_bytes() {
540        let pos: u16 = 0xABCD;
541        let item = TokenConfigurationChangeItem::MainControlGroup(Some(pos));
542        let bytes = item.payload_serialization().unwrap().unwrap();
543        assert_eq!(bytes, pos.to_be_bytes().to_vec());
544        assert_eq!(bytes.len(), 2);
545    }
546
547    #[test]
548    fn payload_serialization_minting_allow_choosing_destination_true() {
549        let item = TokenConfigurationChangeItem::MintingAllowChoosingDestination(true);
550        let bytes = item.payload_serialization().unwrap().unwrap();
551        assert_eq!(bytes, vec![1]);
552    }
553
554    #[test]
555    fn payload_serialization_minting_allow_choosing_destination_false() {
556        let item = TokenConfigurationChangeItem::MintingAllowChoosingDestination(false);
557        let bytes = item.payload_serialization().unwrap().unwrap();
558        assert_eq!(bytes, vec![0]);
559    }
560
561    #[test]
562    fn payload_serialization_authorized_action_takers_variants_use_to_bytes() {
563        // The big match arm has 23 variants that all serialize via AuthorizedActionTakers::to_bytes
564        // Sanity-check a few representative variants produce the expected tag bytes.
565        let aat = AuthorizedActionTakers::ContractOwner;
566        let expected = aat.to_bytes();
567
568        let variants = [
569            TokenConfigurationChangeItem::ConventionsControlGroup(aat),
570            TokenConfigurationChangeItem::ConventionsAdminGroup(aat),
571            TokenConfigurationChangeItem::MaxSupplyControlGroup(aat),
572            TokenConfigurationChangeItem::MaxSupplyAdminGroup(aat),
573            TokenConfigurationChangeItem::PerpetualDistributionControlGroup(aat),
574            TokenConfigurationChangeItem::PerpetualDistributionAdminGroup(aat),
575            TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup(aat),
576            TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup(aat),
577            TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup(aat),
578            TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup(aat),
579            TokenConfigurationChangeItem::ManualMinting(aat),
580            TokenConfigurationChangeItem::ManualMintingAdminGroup(aat),
581            TokenConfigurationChangeItem::ManualBurning(aat),
582            TokenConfigurationChangeItem::ManualBurningAdminGroup(aat),
583            TokenConfigurationChangeItem::Freeze(aat),
584            TokenConfigurationChangeItem::FreezeAdminGroup(aat),
585            TokenConfigurationChangeItem::Unfreeze(aat),
586            TokenConfigurationChangeItem::UnfreezeAdminGroup(aat),
587            TokenConfigurationChangeItem::DestroyFrozenFunds(aat),
588            TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup(aat),
589            TokenConfigurationChangeItem::EmergencyAction(aat),
590            TokenConfigurationChangeItem::EmergencyActionAdminGroup(aat),
591            TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(aat),
592            TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(aat),
593        ];
594        for v in &variants {
595            let bytes = v.payload_serialization().unwrap().unwrap();
596            assert_eq!(bytes, expected, "mismatch for variant {:?}", v);
597        }
598    }
599
600    #[test]
601    fn payload_serialization_conventions_roundtrips_via_bincode() {
602        // We don't assert the exact bytes (bincode-dependent) but we assert
603        // (1) Some(..) is returned and (2) it decodes back to the same convention.
604        use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0;
605        let convention = TokenConfigurationConvention::V0(TokenConfigurationConventionV0 {
606            localizations: Default::default(),
607            decimals: 5,
608        });
609        let item = TokenConfigurationChangeItem::Conventions(convention.clone());
610        let bytes = item.payload_serialization().unwrap().unwrap();
611        assert!(!bytes.is_empty());
612        let (decoded, _): (TokenConfigurationConvention, _) =
613            bincode::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
614        assert_eq!(decoded, convention);
615    }
616
617    #[test]
618    fn payload_serialization_marketplace_trade_mode_produces_bytes() {
619        let item = TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::NotTradeable);
620        let bytes = item.payload_serialization().unwrap().unwrap();
621        assert!(!bytes.is_empty());
622        // Roundtrip decode
623        let (decoded, _): (TokenTradeMode, _) =
624            bincode::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
625        assert_eq!(decoded, TokenTradeMode::NotTradeable);
626    }
627
628    // --- Display ---
629
630    #[test]
631    fn display_no_change() {
632        let s = format!(
633            "{}",
634            TokenConfigurationChangeItem::TokenConfigurationNoChange
635        );
636        assert_eq!(s, "No Change in Token Configuration");
637    }
638
639    #[test]
640    fn display_max_supply_some_vs_none() {
641        let some = format!("{}", TokenConfigurationChangeItem::MaxSupply(Some(500)));
642        let none = format!("{}", TokenConfigurationChangeItem::MaxSupply(None));
643        assert!(some.contains("500"));
644        assert!(none.contains("No Limit"));
645    }
646
647    #[test]
648    fn display_perpetual_distribution_none_uses_none_marker() {
649        let s = format!(
650            "{}",
651            TokenConfigurationChangeItem::PerpetualDistribution(None)
652        );
653        assert!(s.contains("None"));
654    }
655
656    #[test]
657    fn display_new_tokens_destination_identity_none_uses_none_marker() {
658        let s = format!(
659            "{}",
660            TokenConfigurationChangeItem::NewTokensDestinationIdentity(None)
661        );
662        assert!(s.contains("None"));
663    }
664
665    #[test]
666    fn display_new_tokens_destination_identity_some_includes_id() {
667        let id = Identifier::from([3u8; 32]);
668        let s = format!(
669            "{}",
670            TokenConfigurationChangeItem::NewTokensDestinationIdentity(Some(id))
671        );
672        assert!(s.contains("New Tokens Destination Identity"));
673    }
674
675    #[test]
676    fn display_main_control_group_none_uses_none_marker() {
677        let s = format!("{}", TokenConfigurationChangeItem::MainControlGroup(None));
678        assert!(s.contains("None"));
679    }
680
681    #[test]
682    fn display_main_control_group_some_contains_position() {
683        let s = format!(
684            "{}",
685            TokenConfigurationChangeItem::MainControlGroup(Some(7))
686        );
687        assert!(s.contains("7"));
688    }
689
690    #[test]
691    fn display_minting_allow_choosing_destination_contains_value() {
692        let s_true = format!(
693            "{}",
694            TokenConfigurationChangeItem::MintingAllowChoosingDestination(true)
695        );
696        let s_false = format!(
697            "{}",
698            TokenConfigurationChangeItem::MintingAllowChoosingDestination(false)
699        );
700        assert!(s_true.contains("true"));
701        assert!(s_false.contains("false"));
702    }
703
704    #[test]
705    fn display_marketplace_trade_mode_uses_debug() {
706        let s = format!(
707            "{}",
708            TokenConfigurationChangeItem::MarketplaceTradeMode(TokenTradeMode::NotTradeable)
709        );
710        assert!(s.contains("NotTradeable"));
711    }
712
713    #[test]
714    fn display_action_takers_group_variants() {
715        // Exercise a handful of action-taker-carrying variants to confirm each
716        // writes its label.
717        let aat = AuthorizedActionTakers::ContractOwner;
718        let cases = vec![
719            (
720                format!("{}", TokenConfigurationChangeItem::ManualMinting(aat)),
721                "Manual Minting",
722            ),
723            (
724                format!(
725                    "{}",
726                    TokenConfigurationChangeItem::ManualMintingAdminGroup(aat)
727                ),
728                "Manual Minting Admin Group",
729            ),
730            (
731                format!("{}", TokenConfigurationChangeItem::ManualBurning(aat)),
732                "Manual Burning",
733            ),
734            (
735                format!("{}", TokenConfigurationChangeItem::Freeze(aat)),
736                "Freeze",
737            ),
738            (
739                format!("{}", TokenConfigurationChangeItem::Unfreeze(aat)),
740                "Unfreeze",
741            ),
742            (
743                format!("{}", TokenConfigurationChangeItem::DestroyFrozenFunds(aat)),
744                "Destroy Frozen Funds",
745            ),
746            (
747                format!("{}", TokenConfigurationChangeItem::EmergencyAction(aat)),
748                "Emergency Action",
749            ),
750            (
751                format!(
752                    "{}",
753                    TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(aat)
754                ),
755                "Marketplace Trade Mode Control Group",
756            ),
757            (
758                format!(
759                    "{}",
760                    TokenConfigurationChangeItem::ConventionsControlGroup(aat)
761                ),
762                "Conventions Control Group",
763            ),
764        ];
765        for (output, expected_prefix) in cases {
766            assert!(
767                output.contains(expected_prefix),
768                "expected {:?} in {:?}",
769                expected_prefix,
770                output
771            );
772        }
773    }
774
775    // --- Equality ---
776
777    #[test]
778    fn equality_same_variant_same_data() {
779        let a = TokenConfigurationChangeItem::MaxSupply(Some(42));
780        let b = TokenConfigurationChangeItem::MaxSupply(Some(42));
781        assert_eq!(a, b);
782    }
783
784    #[test]
785    fn equality_same_variant_different_data_unequal() {
786        let a = TokenConfigurationChangeItem::MaxSupply(Some(42));
787        let b = TokenConfigurationChangeItem::MaxSupply(Some(43));
788        assert_ne!(a, b);
789    }
790
791    #[test]
792    fn equality_different_variants_unequal() {
793        let a = TokenConfigurationChangeItem::MaxSupply(None);
794        let b = TokenConfigurationChangeItem::MainControlGroup(None);
795        assert_ne!(a, b);
796    }
797
798    #[test]
799    fn equality_authorized_action_takers_sensitivity() {
800        let a = TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne);
801        let b = TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::ContractOwner);
802        assert_ne!(a, b);
803    }
804
805    #[test]
806    fn equality_bool_variant_distinguishes_values() {
807        let a = TokenConfigurationChangeItem::MintingAllowChoosingDestination(true);
808        let b = TokenConfigurationChangeItem::MintingAllowChoosingDestination(false);
809        assert_ne!(a, b);
810    }
811
812    // --- Default + Clone ---
813
814    #[test]
815    fn default_is_no_change() {
816        let d: TokenConfigurationChangeItem = Default::default();
817        assert_eq!(d, TokenConfigurationChangeItem::TokenConfigurationNoChange);
818    }
819
820    #[test]
821    fn clone_preserves_variant_and_data() {
822        let aat = AuthorizedActionTakers::Identity(Identifier::from([5u8; 32]));
823        let a = TokenConfigurationChangeItem::Freeze(aat);
824        let b = a.clone();
825        assert_eq!(a, b);
826    }
827
828    // --- Debug ---
829
830    #[test]
831    fn debug_trait_contains_variant_name() {
832        let a = TokenConfigurationChangeItem::ManualMinting(AuthorizedActionTakers::NoOne);
833        let dbg = format!("{:?}", a);
834        assert!(dbg.contains("ManualMinting"));
835    }
836}
837
838// --- canonical conversion trait impls (unification pass 1) ---
839#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
840impl crate::serialization::JsonConvertible for TokenConfigurationChangeItem {}
841
842#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
843impl crate::serialization::ValueConvertible for TokenConfigurationChangeItem {}
844
845#[cfg(all(
846    test,
847    feature = "json-conversion",
848    feature = "value-conversion",
849    feature = "serde-conversion"
850))]
851mod json_convertible_tests {
852    use super::*;
853    use platform_value::platform_value;
854    use serde_json::json;
855
856    /// Non-default variant (`MaxSupply(Some(...))`) with a non-zero inner amount
857    /// so the wire-shape assertion catches a silent variant flip or inner-zero
858    /// on round-trip.
859    fn fixture() -> TokenConfigurationChangeItem {
860        TokenConfigurationChangeItem::MaxSupply(Some(123_456_789u64))
861    }
862
863    #[test]
864    fn json_round_trip_with_full_wire_shape() {
865        use crate::serialization::JsonConvertible;
866        let original = fixture();
867        let json = original.to_json().expect("to_json");
868        // `TokenConfigurationChangeItem` is internally tagged via its Repr:
869        // `{ "$type":"maxSupply", "value":<inner> }` where `Some(x)` -> `x`.
870        // `MaxSupply` now carries json_safe (Option<u64> -> string above
871        // MAX_SAFE_INTEGER in HR JSON); here 123_456_789 stays numeric.
872        // The value-path assertion uses `123_456_789u64` to lock in `Value::U64`.
873        assert_eq!(json, json!({"$type": "maxSupply", "value": 123_456_789u64}));
874        let recovered = TokenConfigurationChangeItem::from_json(json).expect("from_json");
875        assert_eq!(original, recovered);
876    }
877
878    #[test]
879    fn value_round_trip_with_full_wire_shape() {
880        use crate::serialization::ValueConvertible;
881        let original = fixture();
882        let value = original.to_object().expect("to_object");
883        // `123_456_789u64`: explicit suffix forces `Value::U64`, matching
884        // `TokenAmount`'s u64 type. Bare integer would expand to `Value::I32`.
885        assert_eq!(
886            value,
887            platform_value!({"$type": "maxSupply", "value": 123_456_789u64})
888        );
889        let recovered = TokenConfigurationChangeItem::from_object(value).expect("from_object");
890        assert_eq!(original, recovered);
891    }
892
893    #[test]
894    fn json_large_max_supply_serializes_as_string_for_js_safety() {
895        use crate::serialization::JsonConvertible;
896        // `MaxSupply` previously had NO json_safe annotation — a latent JS
897        // precision bug. Above `Number.MAX_SAFE_INTEGER` it must now serialize
898        // as a string in human-readable JSON. This pins that the Repr's
899        // json_safe_option_u64 survives the internal-tag Content buffer.
900        let original = TokenConfigurationChangeItem::MaxSupply(Some(9_007_199_254_740_993)); // 2^53 + 1
901        let json = original.to_json().expect("to_json");
902        assert_eq!(
903            json,
904            json!({"$type": "maxSupply", "value": "9007199254740993"})
905        );
906        let recovered = TokenConfigurationChangeItem::from_json(json).expect("from_json");
907        assert_eq!(original, recovered);
908    }
909}