Skip to main content

dpp/tokens/
token_event.rs

1use crate::balances::credits::TokenAmount;
2use crate::block::block_info::BlockInfo;
3use crate::data_contract::accessors::v0::DataContractV0Getters;
4use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
5use crate::data_contract::associated_token::token_distribution_key::TokenDistributionTypeWithResolvedRecipient;
6use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionResolvedRecipient;
7use crate::data_contract::document_type::DocumentTypeRef;
8use crate::document::{Document, DocumentV0};
9use crate::fee::Credits;
10use crate::prelude::{
11    DataContract, DerivationEncryptionKeyIndex, IdentityNonce, RootEncryptionKeyIndex,
12};
13#[cfg(feature = "serde-conversion")]
14use crate::serialization::json::safe_integer::{json_safe_option_encrypted_note, json_safe_u64};
15#[cfg(feature = "json-conversion")]
16use crate::serialization::JsonConvertible;
17#[cfg(feature = "value-conversion")]
18use crate::serialization::ValueConvertible;
19use bincode::{Decode, DecodeUntrusted, Encode};
20use platform_serialization_derive::{
21    PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
22};
23use platform_value::Identifier;
24use platform_version::version::PlatformVersion;
25use std::collections::BTreeMap;
26use std::fmt;
27
28pub type TokenEventPublicNote = Option<String>;
29pub type TokenEventSharedEncryptedNote = Option<SharedEncryptedNote>;
30pub type TokenEventPersonalEncryptedNote = Option<(
31    RootEncryptionKeyIndex,
32    DerivationEncryptionKeyIndex,
33    Vec<u8>,
34)>;
35use crate::serialization::PlatformSerializableWithPlatformVersion;
36use crate::tokens::emergency_action::TokenEmergencyAction;
37use crate::tokens::token_pricing_schedule::TokenPricingSchedule;
38use crate::tokens::SharedEncryptedNote;
39use crate::ProtocolError;
40
41/// Alias representing the identity that will receive tokens or other effects from a token operation.
42pub type RecipientIdentifier = Identifier;
43
44/// Alias representing the identity that will have tokens burned from their account.
45pub type BurnFromIdentifier = Identifier;
46
47/// Alias representing the identity performing a token purchase.
48pub type PurchaserIdentifier = Identifier;
49
50/// Alias representing the identity whose tokens are subject to freezing or unfreezing.
51pub type FrozenIdentifier = Identifier;
52
53/// Represents a recorded token-related operation for use in historical documents and group actions.
54///
55/// `TokenEvent` is designed to encapsulate a single logical token operation,
56/// such as minting, burning, transferring, or freezing tokens. These events are typically:
57///
58/// - **Persisted as historical records** of state transitions, enabling auditability and tracking.
59/// - **Used in group (multisig) actions**, where multiple identities collaborate to authorize complex transitions.
60///
61/// This enum includes rich metadata for each type of operation, such as optional notes (plaintext or encrypted),
62/// involved identities, and amounts. It is **externally versioned** and marked as `unversioned` in platform serialization,
63/// meaning each variant is self-contained without requiring version dispatching logic.
64#[derive(
65    Debug,
66    PartialEq,
67    PartialOrd,
68    Clone,
69    Eq,
70    Encode,
71    Decode,
72    PlatformDeserializeTrusted,
73    PlatformDeserializeUntrusted,
74    PlatformSerialize,
75    DecodeUntrusted,
76)]
77// Custom `Serialize` / `Deserialize` below — `TokenEvent` is a flat enum
78// with all-tuple variants. Internal tagging requires struct variants or
79// newtype-of-named-struct, which doesn't apply to tuple shapes. The custom
80// impl maps positional tuple fields to named JSON keys per variant, emits
81// an internal `$type` discriminator (no `data` wrapper), and uses the
82// `json_safe_u64`
83// / `json_safe_option_encrypted_note` helpers for u64 + encrypted-note
84// fields. Bincode `Encode` / `Decode` derives above are untouched —
85// consensus binary path is unaffected.
86#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
87#[platform_serialize(unversioned)]
88pub enum TokenEvent {
89    /// Event representing the minting of tokens to a recipient.
90    ///
91    /// - `TokenAmount`: The amount of tokens minted.
92    /// - `RecipientIdentifier`: The identity receiving the minted tokens.
93    /// - `TokenEventPublicNote`: Optional note associated with the event.
94    Mint(TokenAmount, RecipientIdentifier, TokenEventPublicNote),
95
96    /// Event representing the burning of tokens, removing them from circulation.
97    ///
98    /// - `TokenAmount`: The amount of tokens burned.
99    /// - `BurnFromIdentifier`: The account to burn from.
100    /// - `TokenEventPublicNote`: Optional note associated with the event.
101    Burn(TokenAmount, BurnFromIdentifier, TokenEventPublicNote),
102
103    /// Event representing freezing of tokens for a specific identity.
104    ///
105    /// - `FrozenIdentifier`: The identity whose tokens are frozen.
106    /// - `TokenEventPublicNote`: Optional note associated with the event.
107    Freeze(FrozenIdentifier, TokenEventPublicNote),
108
109    /// Event representing unfreezing of tokens for a specific identity.
110    ///
111    /// - `FrozenIdentifier`: The identity whose tokens are unfrozen.
112    /// - `TokenEventPublicNote`: Optional note associated with the event.
113    Unfreeze(FrozenIdentifier, TokenEventPublicNote),
114
115    /// Event representing destruction of tokens that were previously frozen.
116    ///
117    /// - `FrozenIdentifier`: The identity whose frozen tokens are destroyed.
118    /// - `TokenAmount`: The amount of frozen tokens destroyed.
119    /// - `TokenEventPublicNote`: Optional note associated with the event.
120    DestroyFrozenFunds(FrozenIdentifier, TokenAmount, TokenEventPublicNote),
121
122    /// Event representing a transfer of tokens from one identity to another.
123    ///
124    /// - `RecipientIdentifier`: The recipient of the tokens.
125    /// - `TokenEventPublicNote`: Optional plaintext note.
126    /// - `TokenEventSharedEncryptedNote`: Optional shared encrypted metadata (multi-party).
127    /// - `TokenEventPersonalEncryptedNote`: Optional private encrypted metadata (recipient-only).
128    /// - `TokenAmount`: The amount of tokens transferred.
129    Transfer(
130        RecipientIdentifier,
131        TokenEventPublicNote,
132        TokenEventSharedEncryptedNote,
133        TokenEventPersonalEncryptedNote,
134        TokenAmount,
135    ),
136
137    /// Event representing a claim of tokens from a distribution pool or source.
138    ///
139    /// - `TokenDistributionTypeWithResolvedRecipient`: Type and resolved recipient of the claim.
140    /// - `TokenAmount`: The amount of tokens claimed.
141    /// - `TokenEventPublicNote`: Optional note associated with the event.
142    Claim(
143        TokenDistributionTypeWithResolvedRecipient,
144        TokenAmount,
145        TokenEventPublicNote,
146    ),
147
148    /// Event representing an emergency action taken on a token or identity.
149    ///
150    /// - `TokenEmergencyAction`: The type of emergency action performed.
151    /// - `TokenEventPublicNote`: Optional note associated with the event.
152    EmergencyAction(TokenEmergencyAction, TokenEventPublicNote),
153
154    /// Event representing an update to the configuration of a token.
155    ///
156    /// - `TokenConfigurationChangeItem`: The configuration change that was applied.
157    /// - `TokenEventPublicNote`: Optional note associated with the event.
158    ConfigUpdate(TokenConfigurationChangeItem, TokenEventPublicNote),
159
160    /// Event representing a change in the direct purchase price of a token.
161    ///
162    /// - `Option<TokenPricingSchedule>`: The new pricing schedule. `None` disables direct purchase.
163    /// - `TokenEventPublicNote`: Optional note associated with the event.
164    ChangePriceForDirectPurchase(Option<TokenPricingSchedule>, TokenEventPublicNote),
165
166    /// Event representing the direct purchase of tokens by a user.
167    ///
168    /// - `TokenAmount`: The amount of tokens purchased.
169    /// - `Credits`: The number of credits paid.
170    DirectPurchase(TokenAmount, Credits),
171
172    /// Event representing tokens moving from an identity balance into the token's shielded pool.
173    ///
174    /// - `TokenAmount`: The amount shielded.
175    Shield(TokenAmount),
176
177    /// Event representing tokens moving from the token's shielded pool to an identity balance.
178    ///
179    /// - `RecipientIdentifier`: The identity credited.
180    /// - `TokenAmount`: The amount unshielded.
181    Unshield(RecipientIdentifier, TokenAmount),
182
183    /// Event representing a transfer inside the token's shielded pool. Nothing about the
184    /// transfer (parties, amount) is public.
185    ShieldedTransfer,
186
187    /// Event representing a mint straight into the token's shielded pool.
188    ///
189    /// - `TokenAmount`: The amount minted.
190    /// - `Identifier`: Digest of the Orchard actions, so a group action commits to the notes.
191    /// - `TokenEventPublicNote`: Optional note associated with the event.
192    MintToPool(TokenAmount, Identifier, TokenEventPublicNote),
193
194    /// Event representing a burn of notes held in the token's shielded pool.
195    ///
196    /// - `TokenAmount`: The amount destroyed.
197    /// - `Identifier`: Digest of the Orchard actions, so a group action commits to the notes.
198    /// - `TokenEventPublicNote`: Optional note associated with the event.
199    BurnFromPool(TokenAmount, Identifier, TokenEventPublicNote),
200
201    /// Event representing a distribution claim paid into the token's shielded pool.
202    ///
203    /// - `TokenAmount`: The amount claimed.
204    ClaimToPool(TokenAmount),
205
206    /// Event representing a direct purchase paid into the token's shielded pool.
207    ///
208    /// - `TokenAmount`: The amount of tokens purchased.
209    /// - `Credits`: The number of credits paid.
210    DirectPurchaseToPool(TokenAmount, Credits),
211}
212
213// Manual impl because TokenEvent is a flat enum with u64-alias tuple variants
214// (TokenAmount, Credits). `#[derive(JsonConvertible)]` would fail: it asserts inner
215// variant types implement `JsonSafeFields`, but TokenAmount/Credits are u64 aliases
216// which intentionally don't. The `#[json_safe_fields]` macro can't annotate tuple
217// variant fields either. Safety is ensured by manual `impl JsonSafeFields` in
218// safe_fields.rs — the developer takes responsibility for these fields.
219#[cfg(feature = "json-conversion")]
220impl JsonConvertible for TokenEvent {}
221
222#[cfg(feature = "serde-conversion")]
223impl serde::Serialize for TokenEvent {
224    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
225        use serde::ser::SerializeMap;
226
227        // Wrappers that route through `json_safe_u64` and the encrypted-note
228        // helper so large u64s stringify in JSON HR and Vec<u8> inside the
229        // tuple becomes base64.
230        struct SafeU64<'a>(&'a u64);
231        impl<'a> serde::Serialize for SafeU64<'a> {
232            fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
233                json_safe_u64::serialize(self.0, s)
234            }
235        }
236        struct SafeOptEncNote<'a>(&'a Option<(u32, u32, Vec<u8>)>);
237        impl<'a> serde::Serialize for SafeOptEncNote<'a> {
238            fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
239                json_safe_option_encrypted_note::serialize(self.0, s)
240            }
241        }
242
243        match self {
244            TokenEvent::Mint(amount, recipient, note) => {
245                let mut m = serializer.serialize_map(Some(4))?;
246                m.serialize_entry("$type", "mint")?;
247                m.serialize_entry("amount", &SafeU64(amount))?;
248                m.serialize_entry("recipient", recipient)?;
249                m.serialize_entry("publicNote", note)?;
250                m.end()
251            }
252            TokenEvent::Burn(amount, from, note) => {
253                let mut m = serializer.serialize_map(Some(4))?;
254                m.serialize_entry("$type", "burn")?;
255                m.serialize_entry("amount", &SafeU64(amount))?;
256                m.serialize_entry("burnFromIdentifier", from)?;
257                m.serialize_entry("publicNote", note)?;
258                m.end()
259            }
260            TokenEvent::Freeze(frozen, note) => {
261                let mut m = serializer.serialize_map(Some(3))?;
262                m.serialize_entry("$type", "freeze")?;
263                m.serialize_entry("frozenIdentifier", frozen)?;
264                m.serialize_entry("publicNote", note)?;
265                m.end()
266            }
267            TokenEvent::Unfreeze(frozen, note) => {
268                let mut m = serializer.serialize_map(Some(3))?;
269                m.serialize_entry("$type", "unfreeze")?;
270                m.serialize_entry("frozenIdentifier", frozen)?;
271                m.serialize_entry("publicNote", note)?;
272                m.end()
273            }
274            TokenEvent::DestroyFrozenFunds(frozen, amount, note) => {
275                let mut m = serializer.serialize_map(Some(4))?;
276                m.serialize_entry("$type", "destroyFrozenFunds")?;
277                m.serialize_entry("frozenIdentifier", frozen)?;
278                m.serialize_entry("amount", &SafeU64(amount))?;
279                m.serialize_entry("publicNote", note)?;
280                m.end()
281            }
282            TokenEvent::Transfer(recipient, note, shared, private, amount) => {
283                let mut m = serializer.serialize_map(Some(6))?;
284                m.serialize_entry("$type", "transfer")?;
285                m.serialize_entry("recipient", recipient)?;
286                m.serialize_entry("publicNote", note)?;
287                m.serialize_entry("sharedEncryptedNote", &SafeOptEncNote(shared))?;
288                m.serialize_entry("privateEncryptedNote", &SafeOptEncNote(private))?;
289                m.serialize_entry("amount", &SafeU64(amount))?;
290                m.end()
291            }
292            TokenEvent::Claim(distribution_type, amount, note) => {
293                let mut m = serializer.serialize_map(Some(4))?;
294                m.serialize_entry("$type", "claim")?;
295                m.serialize_entry("distributionType", distribution_type)?;
296                m.serialize_entry("amount", &SafeU64(amount))?;
297                m.serialize_entry("publicNote", note)?;
298                m.end()
299            }
300            TokenEvent::EmergencyAction(action, note) => {
301                let mut m = serializer.serialize_map(Some(3))?;
302                m.serialize_entry("$type", "emergencyAction")?;
303                m.serialize_entry("action", action)?;
304                m.serialize_entry("publicNote", note)?;
305                m.end()
306            }
307            TokenEvent::ConfigUpdate(change, note) => {
308                let mut m = serializer.serialize_map(Some(3))?;
309                m.serialize_entry("$type", "configUpdate")?;
310                m.serialize_entry("configurationChange", change)?;
311                m.serialize_entry("publicNote", note)?;
312                m.end()
313            }
314            TokenEvent::ChangePriceForDirectPurchase(schedule, note) => {
315                let mut m = serializer.serialize_map(Some(3))?;
316                m.serialize_entry("$type", "changePriceForDirectPurchase")?;
317                m.serialize_entry("pricingSchedule", schedule)?;
318                m.serialize_entry("publicNote", note)?;
319                m.end()
320            }
321            TokenEvent::DirectPurchase(amount, credits) => {
322                let mut m = serializer.serialize_map(Some(3))?;
323                m.serialize_entry("$type", "directPurchase")?;
324                m.serialize_entry("amount", &SafeU64(amount))?;
325                m.serialize_entry("credits", &SafeU64(credits))?;
326                m.end()
327            }
328            TokenEvent::Shield(amount) => {
329                let mut m = serializer.serialize_map(Some(2))?;
330                m.serialize_entry("$type", "shield")?;
331                m.serialize_entry("amount", &SafeU64(amount))?;
332                m.end()
333            }
334            TokenEvent::Unshield(recipient, amount) => {
335                let mut m = serializer.serialize_map(Some(3))?;
336                m.serialize_entry("$type", "unshield")?;
337                m.serialize_entry("recipient", recipient)?;
338                m.serialize_entry("amount", &SafeU64(amount))?;
339                m.end()
340            }
341            TokenEvent::ShieldedTransfer => {
342                let mut m = serializer.serialize_map(Some(1))?;
343                m.serialize_entry("$type", "shieldedTransfer")?;
344                m.end()
345            }
346            TokenEvent::MintToPool(amount, actions_digest, note) => {
347                let mut m = serializer.serialize_map(Some(4))?;
348                m.serialize_entry("$type", "mintToPool")?;
349                m.serialize_entry("amount", &SafeU64(amount))?;
350                m.serialize_entry("actionsDigest", actions_digest)?;
351                m.serialize_entry("publicNote", note)?;
352                m.end()
353            }
354            TokenEvent::BurnFromPool(amount, actions_digest, note) => {
355                let mut m = serializer.serialize_map(Some(4))?;
356                m.serialize_entry("$type", "burnFromPool")?;
357                m.serialize_entry("amount", &SafeU64(amount))?;
358                m.serialize_entry("actionsDigest", actions_digest)?;
359                m.serialize_entry("publicNote", note)?;
360                m.end()
361            }
362            TokenEvent::ClaimToPool(amount) => {
363                let mut m = serializer.serialize_map(Some(2))?;
364                m.serialize_entry("$type", "claimToPool")?;
365                m.serialize_entry("amount", &SafeU64(amount))?;
366                m.end()
367            }
368            TokenEvent::DirectPurchaseToPool(amount, credits) => {
369                let mut m = serializer.serialize_map(Some(3))?;
370                m.serialize_entry("$type", "directPurchaseToPool")?;
371                m.serialize_entry("amount", &SafeU64(amount))?;
372                m.serialize_entry("credits", &SafeU64(credits))?;
373                m.end()
374            }
375        }
376    }
377}
378
379#[cfg(feature = "serde-conversion")]
380impl<'de> serde::Deserialize<'de> for TokenEvent {
381    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
382        use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
383
384        // Newtype wrappers that route u64 / encrypted-note deserialization
385        // through the json_safe helpers (accept both numeric and string forms
386        // for u64; accept either tuple-with-base64 or tuple-with-bytes).
387        #[derive(serde::Deserialize)]
388        #[serde(transparent)]
389        struct U64Safe(
390            #[serde(with = "crate::serialization::json::safe_integer::json_safe_u64")] u64,
391        );
392        #[derive(serde::Deserialize)]
393        #[serde(transparent)]
394        struct OptEncNote(
395            #[serde(
396                with = "crate::serialization::json::safe_integer::json_safe_option_encrypted_note"
397            )]
398            Option<(u32, u32, Vec<u8>)>,
399        );
400
401        struct V;
402
403        impl<'de> Visitor<'de> for V {
404            type Value = TokenEvent;
405
406            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
407                f.write_str("TokenEvent as a map with `$type` discriminator + variant fields")
408            }
409
410            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<TokenEvent, A::Error> {
411                let mut ty: Option<String> = None;
412                let mut amount: Option<u64> = None;
413                let mut credits: Option<u64> = None;
414                let mut recipient: Option<Identifier> = None;
415                let mut actions_digest: Option<Identifier> = None;
416                let mut burn_from: Option<Identifier> = None;
417                let mut frozen: Option<Identifier> = None;
418                let mut public_note: Option<String> = None;
419                let mut shared_note: Option<(u32, u32, Vec<u8>)> = None;
420                let mut private_note: Option<(u32, u32, Vec<u8>)> = None;
421                let mut distribution_type: Option<TokenDistributionTypeWithResolvedRecipient> =
422                    None;
423                let mut action: Option<TokenEmergencyAction> = None;
424                let mut configuration_change: Option<TokenConfigurationChangeItem> = None;
425                let mut pricing_schedule: Option<TokenPricingSchedule> = None;
426
427                while let Some(key) = map.next_key::<String>()? {
428                    match key.as_str() {
429                        "$type" => ty = Some(map.next_value()?),
430                        "amount" => amount = Some(map.next_value::<U64Safe>()?.0),
431                        "credits" => credits = Some(map.next_value::<U64Safe>()?.0),
432                        "recipient" => recipient = Some(map.next_value()?),
433                        "actionsDigest" => actions_digest = Some(map.next_value()?),
434                        "burnFromIdentifier" => burn_from = Some(map.next_value()?),
435                        "frozenIdentifier" => frozen = Some(map.next_value()?),
436                        "publicNote" => public_note = map.next_value()?,
437                        "sharedEncryptedNote" => {
438                            shared_note = map.next_value::<OptEncNote>()?.0;
439                        }
440                        "privateEncryptedNote" => {
441                            private_note = map.next_value::<OptEncNote>()?.0;
442                        }
443                        "distributionType" => distribution_type = Some(map.next_value()?),
444                        "action" => action = Some(map.next_value()?),
445                        "configurationChange" => configuration_change = Some(map.next_value()?),
446                        "pricingSchedule" => pricing_schedule = map.next_value()?,
447                        _ => {
448                            let _: IgnoredAny = map.next_value()?;
449                        }
450                    }
451                }
452
453                let ty = ty.ok_or_else(|| A::Error::missing_field("$type"))?;
454                match ty.as_str() {
455                    "mint" => Ok(TokenEvent::Mint(
456                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
457                        recipient.ok_or_else(|| A::Error::missing_field("recipient"))?,
458                        public_note,
459                    )),
460                    "burn" => Ok(TokenEvent::Burn(
461                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
462                        burn_from.ok_or_else(|| A::Error::missing_field("burnFromIdentifier"))?,
463                        public_note,
464                    )),
465                    "freeze" => Ok(TokenEvent::Freeze(
466                        frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
467                        public_note,
468                    )),
469                    "unfreeze" => Ok(TokenEvent::Unfreeze(
470                        frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
471                        public_note,
472                    )),
473                    "destroyFrozenFunds" => Ok(TokenEvent::DestroyFrozenFunds(
474                        frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
475                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
476                        public_note,
477                    )),
478                    "transfer" => Ok(TokenEvent::Transfer(
479                        recipient.ok_or_else(|| A::Error::missing_field("recipient"))?,
480                        public_note,
481                        shared_note,
482                        private_note,
483                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
484                    )),
485                    "claim" => Ok(TokenEvent::Claim(
486                        distribution_type
487                            .ok_or_else(|| A::Error::missing_field("distributionType"))?,
488                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
489                        public_note,
490                    )),
491                    "emergencyAction" => Ok(TokenEvent::EmergencyAction(
492                        action.ok_or_else(|| A::Error::missing_field("action"))?,
493                        public_note,
494                    )),
495                    "configUpdate" => Ok(TokenEvent::ConfigUpdate(
496                        configuration_change
497                            .ok_or_else(|| A::Error::missing_field("configurationChange"))?,
498                        public_note,
499                    )),
500                    "changePriceForDirectPurchase" => Ok(TokenEvent::ChangePriceForDirectPurchase(
501                        pricing_schedule,
502                        public_note,
503                    )),
504                    "directPurchase" => Ok(TokenEvent::DirectPurchase(
505                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
506                        credits.ok_or_else(|| A::Error::missing_field("credits"))?,
507                    )),
508                    "shield" => Ok(TokenEvent::Shield(
509                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
510                    )),
511                    "unshield" => Ok(TokenEvent::Unshield(
512                        recipient.ok_or_else(|| A::Error::missing_field("recipient"))?,
513                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
514                    )),
515                    "shieldedTransfer" => Ok(TokenEvent::ShieldedTransfer),
516                    "mintToPool" => Ok(TokenEvent::MintToPool(
517                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
518                        actions_digest.ok_or_else(|| A::Error::missing_field("actionsDigest"))?,
519                        public_note,
520                    )),
521                    "burnFromPool" => Ok(TokenEvent::BurnFromPool(
522                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
523                        actions_digest.ok_or_else(|| A::Error::missing_field("actionsDigest"))?,
524                        public_note,
525                    )),
526                    "claimToPool" => Ok(TokenEvent::ClaimToPool(
527                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
528                    )),
529                    "directPurchaseToPool" => Ok(TokenEvent::DirectPurchaseToPool(
530                        amount.ok_or_else(|| A::Error::missing_field("amount"))?,
531                        credits.ok_or_else(|| A::Error::missing_field("credits"))?,
532                    )),
533                    other => Err(A::Error::unknown_variant(
534                        other,
535                        &[
536                            "mint",
537                            "burn",
538                            "freeze",
539                            "unfreeze",
540                            "destroyFrozenFunds",
541                            "transfer",
542                            "claim",
543                            "emergencyAction",
544                            "configUpdate",
545                            "changePriceForDirectPurchase",
546                            "directPurchase",
547                            "shield",
548                            "unshield",
549                            "shieldedTransfer",
550                            "mintToPool",
551                            "burnFromPool",
552                            "claimToPool",
553                            "directPurchaseToPool",
554                        ],
555                    )),
556                }
557            }
558        }
559
560        deserializer.deserialize_map(V)
561    }
562}
563
564#[cfg(all(
565    test,
566    feature = "json-conversion",
567    feature = "value-conversion",
568    feature = "serde-conversion"
569))]
570pub(crate) mod json_convertible_tests {
571    use super::*;
572    use platform_value::platform_value;
573    use serde_json::json;
574
575    // `TokenEvent` has a custom `Serialize` / `Deserialize` impl emitting an
576    // internally-tagged flat shape: each variant maps positional tuple fields
577    // to named JSON keys (`amount` / `recipient` / `publicNote` / etc.).
578    // Round-trip covers a representative sample: `Mint` (3-tuple), `Freeze`
579    // (2-tuple including null note), `DirectPurchase` (2-tuple of u64 aliases).
580
581    pub(crate) fn mint_fixture() -> TokenEvent {
582        TokenEvent::Mint(
583            5_000,
584            Identifier::new([0xa1; 32]),
585            Some("genesis mint".to_string()),
586        )
587    }
588
589    #[test]
590    fn json_round_trip_mint() {
591        use crate::serialization::JsonConvertible;
592        let original = mint_fixture();
593        let json = original.to_json().expect("to_json");
594        // `TokenAmount` (u64) → `json_safe_u64` (number for small values,
595        // string above MAX_SAFE_INTEGER). `Identifier` → base58 string in HR.
596        assert_eq!(
597            json,
598            json!({
599                "$type": "mint",
600                "amount": 5_000,
601                "recipient": "Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp",
602                "publicNote": "genesis mint",
603            })
604        );
605        let recovered = TokenEvent::from_json(json).expect("from_json");
606        assert_eq!(original, recovered);
607    }
608
609    #[test]
610    fn json_round_trip_freeze_no_note() {
611        use crate::serialization::JsonConvertible;
612        let original = TokenEvent::Freeze(Identifier::new([0xb2; 32]), None);
613        let json = original.to_json().expect("to_json");
614        assert_eq!(
615            json,
616            json!({
617                "$type": "freeze",
618                "frozenIdentifier": "D2ZcUbtpG5sKq7XLeB4YnpNnTGSptKCxTddoNeydzJQq",
619                "publicNote": null,
620            })
621        );
622        let recovered = TokenEvent::from_json(json).expect("from_json");
623        assert_eq!(original, recovered);
624    }
625
626    #[test]
627    fn json_round_trip_direct_purchase() {
628        use crate::serialization::JsonConvertible;
629        let original = TokenEvent::DirectPurchase(100, 5_000);
630        let json = original.to_json().expect("to_json");
631        assert_eq!(
632            json,
633            json!({
634                "$type": "directPurchase",
635                "amount": 100,
636                "credits": 5_000,
637            })
638        );
639        let recovered = TokenEvent::from_json(json).expect("from_json");
640        assert_eq!(original, recovered);
641    }
642
643    #[test]
644    fn value_round_trip_mint() {
645        use crate::serialization::ValueConvertible;
646        let original = mint_fixture();
647        let value = original.to_object().expect("to_object");
648        // `TokenAmount` is `u64` → `Value::U64`. Identifier → `Value::Identifier`.
649        assert_eq!(
650            value,
651            platform_value!({
652                "$type": "mint",
653                "amount": 5_000u64,
654                "recipient": Identifier::new([0xa1; 32]),
655                "publicNote": "genesis mint",
656            })
657        );
658        let recovered = TokenEvent::from_object(value).expect("from_object");
659        assert_eq!(original, recovered);
660    }
661
662    #[test]
663    fn value_round_trip_freeze_no_note() {
664        use crate::serialization::ValueConvertible;
665        let original = TokenEvent::Freeze(Identifier::new([0xb2; 32]), None);
666        let value = original.to_object().expect("to_object");
667        assert_eq!(
668            value,
669            platform_value!({
670                "$type": "freeze",
671                "frozenIdentifier": Identifier::new([0xb2; 32]),
672                "publicNote": null,
673            })
674        );
675        let recovered = TokenEvent::from_object(value).expect("from_object");
676        assert_eq!(original, recovered);
677    }
678
679    #[test]
680    fn value_round_trip_direct_purchase() {
681        use crate::serialization::ValueConvertible;
682        let original = TokenEvent::DirectPurchase(100, 5_000);
683        let value = original.to_object().expect("to_object");
684        // `TokenAmount` and `Credits` are both `u64`.
685        assert_eq!(
686            value,
687            platform_value!({
688                "$type": "directPurchase",
689                "amount": 100u64,
690                "credits": 5_000u64,
691            })
692        );
693        let recovered = TokenEvent::from_object(value).expect("from_object");
694        assert_eq!(original, recovered);
695    }
696}
697
698impl fmt::Display for TokenEvent {
699    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700        match self {
701            TokenEvent::Mint(amount, recipient, note) => {
702                write!(f, "Mint {} to {}{}", amount, recipient, format_note(note))
703            }
704            TokenEvent::Burn(amount, burn_from_identifier, note) => {
705                write!(
706                    f,
707                    "Burn {} from {}{}",
708                    amount,
709                    burn_from_identifier,
710                    format_note(note)
711                )
712            }
713            TokenEvent::Freeze(identity, note) => {
714                write!(f, "Freeze {}{}", identity, format_note(note))
715            }
716            TokenEvent::Unfreeze(identity, note) => {
717                write!(f, "Unfreeze {}{}", identity, format_note(note))
718            }
719            TokenEvent::DestroyFrozenFunds(identity, amount, note) => {
720                write!(
721                    f,
722                    "Destroy {} frozen from {}{}",
723                    amount,
724                    identity,
725                    format_note(note)
726                )
727            }
728            TokenEvent::Transfer(to, note, _, _, amount) => {
729                write!(f, "Transfer {} to {}{}", amount, to, format_note(note))
730            }
731            TokenEvent::Claim(recipient, amount, note) => {
732                write!(
733                    f,
734                    "Claim {} by {:?}{}",
735                    amount,
736                    recipient,
737                    format_note(note)
738                )
739            }
740            TokenEvent::EmergencyAction(action, note) => {
741                write!(f, "Emergency action {:?}{}", action, format_note(note))
742            }
743            TokenEvent::ConfigUpdate(change, note) => {
744                write!(f, "Configuration update {:?}{}", change, format_note(note))
745            }
746            TokenEvent::ChangePriceForDirectPurchase(schedule, note) => match schedule {
747                Some(s) => write!(f, "Change price schedule to {:?}{}", s, format_note(note)),
748                None => write!(f, "Disable direct purchase{}", format_note(note)),
749            },
750            TokenEvent::DirectPurchase(amount, credits) => {
751                write!(f, "Direct purchase of {} for {} credits", amount, credits)
752            }
753            TokenEvent::Shield(amount) => write!(f, "Shield {} into the token pool", amount),
754            TokenEvent::Unshield(to, amount) => {
755                write!(f, "Unshield {} from the token pool to {}", amount, to)
756            }
757            TokenEvent::ShieldedTransfer => write!(f, "Shielded transfer inside the token pool"),
758            TokenEvent::MintToPool(amount, _, note) => {
759                write!(
760                    f,
761                    "Mint {} into the token pool{}",
762                    amount,
763                    format_note(note)
764                )
765            }
766            TokenEvent::BurnFromPool(amount, _, note) => {
767                write!(
768                    f,
769                    "Burn {} from the token pool{}",
770                    amount,
771                    format_note(note)
772                )
773            }
774            TokenEvent::ClaimToPool(amount) => {
775                write!(f, "Claim {} into the token pool", amount)
776            }
777            TokenEvent::DirectPurchaseToPool(amount, credits) => write!(
778                f,
779                "Direct purchase of {} into the token pool for {} credits",
780                amount, credits
781            ),
782        }
783    }
784}
785
786fn format_note(note: &Option<String>) -> String {
787    match note {
788        Some(n) => format!(" (note: {})", n),
789        None => String::new(),
790    }
791}
792
793impl TokenEvent {
794    pub fn associated_document_type_name(&self) -> &str {
795        match self {
796            TokenEvent::Mint(..) => "mint",
797            TokenEvent::Burn(..) => "burn",
798            TokenEvent::Freeze(..) => "freeze",
799            TokenEvent::Unfreeze(..) => "unfreeze",
800            TokenEvent::DestroyFrozenFunds(..) => "destroyFrozenFunds",
801            TokenEvent::Transfer(..) => "transfer",
802            TokenEvent::Claim(..) => "claim",
803            TokenEvent::EmergencyAction(..) => "emergencyAction",
804            TokenEvent::ConfigUpdate(..) => "configUpdate",
805            TokenEvent::DirectPurchase(..) => "directPurchase",
806            TokenEvent::ChangePriceForDirectPurchase(..) => "directPricing",
807            TokenEvent::Shield(..) => "shield",
808            TokenEvent::Unshield(..) => "unshield",
809            TokenEvent::ShieldedTransfer => "shieldedTransfer",
810            TokenEvent::MintToPool(..) => "mintToPool",
811            TokenEvent::BurnFromPool(..) => "burnFromPool",
812            TokenEvent::ClaimToPool(..) => "claimToPool",
813            TokenEvent::DirectPurchaseToPool(..) => "directPurchaseToPool",
814        }
815    }
816
817    /// Returns a reference to the public note if the variant includes one.
818    ///
819    /// Every variant is listed: the co-signers of a pending group action read the proposer's
820    /// note here while deciding whether to sign, so a variant that carries one and is not
821    /// listed shows them nothing. Adding a variant is then a compile error rather than a note
822    /// that silently goes missing.
823    pub fn public_note(&self) -> Option<&str> {
824        match self {
825            TokenEvent::Mint(_, _, note)
826            | TokenEvent::Burn(_, _, note)
827            | TokenEvent::Freeze(_, note)
828            | TokenEvent::Unfreeze(_, note)
829            | TokenEvent::DestroyFrozenFunds(_, _, note)
830            | TokenEvent::Transfer(_, note, _, _, _)
831            | TokenEvent::Claim(_, _, note)
832            | TokenEvent::EmergencyAction(_, note)
833            | TokenEvent::ConfigUpdate(_, note)
834            | TokenEvent::ChangePriceForDirectPurchase(_, note)
835            | TokenEvent::MintToPool(_, _, note)
836            | TokenEvent::BurnFromPool(_, _, note) => note.as_deref(),
837            TokenEvent::DirectPurchase(_, _)
838            | TokenEvent::Shield(_)
839            | TokenEvent::Unshield(_, _)
840            | TokenEvent::ShieldedTransfer
841            | TokenEvent::ClaimToPool(_)
842            | TokenEvent::DirectPurchaseToPool(_, _) => None,
843        }
844    }
845
846    pub fn associated_document_type<'a>(
847        &self,
848        token_history_contract: &'a DataContract,
849    ) -> Result<DocumentTypeRef<'a>, ProtocolError> {
850        Ok(token_history_contract.document_type_for_name(self.associated_document_type_name())?)
851    }
852
853    pub fn build_historical_document_owned(
854        self,
855        token_id: Identifier,
856        owner_id: Identifier,
857        owner_nonce: IdentityNonce,
858        block_info: &BlockInfo,
859        platform_version: &PlatformVersion,
860    ) -> Result<Document, ProtocolError> {
861        let document_id = Document::generate_document_id_v0(
862            &token_id,
863            &owner_id,
864            format!("history_{}", self.associated_document_type_name()).as_str(),
865            owner_nonce.to_be_bytes().as_slice(),
866        );
867
868        let properties = match self {
869            TokenEvent::Mint(mint_amount, recipient_id, public_note) => {
870                let mut properties = BTreeMap::from([
871                    ("tokenId".to_string(), token_id.into()),
872                    ("recipientId".to_string(), recipient_id.into()),
873                    ("amount".to_string(), mint_amount.into()),
874                ]);
875                if let Some(note) = public_note {
876                    properties.insert("note".to_string(), note.into());
877                }
878                properties
879            }
880            TokenEvent::Burn(burn_amount, burn_from_identifier, public_note) => {
881                let mut properties = BTreeMap::from([
882                    ("tokenId".to_string(), token_id.into()),
883                    ("burnFromId".to_string(), burn_from_identifier.into()),
884                    ("amount".to_string(), burn_amount.into()),
885                ]);
886                if let Some(note) = public_note {
887                    properties.insert("note".to_string(), note.into());
888                }
889                properties
890            }
891            TokenEvent::Transfer(
892                to,
893                public_note,
894                token_event_shared_encrypted_note,
895                token_event_personal_encrypted_note,
896                amount,
897            ) => {
898                let mut properties = BTreeMap::from([
899                    ("tokenId".to_string(), token_id.into()),
900                    ("amount".to_string(), amount.into()),
901                    ("toIdentityId".to_string(), to.into()),
902                ]);
903                if let Some(note) = public_note {
904                    properties.insert("publicNote".to_string(), note.into());
905                }
906                if let Some((sender_key_index, recipient_key_index, note)) =
907                    token_event_shared_encrypted_note
908                {
909                    properties.insert("encryptedSharedNote".to_string(), note.into());
910                    properties.insert("senderKeyIndex".to_string(), sender_key_index.into());
911                    properties.insert("recipientKeyIndex".to_string(), recipient_key_index.into());
912                }
913
914                if let Some((root_encryption_key_index, derivation_encryption_key_index, note)) =
915                    token_event_personal_encrypted_note
916                {
917                    properties.insert("encryptedPersonalNote".to_string(), note.into());
918                    properties.insert(
919                        "rootEncryptionKeyIndex".to_string(),
920                        root_encryption_key_index.into(),
921                    );
922                    properties.insert(
923                        "derivationEncryptionKeyIndex".to_string(),
924                        derivation_encryption_key_index.into(),
925                    );
926                }
927                properties
928            }
929            TokenEvent::Freeze(frozen_identity_id, public_note) => {
930                let mut properties = BTreeMap::from([
931                    ("tokenId".to_string(), token_id.into()),
932                    ("frozenIdentityId".to_string(), frozen_identity_id.into()),
933                ]);
934                if let Some(note) = public_note {
935                    properties.insert("note".to_string(), note.into());
936                }
937                properties
938            }
939            TokenEvent::Unfreeze(frozen_identity_id, public_note) => {
940                let mut properties = BTreeMap::from([
941                    ("tokenId".to_string(), token_id.into()),
942                    ("frozenIdentityId".to_string(), frozen_identity_id.into()),
943                ]);
944                if let Some(note) = public_note {
945                    properties.insert("note".to_string(), note.into());
946                }
947                properties
948            }
949            TokenEvent::DestroyFrozenFunds(frozen_identity_id, amount, public_note) => {
950                let mut properties = BTreeMap::from([
951                    ("tokenId".to_string(), token_id.into()),
952                    ("frozenIdentityId".to_string(), frozen_identity_id.into()),
953                    ("destroyedAmount".to_string(), amount.into()),
954                ]);
955                if let Some(note) = public_note {
956                    properties.insert("note".to_string(), note.into());
957                }
958                properties
959            }
960            TokenEvent::EmergencyAction(action, public_note) => {
961                let mut properties = BTreeMap::from([
962                    ("tokenId".to_string(), token_id.into()),
963                    ("action".to_string(), (action as u8).into()),
964                ]);
965                if let Some(note) = public_note {
966                    properties.insert("note".to_string(), note.into());
967                }
968                properties
969            }
970            TokenEvent::ConfigUpdate(configuration_change_item, public_note) => {
971                let mut properties = BTreeMap::from([
972                    ("tokenId".to_string(), token_id.into()),
973                    (
974                        "changeItemType".to_string(),
975                        configuration_change_item.u8_item_index().into(),
976                    ),
977                    (
978                        "changeItem".to_string(),
979                        configuration_change_item
980                            .serialize_consume_to_bytes_with_platform_version(platform_version)?
981                            .into(),
982                    ),
983                ]);
984                if let Some(note) = public_note {
985                    properties.insert("note".to_string(), note.into());
986                }
987                properties
988            }
989            TokenEvent::Claim(recipient, amount, public_note) => {
990                let (recipient_type, recipient_id, distribution_type) = match recipient {
991                    TokenDistributionTypeWithResolvedRecipient::PreProgrammed(identifier) => {
992                        (1u8, identifier, 0u8)
993                    }
994                    TokenDistributionTypeWithResolvedRecipient::Perpetual(
995                        TokenDistributionResolvedRecipient::ContractOwnerIdentity(identifier),
996                    ) => (0, identifier, 1),
997                    TokenDistributionTypeWithResolvedRecipient::Perpetual(
998                        TokenDistributionResolvedRecipient::Identity(identifier),
999                    ) => (1, identifier, 1),
1000                    TokenDistributionTypeWithResolvedRecipient::Perpetual(
1001                        TokenDistributionResolvedRecipient::Evonode(identifier),
1002                    ) => (2, identifier, 1),
1003                    TokenDistributionTypeWithResolvedRecipient::OncePerIdentity(identifier) => {
1004                        (1, identifier, 2)
1005                    }
1006                };
1007
1008                let mut properties = BTreeMap::from([
1009                    ("tokenId".to_string(), token_id.into()),
1010                    ("recipientType".to_string(), recipient_type.into()),
1011                    ("recipientId".to_string(), recipient_id.into()),
1012                    ("distributionType".to_string(), distribution_type.into()),
1013                    ("amount".to_string(), amount.into()),
1014                ]);
1015
1016                if let Some(note) = public_note {
1017                    properties.insert("note".to_string(), note.into());
1018                }
1019                properties
1020            }
1021            TokenEvent::ChangePriceForDirectPurchase(price, note) => {
1022                let mut properties = BTreeMap::from([("tokenId".to_string(), token_id.into())]);
1023
1024                if let Some(price_schedule) = price {
1025                    properties.insert(
1026                        "priceSchedule".to_string(),
1027                        price_schedule
1028                            .serialize_consume_to_bytes_with_platform_version(platform_version)?
1029                            .into(),
1030                    );
1031                }
1032
1033                if let Some(note) = note {
1034                    properties.insert("note".to_string(), note.into());
1035                }
1036
1037                properties
1038            }
1039            TokenEvent::DirectPurchase(amount, total_cost) => BTreeMap::from([
1040                ("tokenId".to_string(), token_id.into()),
1041                ("tokenAmount".to_string(), amount.into()),
1042                ("purchaseCost".to_string(), total_cost.into()),
1043            ]),
1044            TokenEvent::Shield(amount) => BTreeMap::from([
1045                ("tokenId".to_string(), token_id.into()),
1046                ("amount".to_string(), amount.into()),
1047            ]),
1048            TokenEvent::Unshield(recipient_id, amount) => BTreeMap::from([
1049                ("tokenId".to_string(), token_id.into()),
1050                ("recipientId".to_string(), recipient_id.into()),
1051                ("amount".to_string(), amount.into()),
1052            ]),
1053            TokenEvent::ShieldedTransfer => {
1054                BTreeMap::from([("tokenId".to_string(), token_id.into())])
1055            }
1056            TokenEvent::MintToPool(amount, _, _) | TokenEvent::BurnFromPool(amount, _, _) => {
1057                BTreeMap::from([
1058                    ("tokenId".to_string(), token_id.into()),
1059                    ("amount".to_string(), amount.into()),
1060                ])
1061            }
1062            TokenEvent::ClaimToPool(amount) => BTreeMap::from([
1063                ("tokenId".to_string(), token_id.into()),
1064                ("amount".to_string(), amount.into()),
1065            ]),
1066            TokenEvent::DirectPurchaseToPool(amount, total_cost) => BTreeMap::from([
1067                ("tokenId".to_string(), token_id.into()),
1068                ("tokenAmount".to_string(), amount.into()),
1069                ("purchaseCost".to_string(), total_cost.into()),
1070            ]),
1071        };
1072
1073        let document: Document = DocumentV0 {
1074            contract_version: None,
1075            id: document_id,
1076            owner_id,
1077            properties,
1078            revision: None,
1079            created_at: Some(block_info.time_ms),
1080            updated_at: None,
1081            transferred_at: None,
1082            created_at_block_height: Some(block_info.height),
1083            updated_at_block_height: None,
1084            transferred_at_block_height: None,
1085            created_at_core_block_height: None,
1086            updated_at_core_block_height: None,
1087            transferred_at_core_block_height: None,
1088            creator_id: None,
1089            moderated_at: None,
1090            moderated_by: None,
1091        }
1092        .into();
1093
1094        Ok(document)
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use super::*;
1101
1102    fn test_id() -> Identifier {
1103        Identifier::from([1u8; 32])
1104    }
1105
1106    fn test_id_2() -> Identifier {
1107        Identifier::from([2u8; 32])
1108    }
1109
1110    // ---- associated_document_type_name tests ----
1111
1112    #[test]
1113    fn associated_name_mint() {
1114        let event = TokenEvent::Mint(0, test_id(), None);
1115        assert_eq!(event.associated_document_type_name(), "mint");
1116    }
1117
1118    #[test]
1119    fn associated_name_burn() {
1120        let event = TokenEvent::Burn(0, test_id(), None);
1121        assert_eq!(event.associated_document_type_name(), "burn");
1122    }
1123
1124    #[test]
1125    fn associated_name_freeze() {
1126        let event = TokenEvent::Freeze(test_id(), None);
1127        assert_eq!(event.associated_document_type_name(), "freeze");
1128    }
1129
1130    #[test]
1131    fn associated_name_unfreeze() {
1132        let event = TokenEvent::Unfreeze(test_id(), None);
1133        assert_eq!(event.associated_document_type_name(), "unfreeze");
1134    }
1135
1136    #[test]
1137    fn associated_name_destroy_frozen_funds() {
1138        let event = TokenEvent::DestroyFrozenFunds(test_id(), 0, None);
1139        assert_eq!(event.associated_document_type_name(), "destroyFrozenFunds");
1140    }
1141
1142    #[test]
1143    fn associated_name_transfer() {
1144        let event = TokenEvent::Transfer(test_id(), None, None, None, 0);
1145        assert_eq!(event.associated_document_type_name(), "transfer");
1146    }
1147
1148    #[test]
1149    fn associated_name_claim() {
1150        let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id());
1151        let event = TokenEvent::Claim(recipient, 0, None);
1152        assert_eq!(event.associated_document_type_name(), "claim");
1153    }
1154
1155    #[test]
1156    fn associated_name_emergency_action() {
1157        let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None);
1158        assert_eq!(event.associated_document_type_name(), "emergencyAction");
1159    }
1160
1161    #[test]
1162    fn associated_name_config_update() {
1163        let event = TokenEvent::ConfigUpdate(
1164            TokenConfigurationChangeItem::TokenConfigurationNoChange,
1165            None,
1166        );
1167        assert_eq!(event.associated_document_type_name(), "configUpdate");
1168    }
1169
1170    #[test]
1171    fn associated_name_direct_purchase() {
1172        let event = TokenEvent::DirectPurchase(0, 0);
1173        assert_eq!(event.associated_document_type_name(), "directPurchase");
1174    }
1175
1176    #[test]
1177    fn associated_name_change_price() {
1178        let event = TokenEvent::ChangePriceForDirectPurchase(None, None);
1179        assert_eq!(event.associated_document_type_name(), "directPricing");
1180    }
1181
1182    // ---- all associated_document_type_name values are distinct ----
1183
1184    #[test]
1185    fn all_document_type_names_are_unique() {
1186        let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id());
1187        let events: Vec<TokenEvent> = vec![
1188            TokenEvent::Mint(0, test_id(), None),
1189            TokenEvent::Burn(0, test_id(), None),
1190            TokenEvent::Freeze(test_id(), None),
1191            TokenEvent::Unfreeze(test_id(), None),
1192            TokenEvent::DestroyFrozenFunds(test_id(), 0, None),
1193            TokenEvent::Transfer(test_id(), None, None, None, 0),
1194            TokenEvent::Claim(recipient, 0, None),
1195            TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None),
1196            TokenEvent::ConfigUpdate(
1197                TokenConfigurationChangeItem::TokenConfigurationNoChange,
1198                None,
1199            ),
1200            TokenEvent::DirectPurchase(0, 0),
1201            TokenEvent::ChangePriceForDirectPurchase(None, None),
1202        ];
1203        let names: Vec<&str> = events
1204            .iter()
1205            .map(|e| e.associated_document_type_name())
1206            .collect();
1207        let mut unique = names.clone();
1208        unique.sort();
1209        unique.dedup();
1210        assert_eq!(
1211            names.len(),
1212            unique.len(),
1213            "Duplicate document type names found"
1214        );
1215    }
1216
1217    // ---- format_note helper ----
1218
1219    #[test]
1220    fn format_note_none_returns_empty() {
1221        assert_eq!(format_note(&None), "");
1222    }
1223
1224    #[test]
1225    fn format_note_some_returns_formatted() {
1226        assert_eq!(format_note(&Some("hello".to_string())), " (note: hello)");
1227    }
1228}
1229
1230#[cfg(test)]
1231mod public_note_tests {
1232    use super::*;
1233    use crate::group::action_event::GroupActionEvent;
1234
1235    fn actions_digest() -> Identifier {
1236        Identifier::from([7u8; 32])
1237    }
1238
1239    /// The co-signers of a pending group action read the proposer's note off the event to
1240    /// decide whether to sign it. A pool mint or burn carries one like any other proposal.
1241    #[test]
1242    fn should_show_the_proposers_note_on_a_pool_mint_or_burn() {
1243        let mint = TokenEvent::MintToPool(
1244            100,
1245            actions_digest(),
1246            Some("quarterly issuance".to_string()),
1247        );
1248        assert_eq!(mint.public_note(), Some("quarterly issuance"));
1249        assert_eq!(
1250            GroupActionEvent::TokenEvent(mint).public_note(),
1251            Some("quarterly issuance")
1252        );
1253
1254        let burn = TokenEvent::BurnFromPool(
1255            40,
1256            actions_digest(),
1257            Some("retiring treasury notes".to_string()),
1258        );
1259        assert_eq!(burn.public_note(), Some("retiring treasury notes"));
1260        assert_eq!(
1261            GroupActionEvent::TokenEvent(burn).public_note(),
1262            Some("retiring treasury notes")
1263        );
1264    }
1265
1266    /// A pool mint or burn the proposer left unannotated reads as no note, not as an empty one.
1267    #[test]
1268    fn should_report_no_note_on_an_unannotated_pool_mint_or_burn() {
1269        assert_eq!(
1270            TokenEvent::MintToPool(100, actions_digest(), None).public_note(),
1271            None
1272        );
1273        assert_eq!(
1274            TokenEvent::BurnFromPool(40, actions_digest(), None).public_note(),
1275            None
1276        );
1277    }
1278}