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, DecodeUntrusted, Encode};
18use platform_serialization_derive::{
19 PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
20};
21use platform_value::Identifier;
22use platform_version::version::PlatformVersion;
23use std::collections::BTreeMap;
24use std::fmt;
25
26pub type TokenEventPublicNote = Option<String>;
27pub type TokenEventSharedEncryptedNote = Option<SharedEncryptedNote>;
28pub type TokenEventPersonalEncryptedNote = Option<(
29 RootEncryptionKeyIndex,
30 DerivationEncryptionKeyIndex,
31 Vec<u8>,
32)>;
33use crate::serialization::PlatformSerializableWithPlatformVersion;
34use crate::tokens::emergency_action::TokenEmergencyAction;
35use crate::tokens::token_pricing_schedule::TokenPricingSchedule;
36use crate::tokens::SharedEncryptedNote;
37use crate::ProtocolError;
38
39pub type RecipientIdentifier = Identifier;
41
42pub type BurnFromIdentifier = Identifier;
44
45pub type PurchaserIdentifier = Identifier;
47
48pub type FrozenIdentifier = Identifier;
50
51#[derive(
63 Debug,
64 PartialEq,
65 PartialOrd,
66 Clone,
67 Eq,
68 Encode,
69 Decode,
70 PlatformDeserializeTrusted,
71 PlatformDeserializeUntrusted,
72 PlatformSerialize,
73 DecodeUntrusted,
74)]
75#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
85#[platform_serialize(unversioned)]
86pub enum TokenEvent {
87 Mint(TokenAmount, RecipientIdentifier, TokenEventPublicNote),
93
94 Burn(TokenAmount, BurnFromIdentifier, TokenEventPublicNote),
100
101 Freeze(FrozenIdentifier, TokenEventPublicNote),
106
107 Unfreeze(FrozenIdentifier, TokenEventPublicNote),
112
113 DestroyFrozenFunds(FrozenIdentifier, TokenAmount, TokenEventPublicNote),
119
120 Transfer(
128 RecipientIdentifier,
129 TokenEventPublicNote,
130 TokenEventSharedEncryptedNote,
131 TokenEventPersonalEncryptedNote,
132 TokenAmount,
133 ),
134
135 Claim(
141 TokenDistributionTypeWithResolvedRecipient,
142 TokenAmount,
143 TokenEventPublicNote,
144 ),
145
146 EmergencyAction(TokenEmergencyAction, TokenEventPublicNote),
151
152 ConfigUpdate(TokenConfigurationChangeItem, TokenEventPublicNote),
157
158 ChangePriceForDirectPurchase(Option<TokenPricingSchedule>, TokenEventPublicNote),
163
164 DirectPurchase(TokenAmount, Credits),
169}
170
171#[cfg(feature = "json-conversion")]
178impl JsonConvertible for TokenEvent {}
179
180#[cfg(feature = "serde-conversion")]
181impl serde::Serialize for TokenEvent {
182 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
183 use serde::ser::SerializeMap;
184
185 struct SafeU64<'a>(&'a u64);
189 impl<'a> serde::Serialize for SafeU64<'a> {
190 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
191 crate::serialization::json::safe_integer::json_safe_u64::serialize(self.0, s)
192 }
193 }
194 struct SafeOptEncNote<'a>(&'a Option<(u32, u32, Vec<u8>)>);
195 impl<'a> serde::Serialize for SafeOptEncNote<'a> {
196 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
197 crate::serialization::json::safe_integer::json_safe_option_encrypted_note::serialize(
198 self.0, s,
199 )
200 }
201 }
202
203 match self {
204 TokenEvent::Mint(amount, recipient, note) => {
205 let mut m = serializer.serialize_map(Some(4))?;
206 m.serialize_entry("$type", "mint")?;
207 m.serialize_entry("amount", &SafeU64(amount))?;
208 m.serialize_entry("recipient", recipient)?;
209 m.serialize_entry("publicNote", note)?;
210 m.end()
211 }
212 TokenEvent::Burn(amount, from, note) => {
213 let mut m = serializer.serialize_map(Some(4))?;
214 m.serialize_entry("$type", "burn")?;
215 m.serialize_entry("amount", &SafeU64(amount))?;
216 m.serialize_entry("burnFromIdentifier", from)?;
217 m.serialize_entry("publicNote", note)?;
218 m.end()
219 }
220 TokenEvent::Freeze(frozen, note) => {
221 let mut m = serializer.serialize_map(Some(3))?;
222 m.serialize_entry("$type", "freeze")?;
223 m.serialize_entry("frozenIdentifier", frozen)?;
224 m.serialize_entry("publicNote", note)?;
225 m.end()
226 }
227 TokenEvent::Unfreeze(frozen, note) => {
228 let mut m = serializer.serialize_map(Some(3))?;
229 m.serialize_entry("$type", "unfreeze")?;
230 m.serialize_entry("frozenIdentifier", frozen)?;
231 m.serialize_entry("publicNote", note)?;
232 m.end()
233 }
234 TokenEvent::DestroyFrozenFunds(frozen, amount, note) => {
235 let mut m = serializer.serialize_map(Some(4))?;
236 m.serialize_entry("$type", "destroyFrozenFunds")?;
237 m.serialize_entry("frozenIdentifier", frozen)?;
238 m.serialize_entry("amount", &SafeU64(amount))?;
239 m.serialize_entry("publicNote", note)?;
240 m.end()
241 }
242 TokenEvent::Transfer(recipient, note, shared, private, amount) => {
243 let mut m = serializer.serialize_map(Some(6))?;
244 m.serialize_entry("$type", "transfer")?;
245 m.serialize_entry("recipient", recipient)?;
246 m.serialize_entry("publicNote", note)?;
247 m.serialize_entry("sharedEncryptedNote", &SafeOptEncNote(shared))?;
248 m.serialize_entry("privateEncryptedNote", &SafeOptEncNote(private))?;
249 m.serialize_entry("amount", &SafeU64(amount))?;
250 m.end()
251 }
252 TokenEvent::Claim(distribution_type, amount, note) => {
253 let mut m = serializer.serialize_map(Some(4))?;
254 m.serialize_entry("$type", "claim")?;
255 m.serialize_entry("distributionType", distribution_type)?;
256 m.serialize_entry("amount", &SafeU64(amount))?;
257 m.serialize_entry("publicNote", note)?;
258 m.end()
259 }
260 TokenEvent::EmergencyAction(action, note) => {
261 let mut m = serializer.serialize_map(Some(3))?;
262 m.serialize_entry("$type", "emergencyAction")?;
263 m.serialize_entry("action", action)?;
264 m.serialize_entry("publicNote", note)?;
265 m.end()
266 }
267 TokenEvent::ConfigUpdate(change, note) => {
268 let mut m = serializer.serialize_map(Some(3))?;
269 m.serialize_entry("$type", "configUpdate")?;
270 m.serialize_entry("configurationChange", change)?;
271 m.serialize_entry("publicNote", note)?;
272 m.end()
273 }
274 TokenEvent::ChangePriceForDirectPurchase(schedule, note) => {
275 let mut m = serializer.serialize_map(Some(3))?;
276 m.serialize_entry("$type", "changePriceForDirectPurchase")?;
277 m.serialize_entry("pricingSchedule", schedule)?;
278 m.serialize_entry("publicNote", note)?;
279 m.end()
280 }
281 TokenEvent::DirectPurchase(amount, credits) => {
282 let mut m = serializer.serialize_map(Some(3))?;
283 m.serialize_entry("$type", "directPurchase")?;
284 m.serialize_entry("amount", &SafeU64(amount))?;
285 m.serialize_entry("credits", &SafeU64(credits))?;
286 m.end()
287 }
288 }
289 }
290}
291
292#[cfg(feature = "serde-conversion")]
293impl<'de> serde::Deserialize<'de> for TokenEvent {
294 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
295 use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
296
297 #[derive(serde::Deserialize)]
301 #[serde(transparent)]
302 struct U64Safe(
303 #[serde(with = "crate::serialization::json::safe_integer::json_safe_u64")] u64,
304 );
305 #[derive(serde::Deserialize)]
306 #[serde(transparent)]
307 struct OptEncNote(
308 #[serde(
309 with = "crate::serialization::json::safe_integer::json_safe_option_encrypted_note"
310 )]
311 Option<(u32, u32, Vec<u8>)>,
312 );
313
314 struct V;
315
316 impl<'de> Visitor<'de> for V {
317 type Value = TokenEvent;
318
319 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
320 f.write_str("TokenEvent as a map with `$type` discriminator + variant fields")
321 }
322
323 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<TokenEvent, A::Error> {
324 let mut ty: Option<String> = None;
325 let mut amount: Option<u64> = None;
326 let mut credits: Option<u64> = None;
327 let mut recipient: Option<Identifier> = None;
328 let mut burn_from: Option<Identifier> = None;
329 let mut frozen: Option<Identifier> = None;
330 let mut public_note: Option<String> = None;
331 let mut shared_note: Option<(u32, u32, Vec<u8>)> = None;
332 let mut private_note: Option<(u32, u32, Vec<u8>)> = None;
333 let mut distribution_type: Option<TokenDistributionTypeWithResolvedRecipient> =
334 None;
335 let mut action: Option<TokenEmergencyAction> = None;
336 let mut configuration_change: Option<TokenConfigurationChangeItem> = None;
337 let mut pricing_schedule: Option<TokenPricingSchedule> = None;
338
339 while let Some(key) = map.next_key::<String>()? {
340 match key.as_str() {
341 "$type" => ty = Some(map.next_value()?),
342 "amount" => amount = Some(map.next_value::<U64Safe>()?.0),
343 "credits" => credits = Some(map.next_value::<U64Safe>()?.0),
344 "recipient" => recipient = Some(map.next_value()?),
345 "burnFromIdentifier" => burn_from = Some(map.next_value()?),
346 "frozenIdentifier" => frozen = Some(map.next_value()?),
347 "publicNote" => public_note = map.next_value()?,
348 "sharedEncryptedNote" => {
349 shared_note = map.next_value::<OptEncNote>()?.0;
350 }
351 "privateEncryptedNote" => {
352 private_note = map.next_value::<OptEncNote>()?.0;
353 }
354 "distributionType" => distribution_type = Some(map.next_value()?),
355 "action" => action = Some(map.next_value()?),
356 "configurationChange" => configuration_change = Some(map.next_value()?),
357 "pricingSchedule" => pricing_schedule = map.next_value()?,
358 _ => {
359 let _: IgnoredAny = map.next_value()?;
360 }
361 }
362 }
363
364 let ty = ty.ok_or_else(|| A::Error::missing_field("$type"))?;
365 match ty.as_str() {
366 "mint" => Ok(TokenEvent::Mint(
367 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
368 recipient.ok_or_else(|| A::Error::missing_field("recipient"))?,
369 public_note,
370 )),
371 "burn" => Ok(TokenEvent::Burn(
372 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
373 burn_from.ok_or_else(|| A::Error::missing_field("burnFromIdentifier"))?,
374 public_note,
375 )),
376 "freeze" => Ok(TokenEvent::Freeze(
377 frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
378 public_note,
379 )),
380 "unfreeze" => Ok(TokenEvent::Unfreeze(
381 frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
382 public_note,
383 )),
384 "destroyFrozenFunds" => Ok(TokenEvent::DestroyFrozenFunds(
385 frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
386 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
387 public_note,
388 )),
389 "transfer" => Ok(TokenEvent::Transfer(
390 recipient.ok_or_else(|| A::Error::missing_field("recipient"))?,
391 public_note,
392 shared_note,
393 private_note,
394 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
395 )),
396 "claim" => Ok(TokenEvent::Claim(
397 distribution_type
398 .ok_or_else(|| A::Error::missing_field("distributionType"))?,
399 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
400 public_note,
401 )),
402 "emergencyAction" => Ok(TokenEvent::EmergencyAction(
403 action.ok_or_else(|| A::Error::missing_field("action"))?,
404 public_note,
405 )),
406 "configUpdate" => Ok(TokenEvent::ConfigUpdate(
407 configuration_change
408 .ok_or_else(|| A::Error::missing_field("configurationChange"))?,
409 public_note,
410 )),
411 "changePriceForDirectPurchase" => Ok(TokenEvent::ChangePriceForDirectPurchase(
412 pricing_schedule,
413 public_note,
414 )),
415 "directPurchase" => Ok(TokenEvent::DirectPurchase(
416 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
417 credits.ok_or_else(|| A::Error::missing_field("credits"))?,
418 )),
419 other => Err(A::Error::unknown_variant(
420 other,
421 &[
422 "mint",
423 "burn",
424 "freeze",
425 "unfreeze",
426 "destroyFrozenFunds",
427 "transfer",
428 "claim",
429 "emergencyAction",
430 "configUpdate",
431 "changePriceForDirectPurchase",
432 "directPurchase",
433 ],
434 )),
435 }
436 }
437 }
438
439 deserializer.deserialize_map(V)
440 }
441}
442
443#[cfg(all(
444 test,
445 feature = "json-conversion",
446 feature = "value-conversion",
447 feature = "serde-conversion"
448))]
449pub(crate) mod json_convertible_tests {
450 use super::*;
451 use platform_value::platform_value;
452 use serde_json::json;
453
454 pub(crate) fn mint_fixture() -> TokenEvent {
461 TokenEvent::Mint(
462 5_000,
463 Identifier::new([0xa1; 32]),
464 Some("genesis mint".to_string()),
465 )
466 }
467
468 #[test]
469 fn json_round_trip_mint() {
470 use crate::serialization::JsonConvertible;
471 let original = mint_fixture();
472 let json = original.to_json().expect("to_json");
473 assert_eq!(
476 json,
477 json!({
478 "$type": "mint",
479 "amount": 5_000,
480 "recipient": "Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp",
481 "publicNote": "genesis mint",
482 })
483 );
484 let recovered = TokenEvent::from_json(json).expect("from_json");
485 assert_eq!(original, recovered);
486 }
487
488 #[test]
489 fn json_round_trip_freeze_no_note() {
490 use crate::serialization::JsonConvertible;
491 let original = TokenEvent::Freeze(Identifier::new([0xb2; 32]), None);
492 let json = original.to_json().expect("to_json");
493 assert_eq!(
494 json,
495 json!({
496 "$type": "freeze",
497 "frozenIdentifier": "D2ZcUbtpG5sKq7XLeB4YnpNnTGSptKCxTddoNeydzJQq",
498 "publicNote": null,
499 })
500 );
501 let recovered = TokenEvent::from_json(json).expect("from_json");
502 assert_eq!(original, recovered);
503 }
504
505 #[test]
506 fn json_round_trip_direct_purchase() {
507 use crate::serialization::JsonConvertible;
508 let original = TokenEvent::DirectPurchase(100, 5_000);
509 let json = original.to_json().expect("to_json");
510 assert_eq!(
511 json,
512 json!({
513 "$type": "directPurchase",
514 "amount": 100,
515 "credits": 5_000,
516 })
517 );
518 let recovered = TokenEvent::from_json(json).expect("from_json");
519 assert_eq!(original, recovered);
520 }
521
522 #[test]
523 fn value_round_trip_mint() {
524 use crate::serialization::ValueConvertible;
525 let original = mint_fixture();
526 let value = original.to_object().expect("to_object");
527 assert_eq!(
529 value,
530 platform_value!({
531 "$type": "mint",
532 "amount": 5_000u64,
533 "recipient": Identifier::new([0xa1; 32]),
534 "publicNote": "genesis mint",
535 })
536 );
537 let recovered = TokenEvent::from_object(value).expect("from_object");
538 assert_eq!(original, recovered);
539 }
540
541 #[test]
542 fn value_round_trip_freeze_no_note() {
543 use crate::serialization::ValueConvertible;
544 let original = TokenEvent::Freeze(Identifier::new([0xb2; 32]), None);
545 let value = original.to_object().expect("to_object");
546 assert_eq!(
547 value,
548 platform_value!({
549 "$type": "freeze",
550 "frozenIdentifier": Identifier::new([0xb2; 32]),
551 "publicNote": null,
552 })
553 );
554 let recovered = TokenEvent::from_object(value).expect("from_object");
555 assert_eq!(original, recovered);
556 }
557
558 #[test]
559 fn value_round_trip_direct_purchase() {
560 use crate::serialization::ValueConvertible;
561 let original = TokenEvent::DirectPurchase(100, 5_000);
562 let value = original.to_object().expect("to_object");
563 assert_eq!(
565 value,
566 platform_value!({
567 "$type": "directPurchase",
568 "amount": 100u64,
569 "credits": 5_000u64,
570 })
571 );
572 let recovered = TokenEvent::from_object(value).expect("from_object");
573 assert_eq!(original, recovered);
574 }
575}
576
577impl fmt::Display for TokenEvent {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 match self {
580 TokenEvent::Mint(amount, recipient, note) => {
581 write!(f, "Mint {} to {}{}", amount, recipient, format_note(note))
582 }
583 TokenEvent::Burn(amount, burn_from_identifier, note) => {
584 write!(
585 f,
586 "Burn {} from {}{}",
587 amount,
588 burn_from_identifier,
589 format_note(note)
590 )
591 }
592 TokenEvent::Freeze(identity, note) => {
593 write!(f, "Freeze {}{}", identity, format_note(note))
594 }
595 TokenEvent::Unfreeze(identity, note) => {
596 write!(f, "Unfreeze {}{}", identity, format_note(note))
597 }
598 TokenEvent::DestroyFrozenFunds(identity, amount, note) => {
599 write!(
600 f,
601 "Destroy {} frozen from {}{}",
602 amount,
603 identity,
604 format_note(note)
605 )
606 }
607 TokenEvent::Transfer(to, note, _, _, amount) => {
608 write!(f, "Transfer {} to {}{}", amount, to, format_note(note))
609 }
610 TokenEvent::Claim(recipient, amount, note) => {
611 write!(
612 f,
613 "Claim {} by {:?}{}",
614 amount,
615 recipient,
616 format_note(note)
617 )
618 }
619 TokenEvent::EmergencyAction(action, note) => {
620 write!(f, "Emergency action {:?}{}", action, format_note(note))
621 }
622 TokenEvent::ConfigUpdate(change, note) => {
623 write!(f, "Configuration update {:?}{}", change, format_note(note))
624 }
625 TokenEvent::ChangePriceForDirectPurchase(schedule, note) => match schedule {
626 Some(s) => write!(f, "Change price schedule to {:?}{}", s, format_note(note)),
627 None => write!(f, "Disable direct purchase{}", format_note(note)),
628 },
629 TokenEvent::DirectPurchase(amount, credits) => {
630 write!(f, "Direct purchase of {} for {} credits", amount, credits)
631 }
632 }
633 }
634}
635
636fn format_note(note: &Option<String>) -> String {
637 match note {
638 Some(n) => format!(" (note: {})", n),
639 None => String::new(),
640 }
641}
642
643impl TokenEvent {
644 pub fn associated_document_type_name(&self) -> &str {
645 match self {
646 TokenEvent::Mint(..) => "mint",
647 TokenEvent::Burn(..) => "burn",
648 TokenEvent::Freeze(..) => "freeze",
649 TokenEvent::Unfreeze(..) => "unfreeze",
650 TokenEvent::DestroyFrozenFunds(..) => "destroyFrozenFunds",
651 TokenEvent::Transfer(..) => "transfer",
652 TokenEvent::Claim(..) => "claim",
653 TokenEvent::EmergencyAction(..) => "emergencyAction",
654 TokenEvent::ConfigUpdate(..) => "configUpdate",
655 TokenEvent::DirectPurchase(..) => "directPurchase",
656 TokenEvent::ChangePriceForDirectPurchase(..) => "directPricing",
657 }
658 }
659
660 pub fn public_note(&self) -> Option<&str> {
662 match self {
663 TokenEvent::Mint(_, _, Some(note))
664 | TokenEvent::Burn(_, _, Some(note))
665 | TokenEvent::Freeze(_, Some(note))
666 | TokenEvent::Unfreeze(_, Some(note))
667 | TokenEvent::DestroyFrozenFunds(_, _, Some(note))
668 | TokenEvent::Transfer(_, Some(note), _, _, _)
669 | TokenEvent::Claim(_, _, Some(note))
670 | TokenEvent::EmergencyAction(_, Some(note))
671 | TokenEvent::ConfigUpdate(_, Some(note))
672 | TokenEvent::ChangePriceForDirectPurchase(_, Some(note)) => Some(note),
673 _ => None,
674 }
675 }
676
677 pub fn associated_document_type<'a>(
678 &self,
679 token_history_contract: &'a DataContract,
680 ) -> Result<DocumentTypeRef<'a>, ProtocolError> {
681 Ok(token_history_contract.document_type_for_name(self.associated_document_type_name())?)
682 }
683
684 pub fn build_historical_document_owned(
685 self,
686 token_id: Identifier,
687 owner_id: Identifier,
688 owner_nonce: IdentityNonce,
689 block_info: &BlockInfo,
690 platform_version: &PlatformVersion,
691 ) -> Result<Document, ProtocolError> {
692 let document_id = Document::generate_document_id_v0(
693 &token_id,
694 &owner_id,
695 format!("history_{}", self.associated_document_type_name()).as_str(),
696 owner_nonce.to_be_bytes().as_slice(),
697 );
698
699 let properties = match self {
700 TokenEvent::Mint(mint_amount, recipient_id, public_note) => {
701 let mut properties = BTreeMap::from([
702 ("tokenId".to_string(), token_id.into()),
703 ("recipientId".to_string(), recipient_id.into()),
704 ("amount".to_string(), mint_amount.into()),
705 ]);
706 if let Some(note) = public_note {
707 properties.insert("note".to_string(), note.into());
708 }
709 properties
710 }
711 TokenEvent::Burn(burn_amount, burn_from_identifier, public_note) => {
712 let mut properties = BTreeMap::from([
713 ("tokenId".to_string(), token_id.into()),
714 ("burnFromId".to_string(), burn_from_identifier.into()),
715 ("amount".to_string(), burn_amount.into()),
716 ]);
717 if let Some(note) = public_note {
718 properties.insert("note".to_string(), note.into());
719 }
720 properties
721 }
722 TokenEvent::Transfer(
723 to,
724 public_note,
725 token_event_shared_encrypted_note,
726 token_event_personal_encrypted_note,
727 amount,
728 ) => {
729 let mut properties = BTreeMap::from([
730 ("tokenId".to_string(), token_id.into()),
731 ("amount".to_string(), amount.into()),
732 ("toIdentityId".to_string(), to.into()),
733 ]);
734 if let Some(note) = public_note {
735 properties.insert("publicNote".to_string(), note.into());
736 }
737 if let Some((sender_key_index, recipient_key_index, note)) =
738 token_event_shared_encrypted_note
739 {
740 properties.insert("encryptedSharedNote".to_string(), note.into());
741 properties.insert("senderKeyIndex".to_string(), sender_key_index.into());
742 properties.insert("recipientKeyIndex".to_string(), recipient_key_index.into());
743 }
744
745 if let Some((root_encryption_key_index, derivation_encryption_key_index, note)) =
746 token_event_personal_encrypted_note
747 {
748 properties.insert("encryptedPersonalNote".to_string(), note.into());
749 properties.insert(
750 "rootEncryptionKeyIndex".to_string(),
751 root_encryption_key_index.into(),
752 );
753 properties.insert(
754 "derivationEncryptionKeyIndex".to_string(),
755 derivation_encryption_key_index.into(),
756 );
757 }
758 properties
759 }
760 TokenEvent::Freeze(frozen_identity_id, public_note) => {
761 let mut properties = BTreeMap::from([
762 ("tokenId".to_string(), token_id.into()),
763 ("frozenIdentityId".to_string(), frozen_identity_id.into()),
764 ]);
765 if let Some(note) = public_note {
766 properties.insert("note".to_string(), note.into());
767 }
768 properties
769 }
770 TokenEvent::Unfreeze(frozen_identity_id, public_note) => {
771 let mut properties = BTreeMap::from([
772 ("tokenId".to_string(), token_id.into()),
773 ("frozenIdentityId".to_string(), frozen_identity_id.into()),
774 ]);
775 if let Some(note) = public_note {
776 properties.insert("note".to_string(), note.into());
777 }
778 properties
779 }
780 TokenEvent::DestroyFrozenFunds(frozen_identity_id, amount, public_note) => {
781 let mut properties = BTreeMap::from([
782 ("tokenId".to_string(), token_id.into()),
783 ("frozenIdentityId".to_string(), frozen_identity_id.into()),
784 ("destroyedAmount".to_string(), amount.into()),
785 ]);
786 if let Some(note) = public_note {
787 properties.insert("note".to_string(), note.into());
788 }
789 properties
790 }
791 TokenEvent::EmergencyAction(action, public_note) => {
792 let mut properties = BTreeMap::from([
793 ("tokenId".to_string(), token_id.into()),
794 ("action".to_string(), (action as u8).into()),
795 ]);
796 if let Some(note) = public_note {
797 properties.insert("note".to_string(), note.into());
798 }
799 properties
800 }
801 TokenEvent::ConfigUpdate(configuration_change_item, public_note) => {
802 let mut properties = BTreeMap::from([
803 ("tokenId".to_string(), token_id.into()),
804 (
805 "changeItemType".to_string(),
806 configuration_change_item.u8_item_index().into(),
807 ),
808 (
809 "changeItem".to_string(),
810 configuration_change_item
811 .serialize_consume_to_bytes_with_platform_version(platform_version)?
812 .into(),
813 ),
814 ]);
815 if let Some(note) = public_note {
816 properties.insert("note".to_string(), note.into());
817 }
818 properties
819 }
820 TokenEvent::Claim(recipient, amount, public_note) => {
821 let (recipient_type, recipient_id, distribution_type) = match recipient {
822 TokenDistributionTypeWithResolvedRecipient::PreProgrammed(identifier) => {
823 (1u8, identifier, 0u8)
824 }
825 TokenDistributionTypeWithResolvedRecipient::Perpetual(
826 TokenDistributionResolvedRecipient::ContractOwnerIdentity(identifier),
827 ) => (0, identifier, 1),
828 TokenDistributionTypeWithResolvedRecipient::Perpetual(
829 TokenDistributionResolvedRecipient::Identity(identifier),
830 ) => (1, identifier, 1),
831 TokenDistributionTypeWithResolvedRecipient::Perpetual(
832 TokenDistributionResolvedRecipient::Evonode(identifier),
833 ) => (2, identifier, 1),
834 };
835
836 let mut properties = BTreeMap::from([
837 ("tokenId".to_string(), token_id.into()),
838 ("recipientType".to_string(), recipient_type.into()),
839 ("recipientId".to_string(), recipient_id.into()),
840 ("distributionType".to_string(), distribution_type.into()),
841 ("amount".to_string(), amount.into()),
842 ]);
843
844 if let Some(note) = public_note {
845 properties.insert("note".to_string(), note.into());
846 }
847 properties
848 }
849 TokenEvent::ChangePriceForDirectPurchase(price, note) => {
850 let mut properties = BTreeMap::from([("tokenId".to_string(), token_id.into())]);
851
852 if let Some(price_schedule) = price {
853 properties.insert(
854 "priceSchedule".to_string(),
855 price_schedule
856 .serialize_consume_to_bytes_with_platform_version(platform_version)?
857 .into(),
858 );
859 }
860
861 if let Some(note) = note {
862 properties.insert("note".to_string(), note.into());
863 }
864
865 properties
866 }
867 TokenEvent::DirectPurchase(amount, total_cost) => BTreeMap::from([
868 ("tokenId".to_string(), token_id.into()),
869 ("tokenAmount".to_string(), amount.into()),
870 ("purchaseCost".to_string(), total_cost.into()),
871 ]),
872 };
873
874 let document: Document = DocumentV0 {
875 contract_version: None,
876 id: document_id,
877 owner_id,
878 properties,
879 revision: None,
880 created_at: Some(block_info.time_ms),
881 updated_at: None,
882 transferred_at: None,
883 created_at_block_height: Some(block_info.height),
884 updated_at_block_height: None,
885 transferred_at_block_height: None,
886 created_at_core_block_height: None,
887 updated_at_core_block_height: None,
888 transferred_at_core_block_height: None,
889 creator_id: None,
890 }
891 .into();
892
893 Ok(document)
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900
901 fn test_id() -> Identifier {
902 Identifier::from([1u8; 32])
903 }
904
905 fn test_id_2() -> Identifier {
906 Identifier::from([2u8; 32])
907 }
908
909 #[test]
912 fn associated_name_mint() {
913 let event = TokenEvent::Mint(0, test_id(), None);
914 assert_eq!(event.associated_document_type_name(), "mint");
915 }
916
917 #[test]
918 fn associated_name_burn() {
919 let event = TokenEvent::Burn(0, test_id(), None);
920 assert_eq!(event.associated_document_type_name(), "burn");
921 }
922
923 #[test]
924 fn associated_name_freeze() {
925 let event = TokenEvent::Freeze(test_id(), None);
926 assert_eq!(event.associated_document_type_name(), "freeze");
927 }
928
929 #[test]
930 fn associated_name_unfreeze() {
931 let event = TokenEvent::Unfreeze(test_id(), None);
932 assert_eq!(event.associated_document_type_name(), "unfreeze");
933 }
934
935 #[test]
936 fn associated_name_destroy_frozen_funds() {
937 let event = TokenEvent::DestroyFrozenFunds(test_id(), 0, None);
938 assert_eq!(event.associated_document_type_name(), "destroyFrozenFunds");
939 }
940
941 #[test]
942 fn associated_name_transfer() {
943 let event = TokenEvent::Transfer(test_id(), None, None, None, 0);
944 assert_eq!(event.associated_document_type_name(), "transfer");
945 }
946
947 #[test]
948 fn associated_name_claim() {
949 let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id());
950 let event = TokenEvent::Claim(recipient, 0, None);
951 assert_eq!(event.associated_document_type_name(), "claim");
952 }
953
954 #[test]
955 fn associated_name_emergency_action() {
956 let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None);
957 assert_eq!(event.associated_document_type_name(), "emergencyAction");
958 }
959
960 #[test]
961 fn associated_name_config_update() {
962 let event = TokenEvent::ConfigUpdate(
963 TokenConfigurationChangeItem::TokenConfigurationNoChange,
964 None,
965 );
966 assert_eq!(event.associated_document_type_name(), "configUpdate");
967 }
968
969 #[test]
970 fn associated_name_direct_purchase() {
971 let event = TokenEvent::DirectPurchase(0, 0);
972 assert_eq!(event.associated_document_type_name(), "directPurchase");
973 }
974
975 #[test]
976 fn associated_name_change_price() {
977 let event = TokenEvent::ChangePriceForDirectPurchase(None, None);
978 assert_eq!(event.associated_document_type_name(), "directPricing");
979 }
980
981 #[test]
984 fn all_document_type_names_are_unique() {
985 let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id());
986 let events: Vec<TokenEvent> = vec![
987 TokenEvent::Mint(0, test_id(), None),
988 TokenEvent::Burn(0, test_id(), None),
989 TokenEvent::Freeze(test_id(), None),
990 TokenEvent::Unfreeze(test_id(), None),
991 TokenEvent::DestroyFrozenFunds(test_id(), 0, None),
992 TokenEvent::Transfer(test_id(), None, None, None, 0),
993 TokenEvent::Claim(recipient, 0, None),
994 TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None),
995 TokenEvent::ConfigUpdate(
996 TokenConfigurationChangeItem::TokenConfigurationNoChange,
997 None,
998 ),
999 TokenEvent::DirectPurchase(0, 0),
1000 TokenEvent::ChangePriceForDirectPurchase(None, None),
1001 ];
1002 let names: Vec<&str> = events
1003 .iter()
1004 .map(|e| e.associated_document_type_name())
1005 .collect();
1006 let mut unique = names.clone();
1007 unique.sort();
1008 unique.dedup();
1009 assert_eq!(
1010 names.len(),
1011 unique.len(),
1012 "Duplicate document type names found"
1013 );
1014 }
1015
1016 #[test]
1019 fn format_note_none_returns_empty() {
1020 assert_eq!(format_note(&None), "");
1021 }
1022
1023 #[test]
1024 fn format_note_some_returns_formatted() {
1025 assert_eq!(format_note(&Some("hello".to_string())), " (note: hello)");
1026 }
1027}