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