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 = "json-conversion")]
14use crate::serialization::JsonConvertible;
15#[cfg(feature = "value-conversion")]
16use crate::serialization::ValueConvertible;
17use bincode::{Decode, Encode};
18use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize};
19use platform_value::Identifier;
20use platform_version::version::PlatformVersion;
21use std::collections::BTreeMap;
22use std::fmt;
23
24pub type TokenEventPublicNote = Option<String>;
25pub type TokenEventSharedEncryptedNote = Option<SharedEncryptedNote>;
26pub type TokenEventPersonalEncryptedNote = Option<(
27    RootEncryptionKeyIndex,
28    DerivationEncryptionKeyIndex,
29    Vec<u8>,
30)>;
31use crate::serialization::PlatformSerializableWithPlatformVersion;
32use crate::tokens::emergency_action::TokenEmergencyAction;
33use crate::tokens::token_pricing_schedule::TokenPricingSchedule;
34use crate::tokens::SharedEncryptedNote;
35use crate::ProtocolError;
36
37/// Alias representing the identity that will receive tokens or other effects from a token operation.
38pub type RecipientIdentifier = Identifier;
39
40/// Alias representing the identity that will have tokens burned from their account.
41pub type BurnFromIdentifier = Identifier;
42
43/// Alias representing the identity performing a token purchase.
44pub type PurchaserIdentifier = Identifier;
45
46/// Alias representing the identity whose tokens are subject to freezing or unfreezing.
47pub type FrozenIdentifier = Identifier;
48
49/// Represents a recorded token-related operation for use in historical documents and group actions.
50///
51/// `TokenEvent` is designed to encapsulate a single logical token operation,
52/// such as minting, burning, transferring, or freezing tokens. These events are typically:
53///
54/// - **Persisted as historical records** of state transitions, enabling auditability and tracking.
55/// - **Used in group (multisig) actions**, where multiple identities collaborate to authorize complex transitions.
56///
57/// This enum includes rich metadata for each type of operation, such as optional notes (plaintext or encrypted),
58/// involved identities, and amounts. It is **externally versioned** and marked as `unversioned` in platform serialization,
59/// meaning each variant is self-contained without requiring version dispatching logic.
60#[derive(
61    Debug, PartialEq, PartialOrd, Clone, Eq, Encode, Decode, PlatformDeserialize, PlatformSerialize,
62)]
63#[cfg_attr(
64    feature = "serde-conversion",
65    derive(serde::Serialize, serde::Deserialize),
66    serde(tag = "type", content = "data", rename_all = "camelCase")
67)]
68#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
69#[platform_serialize(unversioned)]
70pub enum TokenEvent {
71    /// Event representing the minting of tokens to a recipient.
72    ///
73    /// - `TokenAmount`: The amount of tokens minted.
74    /// - `RecipientIdentifier`: The identity receiving the minted tokens.
75    /// - `TokenEventPublicNote`: Optional note associated with the event.
76    Mint(TokenAmount, RecipientIdentifier, TokenEventPublicNote),
77
78    /// Event representing the burning of tokens, removing them from circulation.
79    ///
80    /// - `TokenAmount`: The amount of tokens burned.
81    /// - `BurnFromIdentifier`: The account to burn from.
82    /// - `TokenEventPublicNote`: Optional note associated with the event.
83    Burn(TokenAmount, BurnFromIdentifier, TokenEventPublicNote),
84
85    /// Event representing freezing of tokens for a specific identity.
86    ///
87    /// - `FrozenIdentifier`: The identity whose tokens are frozen.
88    /// - `TokenEventPublicNote`: Optional note associated with the event.
89    Freeze(FrozenIdentifier, TokenEventPublicNote),
90
91    /// Event representing unfreezing of tokens for a specific identity.
92    ///
93    /// - `FrozenIdentifier`: The identity whose tokens are unfrozen.
94    /// - `TokenEventPublicNote`: Optional note associated with the event.
95    Unfreeze(FrozenIdentifier, TokenEventPublicNote),
96
97    /// Event representing destruction of tokens that were previously frozen.
98    ///
99    /// - `FrozenIdentifier`: The identity whose frozen tokens are destroyed.
100    /// - `TokenAmount`: The amount of frozen tokens destroyed.
101    /// - `TokenEventPublicNote`: Optional note associated with the event.
102    DestroyFrozenFunds(FrozenIdentifier, TokenAmount, TokenEventPublicNote),
103
104    /// Event representing a transfer of tokens from one identity to another.
105    ///
106    /// - `RecipientIdentifier`: The recipient of the tokens.
107    /// - `TokenEventPublicNote`: Optional plaintext note.
108    /// - `TokenEventSharedEncryptedNote`: Optional shared encrypted metadata (multi-party).
109    /// - `TokenEventPersonalEncryptedNote`: Optional private encrypted metadata (recipient-only).
110    /// - `TokenAmount`: The amount of tokens transferred.
111    Transfer(
112        RecipientIdentifier,
113        TokenEventPublicNote,
114        TokenEventSharedEncryptedNote,
115        TokenEventPersonalEncryptedNote,
116        TokenAmount,
117    ),
118
119    /// Event representing a claim of tokens from a distribution pool or source.
120    ///
121    /// - `TokenDistributionTypeWithResolvedRecipient`: Type and resolved recipient of the claim.
122    /// - `TokenAmount`: The amount of tokens claimed.
123    /// - `TokenEventPublicNote`: Optional note associated with the event.
124    Claim(
125        TokenDistributionTypeWithResolvedRecipient,
126        TokenAmount,
127        TokenEventPublicNote,
128    ),
129
130    /// Event representing an emergency action taken on a token or identity.
131    ///
132    /// - `TokenEmergencyAction`: The type of emergency action performed.
133    /// - `TokenEventPublicNote`: Optional note associated with the event.
134    EmergencyAction(TokenEmergencyAction, TokenEventPublicNote),
135
136    /// Event representing an update to the configuration of a token.
137    ///
138    /// - `TokenConfigurationChangeItem`: The configuration change that was applied.
139    /// - `TokenEventPublicNote`: Optional note associated with the event.
140    ConfigUpdate(TokenConfigurationChangeItem, TokenEventPublicNote),
141
142    /// Event representing a change in the direct purchase price of a token.
143    ///
144    /// - `Option<TokenPricingSchedule>`: The new pricing schedule. `None` disables direct purchase.
145    /// - `TokenEventPublicNote`: Optional note associated with the event.
146    ChangePriceForDirectPurchase(Option<TokenPricingSchedule>, TokenEventPublicNote),
147
148    /// Event representing the direct purchase of tokens by a user.
149    ///
150    /// - `TokenAmount`: The amount of tokens purchased.
151    /// - `Credits`: The number of credits paid.
152    DirectPurchase(TokenAmount, Credits),
153}
154
155// Manual impl because TokenEvent is a flat enum with u64-alias tuple variants
156// (TokenAmount, Credits). `#[derive(JsonConvertible)]` would fail: it asserts inner
157// variant types implement `JsonSafeFields`, but TokenAmount/Credits are u64 aliases
158// which intentionally don't. The `#[json_safe_fields]` macro can't annotate tuple
159// variant fields either. Safety is ensured by manual `impl JsonSafeFields` in
160// safe_fields.rs — the developer takes responsibility for these fields.
161#[cfg(feature = "json-conversion")]
162impl JsonConvertible for TokenEvent {}
163
164impl fmt::Display for TokenEvent {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        match self {
167            TokenEvent::Mint(amount, recipient, note) => {
168                write!(f, "Mint {} to {}{}", amount, recipient, format_note(note))
169            }
170            TokenEvent::Burn(amount, burn_from_identifier, note) => {
171                write!(
172                    f,
173                    "Burn {} from {}{}",
174                    amount,
175                    burn_from_identifier,
176                    format_note(note)
177                )
178            }
179            TokenEvent::Freeze(identity, note) => {
180                write!(f, "Freeze {}{}", identity, format_note(note))
181            }
182            TokenEvent::Unfreeze(identity, note) => {
183                write!(f, "Unfreeze {}{}", identity, format_note(note))
184            }
185            TokenEvent::DestroyFrozenFunds(identity, amount, note) => {
186                write!(
187                    f,
188                    "Destroy {} frozen from {}{}",
189                    amount,
190                    identity,
191                    format_note(note)
192                )
193            }
194            TokenEvent::Transfer(to, note, _, _, amount) => {
195                write!(f, "Transfer {} to {}{}", amount, to, format_note(note))
196            }
197            TokenEvent::Claim(recipient, amount, note) => {
198                write!(
199                    f,
200                    "Claim {} by {:?}{}",
201                    amount,
202                    recipient,
203                    format_note(note)
204                )
205            }
206            TokenEvent::EmergencyAction(action, note) => {
207                write!(f, "Emergency action {:?}{}", action, format_note(note))
208            }
209            TokenEvent::ConfigUpdate(change, note) => {
210                write!(f, "Configuration update {:?}{}", change, format_note(note))
211            }
212            TokenEvent::ChangePriceForDirectPurchase(schedule, note) => match schedule {
213                Some(s) => write!(f, "Change price schedule to {:?}{}", s, format_note(note)),
214                None => write!(f, "Disable direct purchase{}", format_note(note)),
215            },
216            TokenEvent::DirectPurchase(amount, credits) => {
217                write!(f, "Direct purchase of {} for {} credits", amount, credits)
218            }
219        }
220    }
221}
222
223fn format_note(note: &Option<String>) -> String {
224    match note {
225        Some(n) => format!(" (note: {})", n),
226        None => String::new(),
227    }
228}
229
230impl TokenEvent {
231    pub fn associated_document_type_name(&self) -> &str {
232        match self {
233            TokenEvent::Mint(..) => "mint",
234            TokenEvent::Burn(..) => "burn",
235            TokenEvent::Freeze(..) => "freeze",
236            TokenEvent::Unfreeze(..) => "unfreeze",
237            TokenEvent::DestroyFrozenFunds(..) => "destroyFrozenFunds",
238            TokenEvent::Transfer(..) => "transfer",
239            TokenEvent::Claim(..) => "claim",
240            TokenEvent::EmergencyAction(..) => "emergencyAction",
241            TokenEvent::ConfigUpdate(..) => "configUpdate",
242            TokenEvent::DirectPurchase(..) => "directPurchase",
243            TokenEvent::ChangePriceForDirectPurchase(..) => "directPricing",
244        }
245    }
246
247    /// Returns a reference to the public note if the variant includes one.
248    pub fn public_note(&self) -> Option<&str> {
249        match self {
250            TokenEvent::Mint(_, _, Some(note))
251            | TokenEvent::Burn(_, _, Some(note))
252            | TokenEvent::Freeze(_, Some(note))
253            | TokenEvent::Unfreeze(_, Some(note))
254            | TokenEvent::DestroyFrozenFunds(_, _, Some(note))
255            | TokenEvent::Transfer(_, Some(note), _, _, _)
256            | TokenEvent::Claim(_, _, Some(note))
257            | TokenEvent::EmergencyAction(_, Some(note))
258            | TokenEvent::ConfigUpdate(_, Some(note))
259            | TokenEvent::ChangePriceForDirectPurchase(_, Some(note)) => Some(note),
260            _ => None,
261        }
262    }
263
264    pub fn associated_document_type<'a>(
265        &self,
266        token_history_contract: &'a DataContract,
267    ) -> Result<DocumentTypeRef<'a>, ProtocolError> {
268        Ok(token_history_contract.document_type_for_name(self.associated_document_type_name())?)
269    }
270
271    pub fn build_historical_document_owned(
272        self,
273        token_id: Identifier,
274        owner_id: Identifier,
275        owner_nonce: IdentityNonce,
276        block_info: &BlockInfo,
277        platform_version: &PlatformVersion,
278    ) -> Result<Document, ProtocolError> {
279        let document_id = Document::generate_document_id_v0(
280            &token_id,
281            &owner_id,
282            format!("history_{}", self.associated_document_type_name()).as_str(),
283            owner_nonce.to_be_bytes().as_slice(),
284        );
285
286        let properties = match self {
287            TokenEvent::Mint(mint_amount, recipient_id, public_note) => {
288                let mut properties = BTreeMap::from([
289                    ("tokenId".to_string(), token_id.into()),
290                    ("recipientId".to_string(), recipient_id.into()),
291                    ("amount".to_string(), mint_amount.into()),
292                ]);
293                if let Some(note) = public_note {
294                    properties.insert("note".to_string(), note.into());
295                }
296                properties
297            }
298            TokenEvent::Burn(burn_amount, burn_from_identifier, public_note) => {
299                let mut properties = BTreeMap::from([
300                    ("tokenId".to_string(), token_id.into()),
301                    ("burnFromId".to_string(), burn_from_identifier.into()),
302                    ("amount".to_string(), burn_amount.into()),
303                ]);
304                if let Some(note) = public_note {
305                    properties.insert("note".to_string(), note.into());
306                }
307                properties
308            }
309            TokenEvent::Transfer(
310                to,
311                public_note,
312                token_event_shared_encrypted_note,
313                token_event_personal_encrypted_note,
314                amount,
315            ) => {
316                let mut properties = BTreeMap::from([
317                    ("tokenId".to_string(), token_id.into()),
318                    ("amount".to_string(), amount.into()),
319                    ("toIdentityId".to_string(), to.into()),
320                ]);
321                if let Some(note) = public_note {
322                    properties.insert("publicNote".to_string(), note.into());
323                }
324                if let Some((sender_key_index, recipient_key_index, note)) =
325                    token_event_shared_encrypted_note
326                {
327                    properties.insert("encryptedSharedNote".to_string(), note.into());
328                    properties.insert("senderKeyIndex".to_string(), sender_key_index.into());
329                    properties.insert("recipientKeyIndex".to_string(), recipient_key_index.into());
330                }
331
332                if let Some((root_encryption_key_index, derivation_encryption_key_index, note)) =
333                    token_event_personal_encrypted_note
334                {
335                    properties.insert("encryptedPersonalNote".to_string(), note.into());
336                    properties.insert(
337                        "rootEncryptionKeyIndex".to_string(),
338                        root_encryption_key_index.into(),
339                    );
340                    properties.insert(
341                        "derivationEncryptionKeyIndex".to_string(),
342                        derivation_encryption_key_index.into(),
343                    );
344                }
345                properties
346            }
347            TokenEvent::Freeze(frozen_identity_id, public_note) => {
348                let mut properties = BTreeMap::from([
349                    ("tokenId".to_string(), token_id.into()),
350                    ("frozenIdentityId".to_string(), frozen_identity_id.into()),
351                ]);
352                if let Some(note) = public_note {
353                    properties.insert("note".to_string(), note.into());
354                }
355                properties
356            }
357            TokenEvent::Unfreeze(frozen_identity_id, public_note) => {
358                let mut properties = BTreeMap::from([
359                    ("tokenId".to_string(), token_id.into()),
360                    ("frozenIdentityId".to_string(), frozen_identity_id.into()),
361                ]);
362                if let Some(note) = public_note {
363                    properties.insert("note".to_string(), note.into());
364                }
365                properties
366            }
367            TokenEvent::DestroyFrozenFunds(frozen_identity_id, amount, public_note) => {
368                let mut properties = BTreeMap::from([
369                    ("tokenId".to_string(), token_id.into()),
370                    ("frozenIdentityId".to_string(), frozen_identity_id.into()),
371                    ("destroyedAmount".to_string(), amount.into()),
372                ]);
373                if let Some(note) = public_note {
374                    properties.insert("note".to_string(), note.into());
375                }
376                properties
377            }
378            TokenEvent::EmergencyAction(action, public_note) => {
379                let mut properties = BTreeMap::from([
380                    ("tokenId".to_string(), token_id.into()),
381                    ("action".to_string(), (action as u8).into()),
382                ]);
383                if let Some(note) = public_note {
384                    properties.insert("note".to_string(), note.into());
385                }
386                properties
387            }
388            TokenEvent::ConfigUpdate(configuration_change_item, public_note) => {
389                let mut properties = BTreeMap::from([
390                    ("tokenId".to_string(), token_id.into()),
391                    (
392                        "changeItemType".to_string(),
393                        configuration_change_item.u8_item_index().into(),
394                    ),
395                    (
396                        "changeItem".to_string(),
397                        configuration_change_item
398                            .serialize_consume_to_bytes_with_platform_version(platform_version)?
399                            .into(),
400                    ),
401                ]);
402                if let Some(note) = public_note {
403                    properties.insert("note".to_string(), note.into());
404                }
405                properties
406            }
407            TokenEvent::Claim(recipient, amount, public_note) => {
408                let (recipient_type, recipient_id, distribution_type) = match recipient {
409                    TokenDistributionTypeWithResolvedRecipient::PreProgrammed(identifier) => {
410                        (1u8, identifier, 0u8)
411                    }
412                    TokenDistributionTypeWithResolvedRecipient::Perpetual(
413                        TokenDistributionResolvedRecipient::ContractOwnerIdentity(identifier),
414                    ) => (0, identifier, 1),
415                    TokenDistributionTypeWithResolvedRecipient::Perpetual(
416                        TokenDistributionResolvedRecipient::Identity(identifier),
417                    ) => (1, identifier, 1),
418                    TokenDistributionTypeWithResolvedRecipient::Perpetual(
419                        TokenDistributionResolvedRecipient::Evonode(identifier),
420                    ) => (2, identifier, 1),
421                };
422
423                let mut properties = BTreeMap::from([
424                    ("tokenId".to_string(), token_id.into()),
425                    ("recipientType".to_string(), recipient_type.into()),
426                    ("recipientId".to_string(), recipient_id.into()),
427                    ("distributionType".to_string(), distribution_type.into()),
428                    ("amount".to_string(), amount.into()),
429                ]);
430
431                if let Some(note) = public_note {
432                    properties.insert("note".to_string(), note.into());
433                }
434                properties
435            }
436            TokenEvent::ChangePriceForDirectPurchase(price, note) => {
437                let mut properties = BTreeMap::from([("tokenId".to_string(), token_id.into())]);
438
439                if let Some(price_schedule) = price {
440                    properties.insert(
441                        "priceSchedule".to_string(),
442                        price_schedule
443                            .serialize_consume_to_bytes_with_platform_version(platform_version)?
444                            .into(),
445                    );
446                }
447
448                if let Some(note) = note {
449                    properties.insert("note".to_string(), note.into());
450                }
451
452                properties
453            }
454            TokenEvent::DirectPurchase(amount, total_cost) => BTreeMap::from([
455                ("tokenId".to_string(), token_id.into()),
456                ("tokenAmount".to_string(), amount.into()),
457                ("purchaseCost".to_string(), total_cost.into()),
458            ]),
459        };
460
461        let document: Document = DocumentV0 {
462            id: document_id,
463            owner_id,
464            properties,
465            revision: None,
466            created_at: Some(block_info.time_ms),
467            updated_at: None,
468            transferred_at: None,
469            created_at_block_height: Some(block_info.height),
470            updated_at_block_height: None,
471            transferred_at_block_height: None,
472            created_at_core_block_height: None,
473            updated_at_core_block_height: None,
474            transferred_at_core_block_height: None,
475            creator_id: None,
476        }
477        .into();
478
479        Ok(document)
480    }
481}