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
37pub type RecipientIdentifier = Identifier;
39
40pub type BurnFromIdentifier = Identifier;
42
43pub type PurchaserIdentifier = Identifier;
45
46pub type FrozenIdentifier = Identifier;
48
49#[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 Mint(TokenAmount, RecipientIdentifier, TokenEventPublicNote),
77
78 Burn(TokenAmount, BurnFromIdentifier, TokenEventPublicNote),
84
85 Freeze(FrozenIdentifier, TokenEventPublicNote),
90
91 Unfreeze(FrozenIdentifier, TokenEventPublicNote),
96
97 DestroyFrozenFunds(FrozenIdentifier, TokenAmount, TokenEventPublicNote),
103
104 Transfer(
112 RecipientIdentifier,
113 TokenEventPublicNote,
114 TokenEventSharedEncryptedNote,
115 TokenEventPersonalEncryptedNote,
116 TokenAmount,
117 ),
118
119 Claim(
125 TokenDistributionTypeWithResolvedRecipient,
126 TokenAmount,
127 TokenEventPublicNote,
128 ),
129
130 EmergencyAction(TokenEmergencyAction, TokenEventPublicNote),
135
136 ConfigUpdate(TokenConfigurationChangeItem, TokenEventPublicNote),
141
142 ChangePriceForDirectPurchase(Option<TokenPricingSchedule>, TokenEventPublicNote),
147
148 DirectPurchase(TokenAmount, Credits),
153}
154
155#[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 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}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 fn test_id() -> Identifier {
488 Identifier::from([1u8; 32])
489 }
490
491 fn test_id_2() -> Identifier {
492 Identifier::from([2u8; 32])
493 }
494
495 #[test]
498 fn associated_name_mint() {
499 let event = TokenEvent::Mint(0, test_id(), None);
500 assert_eq!(event.associated_document_type_name(), "mint");
501 }
502
503 #[test]
504 fn associated_name_burn() {
505 let event = TokenEvent::Burn(0, test_id(), None);
506 assert_eq!(event.associated_document_type_name(), "burn");
507 }
508
509 #[test]
510 fn associated_name_freeze() {
511 let event = TokenEvent::Freeze(test_id(), None);
512 assert_eq!(event.associated_document_type_name(), "freeze");
513 }
514
515 #[test]
516 fn associated_name_unfreeze() {
517 let event = TokenEvent::Unfreeze(test_id(), None);
518 assert_eq!(event.associated_document_type_name(), "unfreeze");
519 }
520
521 #[test]
522 fn associated_name_destroy_frozen_funds() {
523 let event = TokenEvent::DestroyFrozenFunds(test_id(), 0, None);
524 assert_eq!(event.associated_document_type_name(), "destroyFrozenFunds");
525 }
526
527 #[test]
528 fn associated_name_transfer() {
529 let event = TokenEvent::Transfer(test_id(), None, None, None, 0);
530 assert_eq!(event.associated_document_type_name(), "transfer");
531 }
532
533 #[test]
534 fn associated_name_claim() {
535 let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id());
536 let event = TokenEvent::Claim(recipient, 0, None);
537 assert_eq!(event.associated_document_type_name(), "claim");
538 }
539
540 #[test]
541 fn associated_name_emergency_action() {
542 let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None);
543 assert_eq!(event.associated_document_type_name(), "emergencyAction");
544 }
545
546 #[test]
547 fn associated_name_config_update() {
548 let event = TokenEvent::ConfigUpdate(
549 TokenConfigurationChangeItem::TokenConfigurationNoChange,
550 None,
551 );
552 assert_eq!(event.associated_document_type_name(), "configUpdate");
553 }
554
555 #[test]
556 fn associated_name_direct_purchase() {
557 let event = TokenEvent::DirectPurchase(0, 0);
558 assert_eq!(event.associated_document_type_name(), "directPurchase");
559 }
560
561 #[test]
562 fn associated_name_change_price() {
563 let event = TokenEvent::ChangePriceForDirectPurchase(None, None);
564 assert_eq!(event.associated_document_type_name(), "directPricing");
565 }
566
567 #[test]
570 fn all_document_type_names_are_unique() {
571 let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id());
572 let events: Vec<TokenEvent> = vec![
573 TokenEvent::Mint(0, test_id(), None),
574 TokenEvent::Burn(0, test_id(), None),
575 TokenEvent::Freeze(test_id(), None),
576 TokenEvent::Unfreeze(test_id(), None),
577 TokenEvent::DestroyFrozenFunds(test_id(), 0, None),
578 TokenEvent::Transfer(test_id(), None, None, None, 0),
579 TokenEvent::Claim(recipient, 0, None),
580 TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None),
581 TokenEvent::ConfigUpdate(
582 TokenConfigurationChangeItem::TokenConfigurationNoChange,
583 None,
584 ),
585 TokenEvent::DirectPurchase(0, 0),
586 TokenEvent::ChangePriceForDirectPurchase(None, None),
587 ];
588 let names: Vec<&str> = events
589 .iter()
590 .map(|e| e.associated_document_type_name())
591 .collect();
592 let mut unique = names.clone();
593 unique.sort();
594 unique.dedup();
595 assert_eq!(
596 names.len(),
597 unique.len(),
598 "Duplicate document type names found"
599 );
600 }
601
602 #[test]
605 fn format_note_none_returns_empty() {
606 assert_eq!(format_note(&None), "");
607 }
608
609 #[test]
610 fn format_note_some_returns_formatted() {
611 assert_eq!(format_note(&Some("hello".to_string())), " (note: hello)");
612 }
613}