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