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