1use crate::balances::credits::TokenAmount;
2use crate::block::block_info::BlockInfo;
3use crate::data_contract::accessors::v0::DataContractV0Getters;
4use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
5use crate::data_contract::associated_token::token_distribution_key::TokenDistributionTypeWithResolvedRecipient;
6use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionResolvedRecipient;
7use crate::data_contract::document_type::DocumentTypeRef;
8use crate::document::{Document, DocumentV0};
9use crate::fee::Credits;
10use crate::prelude::{
11 DataContract, DerivationEncryptionKeyIndex, IdentityNonce, RootEncryptionKeyIndex,
12};
13#[cfg(feature = "serde-conversion")]
14use crate::serialization::json::safe_integer::{json_safe_option_encrypted_note, json_safe_u64};
15#[cfg(feature = "json-conversion")]
16use crate::serialization::JsonConvertible;
17#[cfg(feature = "value-conversion")]
18use crate::serialization::ValueConvertible;
19use bincode::{Decode, DecodeUntrusted, Encode};
20use platform_serialization_derive::{
21 PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
22};
23use platform_value::Identifier;
24use platform_version::version::PlatformVersion;
25use std::collections::BTreeMap;
26use std::fmt;
27
28pub type TokenEventPublicNote = Option<String>;
29pub type TokenEventSharedEncryptedNote = Option<SharedEncryptedNote>;
30pub type TokenEventPersonalEncryptedNote = Option<(
31 RootEncryptionKeyIndex,
32 DerivationEncryptionKeyIndex,
33 Vec<u8>,
34)>;
35use crate::serialization::PlatformSerializableWithPlatformVersion;
36use crate::tokens::emergency_action::TokenEmergencyAction;
37use crate::tokens::token_pricing_schedule::TokenPricingSchedule;
38use crate::tokens::SharedEncryptedNote;
39use crate::ProtocolError;
40
41pub type RecipientIdentifier = Identifier;
43
44pub type BurnFromIdentifier = Identifier;
46
47pub type PurchaserIdentifier = Identifier;
49
50pub type FrozenIdentifier = Identifier;
52
53#[derive(
65 Debug,
66 PartialEq,
67 PartialOrd,
68 Clone,
69 Eq,
70 Encode,
71 Decode,
72 PlatformDeserializeTrusted,
73 PlatformDeserializeUntrusted,
74 PlatformSerialize,
75 DecodeUntrusted,
76)]
77#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
87#[platform_serialize(unversioned)]
88pub enum TokenEvent {
89 Mint(TokenAmount, RecipientIdentifier, TokenEventPublicNote),
95
96 Burn(TokenAmount, BurnFromIdentifier, TokenEventPublicNote),
102
103 Freeze(FrozenIdentifier, TokenEventPublicNote),
108
109 Unfreeze(FrozenIdentifier, TokenEventPublicNote),
114
115 DestroyFrozenFunds(FrozenIdentifier, TokenAmount, TokenEventPublicNote),
121
122 Transfer(
130 RecipientIdentifier,
131 TokenEventPublicNote,
132 TokenEventSharedEncryptedNote,
133 TokenEventPersonalEncryptedNote,
134 TokenAmount,
135 ),
136
137 Claim(
143 TokenDistributionTypeWithResolvedRecipient,
144 TokenAmount,
145 TokenEventPublicNote,
146 ),
147
148 EmergencyAction(TokenEmergencyAction, TokenEventPublicNote),
153
154 ConfigUpdate(TokenConfigurationChangeItem, TokenEventPublicNote),
159
160 ChangePriceForDirectPurchase(Option<TokenPricingSchedule>, TokenEventPublicNote),
165
166 DirectPurchase(TokenAmount, Credits),
171
172 Shield(TokenAmount),
176
177 Unshield(RecipientIdentifier, TokenAmount),
182
183 ShieldedTransfer,
186
187 MintToPool(TokenAmount, Identifier, TokenEventPublicNote),
193
194 BurnFromPool(TokenAmount, Identifier, TokenEventPublicNote),
200
201 ClaimToPool(TokenAmount),
205
206 DirectPurchaseToPool(TokenAmount, Credits),
211}
212
213#[cfg(feature = "json-conversion")]
220impl JsonConvertible for TokenEvent {}
221
222#[cfg(feature = "serde-conversion")]
223impl serde::Serialize for TokenEvent {
224 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
225 use serde::ser::SerializeMap;
226
227 struct SafeU64<'a>(&'a u64);
231 impl<'a> serde::Serialize for SafeU64<'a> {
232 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
233 json_safe_u64::serialize(self.0, s)
234 }
235 }
236 struct SafeOptEncNote<'a>(&'a Option<(u32, u32, Vec<u8>)>);
237 impl<'a> serde::Serialize for SafeOptEncNote<'a> {
238 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
239 json_safe_option_encrypted_note::serialize(self.0, s)
240 }
241 }
242
243 match self {
244 TokenEvent::Mint(amount, recipient, note) => {
245 let mut m = serializer.serialize_map(Some(4))?;
246 m.serialize_entry("$type", "mint")?;
247 m.serialize_entry("amount", &SafeU64(amount))?;
248 m.serialize_entry("recipient", recipient)?;
249 m.serialize_entry("publicNote", note)?;
250 m.end()
251 }
252 TokenEvent::Burn(amount, from, note) => {
253 let mut m = serializer.serialize_map(Some(4))?;
254 m.serialize_entry("$type", "burn")?;
255 m.serialize_entry("amount", &SafeU64(amount))?;
256 m.serialize_entry("burnFromIdentifier", from)?;
257 m.serialize_entry("publicNote", note)?;
258 m.end()
259 }
260 TokenEvent::Freeze(frozen, note) => {
261 let mut m = serializer.serialize_map(Some(3))?;
262 m.serialize_entry("$type", "freeze")?;
263 m.serialize_entry("frozenIdentifier", frozen)?;
264 m.serialize_entry("publicNote", note)?;
265 m.end()
266 }
267 TokenEvent::Unfreeze(frozen, note) => {
268 let mut m = serializer.serialize_map(Some(3))?;
269 m.serialize_entry("$type", "unfreeze")?;
270 m.serialize_entry("frozenIdentifier", frozen)?;
271 m.serialize_entry("publicNote", note)?;
272 m.end()
273 }
274 TokenEvent::DestroyFrozenFunds(frozen, amount, note) => {
275 let mut m = serializer.serialize_map(Some(4))?;
276 m.serialize_entry("$type", "destroyFrozenFunds")?;
277 m.serialize_entry("frozenIdentifier", frozen)?;
278 m.serialize_entry("amount", &SafeU64(amount))?;
279 m.serialize_entry("publicNote", note)?;
280 m.end()
281 }
282 TokenEvent::Transfer(recipient, note, shared, private, amount) => {
283 let mut m = serializer.serialize_map(Some(6))?;
284 m.serialize_entry("$type", "transfer")?;
285 m.serialize_entry("recipient", recipient)?;
286 m.serialize_entry("publicNote", note)?;
287 m.serialize_entry("sharedEncryptedNote", &SafeOptEncNote(shared))?;
288 m.serialize_entry("privateEncryptedNote", &SafeOptEncNote(private))?;
289 m.serialize_entry("amount", &SafeU64(amount))?;
290 m.end()
291 }
292 TokenEvent::Claim(distribution_type, amount, note) => {
293 let mut m = serializer.serialize_map(Some(4))?;
294 m.serialize_entry("$type", "claim")?;
295 m.serialize_entry("distributionType", distribution_type)?;
296 m.serialize_entry("amount", &SafeU64(amount))?;
297 m.serialize_entry("publicNote", note)?;
298 m.end()
299 }
300 TokenEvent::EmergencyAction(action, note) => {
301 let mut m = serializer.serialize_map(Some(3))?;
302 m.serialize_entry("$type", "emergencyAction")?;
303 m.serialize_entry("action", action)?;
304 m.serialize_entry("publicNote", note)?;
305 m.end()
306 }
307 TokenEvent::ConfigUpdate(change, note) => {
308 let mut m = serializer.serialize_map(Some(3))?;
309 m.serialize_entry("$type", "configUpdate")?;
310 m.serialize_entry("configurationChange", change)?;
311 m.serialize_entry("publicNote", note)?;
312 m.end()
313 }
314 TokenEvent::ChangePriceForDirectPurchase(schedule, note) => {
315 let mut m = serializer.serialize_map(Some(3))?;
316 m.serialize_entry("$type", "changePriceForDirectPurchase")?;
317 m.serialize_entry("pricingSchedule", schedule)?;
318 m.serialize_entry("publicNote", note)?;
319 m.end()
320 }
321 TokenEvent::DirectPurchase(amount, credits) => {
322 let mut m = serializer.serialize_map(Some(3))?;
323 m.serialize_entry("$type", "directPurchase")?;
324 m.serialize_entry("amount", &SafeU64(amount))?;
325 m.serialize_entry("credits", &SafeU64(credits))?;
326 m.end()
327 }
328 TokenEvent::Shield(amount) => {
329 let mut m = serializer.serialize_map(Some(2))?;
330 m.serialize_entry("$type", "shield")?;
331 m.serialize_entry("amount", &SafeU64(amount))?;
332 m.end()
333 }
334 TokenEvent::Unshield(recipient, amount) => {
335 let mut m = serializer.serialize_map(Some(3))?;
336 m.serialize_entry("$type", "unshield")?;
337 m.serialize_entry("recipient", recipient)?;
338 m.serialize_entry("amount", &SafeU64(amount))?;
339 m.end()
340 }
341 TokenEvent::ShieldedTransfer => {
342 let mut m = serializer.serialize_map(Some(1))?;
343 m.serialize_entry("$type", "shieldedTransfer")?;
344 m.end()
345 }
346 TokenEvent::MintToPool(amount, actions_digest, note) => {
347 let mut m = serializer.serialize_map(Some(4))?;
348 m.serialize_entry("$type", "mintToPool")?;
349 m.serialize_entry("amount", &SafeU64(amount))?;
350 m.serialize_entry("actionsDigest", actions_digest)?;
351 m.serialize_entry("publicNote", note)?;
352 m.end()
353 }
354 TokenEvent::BurnFromPool(amount, actions_digest, note) => {
355 let mut m = serializer.serialize_map(Some(4))?;
356 m.serialize_entry("$type", "burnFromPool")?;
357 m.serialize_entry("amount", &SafeU64(amount))?;
358 m.serialize_entry("actionsDigest", actions_digest)?;
359 m.serialize_entry("publicNote", note)?;
360 m.end()
361 }
362 TokenEvent::ClaimToPool(amount) => {
363 let mut m = serializer.serialize_map(Some(2))?;
364 m.serialize_entry("$type", "claimToPool")?;
365 m.serialize_entry("amount", &SafeU64(amount))?;
366 m.end()
367 }
368 TokenEvent::DirectPurchaseToPool(amount, credits) => {
369 let mut m = serializer.serialize_map(Some(3))?;
370 m.serialize_entry("$type", "directPurchaseToPool")?;
371 m.serialize_entry("amount", &SafeU64(amount))?;
372 m.serialize_entry("credits", &SafeU64(credits))?;
373 m.end()
374 }
375 }
376 }
377}
378
379#[cfg(feature = "serde-conversion")]
380impl<'de> serde::Deserialize<'de> for TokenEvent {
381 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
382 use serde::de::{Error, IgnoredAny, MapAccess, Visitor};
383
384 #[derive(serde::Deserialize)]
388 #[serde(transparent)]
389 struct U64Safe(
390 #[serde(with = "crate::serialization::json::safe_integer::json_safe_u64")] u64,
391 );
392 #[derive(serde::Deserialize)]
393 #[serde(transparent)]
394 struct OptEncNote(
395 #[serde(
396 with = "crate::serialization::json::safe_integer::json_safe_option_encrypted_note"
397 )]
398 Option<(u32, u32, Vec<u8>)>,
399 );
400
401 struct V;
402
403 impl<'de> Visitor<'de> for V {
404 type Value = TokenEvent;
405
406 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
407 f.write_str("TokenEvent as a map with `$type` discriminator + variant fields")
408 }
409
410 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<TokenEvent, A::Error> {
411 let mut ty: Option<String> = None;
412 let mut amount: Option<u64> = None;
413 let mut credits: Option<u64> = None;
414 let mut recipient: Option<Identifier> = None;
415 let mut actions_digest: Option<Identifier> = None;
416 let mut burn_from: Option<Identifier> = None;
417 let mut frozen: Option<Identifier> = None;
418 let mut public_note: Option<String> = None;
419 let mut shared_note: Option<(u32, u32, Vec<u8>)> = None;
420 let mut private_note: Option<(u32, u32, Vec<u8>)> = None;
421 let mut distribution_type: Option<TokenDistributionTypeWithResolvedRecipient> =
422 None;
423 let mut action: Option<TokenEmergencyAction> = None;
424 let mut configuration_change: Option<TokenConfigurationChangeItem> = None;
425 let mut pricing_schedule: Option<TokenPricingSchedule> = None;
426
427 while let Some(key) = map.next_key::<String>()? {
428 match key.as_str() {
429 "$type" => ty = Some(map.next_value()?),
430 "amount" => amount = Some(map.next_value::<U64Safe>()?.0),
431 "credits" => credits = Some(map.next_value::<U64Safe>()?.0),
432 "recipient" => recipient = Some(map.next_value()?),
433 "actionsDigest" => actions_digest = Some(map.next_value()?),
434 "burnFromIdentifier" => burn_from = Some(map.next_value()?),
435 "frozenIdentifier" => frozen = Some(map.next_value()?),
436 "publicNote" => public_note = map.next_value()?,
437 "sharedEncryptedNote" => {
438 shared_note = map.next_value::<OptEncNote>()?.0;
439 }
440 "privateEncryptedNote" => {
441 private_note = map.next_value::<OptEncNote>()?.0;
442 }
443 "distributionType" => distribution_type = Some(map.next_value()?),
444 "action" => action = Some(map.next_value()?),
445 "configurationChange" => configuration_change = Some(map.next_value()?),
446 "pricingSchedule" => pricing_schedule = map.next_value()?,
447 _ => {
448 let _: IgnoredAny = map.next_value()?;
449 }
450 }
451 }
452
453 let ty = ty.ok_or_else(|| A::Error::missing_field("$type"))?;
454 match ty.as_str() {
455 "mint" => Ok(TokenEvent::Mint(
456 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
457 recipient.ok_or_else(|| A::Error::missing_field("recipient"))?,
458 public_note,
459 )),
460 "burn" => Ok(TokenEvent::Burn(
461 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
462 burn_from.ok_or_else(|| A::Error::missing_field("burnFromIdentifier"))?,
463 public_note,
464 )),
465 "freeze" => Ok(TokenEvent::Freeze(
466 frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
467 public_note,
468 )),
469 "unfreeze" => Ok(TokenEvent::Unfreeze(
470 frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
471 public_note,
472 )),
473 "destroyFrozenFunds" => Ok(TokenEvent::DestroyFrozenFunds(
474 frozen.ok_or_else(|| A::Error::missing_field("frozenIdentifier"))?,
475 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
476 public_note,
477 )),
478 "transfer" => Ok(TokenEvent::Transfer(
479 recipient.ok_or_else(|| A::Error::missing_field("recipient"))?,
480 public_note,
481 shared_note,
482 private_note,
483 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
484 )),
485 "claim" => Ok(TokenEvent::Claim(
486 distribution_type
487 .ok_or_else(|| A::Error::missing_field("distributionType"))?,
488 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
489 public_note,
490 )),
491 "emergencyAction" => Ok(TokenEvent::EmergencyAction(
492 action.ok_or_else(|| A::Error::missing_field("action"))?,
493 public_note,
494 )),
495 "configUpdate" => Ok(TokenEvent::ConfigUpdate(
496 configuration_change
497 .ok_or_else(|| A::Error::missing_field("configurationChange"))?,
498 public_note,
499 )),
500 "changePriceForDirectPurchase" => Ok(TokenEvent::ChangePriceForDirectPurchase(
501 pricing_schedule,
502 public_note,
503 )),
504 "directPurchase" => Ok(TokenEvent::DirectPurchase(
505 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
506 credits.ok_or_else(|| A::Error::missing_field("credits"))?,
507 )),
508 "shield" => Ok(TokenEvent::Shield(
509 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
510 )),
511 "unshield" => Ok(TokenEvent::Unshield(
512 recipient.ok_or_else(|| A::Error::missing_field("recipient"))?,
513 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
514 )),
515 "shieldedTransfer" => Ok(TokenEvent::ShieldedTransfer),
516 "mintToPool" => Ok(TokenEvent::MintToPool(
517 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
518 actions_digest.ok_or_else(|| A::Error::missing_field("actionsDigest"))?,
519 public_note,
520 )),
521 "burnFromPool" => Ok(TokenEvent::BurnFromPool(
522 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
523 actions_digest.ok_or_else(|| A::Error::missing_field("actionsDigest"))?,
524 public_note,
525 )),
526 "claimToPool" => Ok(TokenEvent::ClaimToPool(
527 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
528 )),
529 "directPurchaseToPool" => Ok(TokenEvent::DirectPurchaseToPool(
530 amount.ok_or_else(|| A::Error::missing_field("amount"))?,
531 credits.ok_or_else(|| A::Error::missing_field("credits"))?,
532 )),
533 other => Err(A::Error::unknown_variant(
534 other,
535 &[
536 "mint",
537 "burn",
538 "freeze",
539 "unfreeze",
540 "destroyFrozenFunds",
541 "transfer",
542 "claim",
543 "emergencyAction",
544 "configUpdate",
545 "changePriceForDirectPurchase",
546 "directPurchase",
547 "shield",
548 "unshield",
549 "shieldedTransfer",
550 "mintToPool",
551 "burnFromPool",
552 "claimToPool",
553 "directPurchaseToPool",
554 ],
555 )),
556 }
557 }
558 }
559
560 deserializer.deserialize_map(V)
561 }
562}
563
564#[cfg(all(
565 test,
566 feature = "json-conversion",
567 feature = "value-conversion",
568 feature = "serde-conversion"
569))]
570pub(crate) mod json_convertible_tests {
571 use super::*;
572 use platform_value::platform_value;
573 use serde_json::json;
574
575 pub(crate) fn mint_fixture() -> TokenEvent {
582 TokenEvent::Mint(
583 5_000,
584 Identifier::new([0xa1; 32]),
585 Some("genesis mint".to_string()),
586 )
587 }
588
589 #[test]
590 fn json_round_trip_mint() {
591 use crate::serialization::JsonConvertible;
592 let original = mint_fixture();
593 let json = original.to_json().expect("to_json");
594 assert_eq!(
597 json,
598 json!({
599 "$type": "mint",
600 "amount": 5_000,
601 "recipient": "Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp",
602 "publicNote": "genesis mint",
603 })
604 );
605 let recovered = TokenEvent::from_json(json).expect("from_json");
606 assert_eq!(original, recovered);
607 }
608
609 #[test]
610 fn json_round_trip_freeze_no_note() {
611 use crate::serialization::JsonConvertible;
612 let original = TokenEvent::Freeze(Identifier::new([0xb2; 32]), None);
613 let json = original.to_json().expect("to_json");
614 assert_eq!(
615 json,
616 json!({
617 "$type": "freeze",
618 "frozenIdentifier": "D2ZcUbtpG5sKq7XLeB4YnpNnTGSptKCxTddoNeydzJQq",
619 "publicNote": null,
620 })
621 );
622 let recovered = TokenEvent::from_json(json).expect("from_json");
623 assert_eq!(original, recovered);
624 }
625
626 #[test]
627 fn json_round_trip_direct_purchase() {
628 use crate::serialization::JsonConvertible;
629 let original = TokenEvent::DirectPurchase(100, 5_000);
630 let json = original.to_json().expect("to_json");
631 assert_eq!(
632 json,
633 json!({
634 "$type": "directPurchase",
635 "amount": 100,
636 "credits": 5_000,
637 })
638 );
639 let recovered = TokenEvent::from_json(json).expect("from_json");
640 assert_eq!(original, recovered);
641 }
642
643 #[test]
644 fn value_round_trip_mint() {
645 use crate::serialization::ValueConvertible;
646 let original = mint_fixture();
647 let value = original.to_object().expect("to_object");
648 assert_eq!(
650 value,
651 platform_value!({
652 "$type": "mint",
653 "amount": 5_000u64,
654 "recipient": Identifier::new([0xa1; 32]),
655 "publicNote": "genesis mint",
656 })
657 );
658 let recovered = TokenEvent::from_object(value).expect("from_object");
659 assert_eq!(original, recovered);
660 }
661
662 #[test]
663 fn value_round_trip_freeze_no_note() {
664 use crate::serialization::ValueConvertible;
665 let original = TokenEvent::Freeze(Identifier::new([0xb2; 32]), None);
666 let value = original.to_object().expect("to_object");
667 assert_eq!(
668 value,
669 platform_value!({
670 "$type": "freeze",
671 "frozenIdentifier": Identifier::new([0xb2; 32]),
672 "publicNote": null,
673 })
674 );
675 let recovered = TokenEvent::from_object(value).expect("from_object");
676 assert_eq!(original, recovered);
677 }
678
679 #[test]
680 fn value_round_trip_direct_purchase() {
681 use crate::serialization::ValueConvertible;
682 let original = TokenEvent::DirectPurchase(100, 5_000);
683 let value = original.to_object().expect("to_object");
684 assert_eq!(
686 value,
687 platform_value!({
688 "$type": "directPurchase",
689 "amount": 100u64,
690 "credits": 5_000u64,
691 })
692 );
693 let recovered = TokenEvent::from_object(value).expect("from_object");
694 assert_eq!(original, recovered);
695 }
696}
697
698impl fmt::Display for TokenEvent {
699 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700 match self {
701 TokenEvent::Mint(amount, recipient, note) => {
702 write!(f, "Mint {} to {}{}", amount, recipient, format_note(note))
703 }
704 TokenEvent::Burn(amount, burn_from_identifier, note) => {
705 write!(
706 f,
707 "Burn {} from {}{}",
708 amount,
709 burn_from_identifier,
710 format_note(note)
711 )
712 }
713 TokenEvent::Freeze(identity, note) => {
714 write!(f, "Freeze {}{}", identity, format_note(note))
715 }
716 TokenEvent::Unfreeze(identity, note) => {
717 write!(f, "Unfreeze {}{}", identity, format_note(note))
718 }
719 TokenEvent::DestroyFrozenFunds(identity, amount, note) => {
720 write!(
721 f,
722 "Destroy {} frozen from {}{}",
723 amount,
724 identity,
725 format_note(note)
726 )
727 }
728 TokenEvent::Transfer(to, note, _, _, amount) => {
729 write!(f, "Transfer {} to {}{}", amount, to, format_note(note))
730 }
731 TokenEvent::Claim(recipient, amount, note) => {
732 write!(
733 f,
734 "Claim {} by {:?}{}",
735 amount,
736 recipient,
737 format_note(note)
738 )
739 }
740 TokenEvent::EmergencyAction(action, note) => {
741 write!(f, "Emergency action {:?}{}", action, format_note(note))
742 }
743 TokenEvent::ConfigUpdate(change, note) => {
744 write!(f, "Configuration update {:?}{}", change, format_note(note))
745 }
746 TokenEvent::ChangePriceForDirectPurchase(schedule, note) => match schedule {
747 Some(s) => write!(f, "Change price schedule to {:?}{}", s, format_note(note)),
748 None => write!(f, "Disable direct purchase{}", format_note(note)),
749 },
750 TokenEvent::DirectPurchase(amount, credits) => {
751 write!(f, "Direct purchase of {} for {} credits", amount, credits)
752 }
753 TokenEvent::Shield(amount) => write!(f, "Shield {} into the token pool", amount),
754 TokenEvent::Unshield(to, amount) => {
755 write!(f, "Unshield {} from the token pool to {}", amount, to)
756 }
757 TokenEvent::ShieldedTransfer => write!(f, "Shielded transfer inside the token pool"),
758 TokenEvent::MintToPool(amount, _, note) => {
759 write!(
760 f,
761 "Mint {} into the token pool{}",
762 amount,
763 format_note(note)
764 )
765 }
766 TokenEvent::BurnFromPool(amount, _, note) => {
767 write!(
768 f,
769 "Burn {} from the token pool{}",
770 amount,
771 format_note(note)
772 )
773 }
774 TokenEvent::ClaimToPool(amount) => {
775 write!(f, "Claim {} into the token pool", amount)
776 }
777 TokenEvent::DirectPurchaseToPool(amount, credits) => write!(
778 f,
779 "Direct purchase of {} into the token pool for {} credits",
780 amount, credits
781 ),
782 }
783 }
784}
785
786fn format_note(note: &Option<String>) -> String {
787 match note {
788 Some(n) => format!(" (note: {})", n),
789 None => String::new(),
790 }
791}
792
793impl TokenEvent {
794 pub fn associated_document_type_name(&self) -> &str {
795 match self {
796 TokenEvent::Mint(..) => "mint",
797 TokenEvent::Burn(..) => "burn",
798 TokenEvent::Freeze(..) => "freeze",
799 TokenEvent::Unfreeze(..) => "unfreeze",
800 TokenEvent::DestroyFrozenFunds(..) => "destroyFrozenFunds",
801 TokenEvent::Transfer(..) => "transfer",
802 TokenEvent::Claim(..) => "claim",
803 TokenEvent::EmergencyAction(..) => "emergencyAction",
804 TokenEvent::ConfigUpdate(..) => "configUpdate",
805 TokenEvent::DirectPurchase(..) => "directPurchase",
806 TokenEvent::ChangePriceForDirectPurchase(..) => "directPricing",
807 TokenEvent::Shield(..) => "shield",
808 TokenEvent::Unshield(..) => "unshield",
809 TokenEvent::ShieldedTransfer => "shieldedTransfer",
810 TokenEvent::MintToPool(..) => "mintToPool",
811 TokenEvent::BurnFromPool(..) => "burnFromPool",
812 TokenEvent::ClaimToPool(..) => "claimToPool",
813 TokenEvent::DirectPurchaseToPool(..) => "directPurchaseToPool",
814 }
815 }
816
817 pub fn public_note(&self) -> Option<&str> {
824 match self {
825 TokenEvent::Mint(_, _, note)
826 | TokenEvent::Burn(_, _, note)
827 | TokenEvent::Freeze(_, note)
828 | TokenEvent::Unfreeze(_, note)
829 | TokenEvent::DestroyFrozenFunds(_, _, note)
830 | TokenEvent::Transfer(_, note, _, _, _)
831 | TokenEvent::Claim(_, _, note)
832 | TokenEvent::EmergencyAction(_, note)
833 | TokenEvent::ConfigUpdate(_, note)
834 | TokenEvent::ChangePriceForDirectPurchase(_, note)
835 | TokenEvent::MintToPool(_, _, note)
836 | TokenEvent::BurnFromPool(_, _, note) => note.as_deref(),
837 TokenEvent::DirectPurchase(_, _)
838 | TokenEvent::Shield(_)
839 | TokenEvent::Unshield(_, _)
840 | TokenEvent::ShieldedTransfer
841 | TokenEvent::ClaimToPool(_)
842 | TokenEvent::DirectPurchaseToPool(_, _) => None,
843 }
844 }
845
846 pub fn associated_document_type<'a>(
847 &self,
848 token_history_contract: &'a DataContract,
849 ) -> Result<DocumentTypeRef<'a>, ProtocolError> {
850 Ok(token_history_contract.document_type_for_name(self.associated_document_type_name())?)
851 }
852
853 pub fn build_historical_document_owned(
854 self,
855 token_id: Identifier,
856 owner_id: Identifier,
857 owner_nonce: IdentityNonce,
858 block_info: &BlockInfo,
859 platform_version: &PlatformVersion,
860 ) -> Result<Document, ProtocolError> {
861 let document_id = Document::generate_document_id_v0(
862 &token_id,
863 &owner_id,
864 format!("history_{}", self.associated_document_type_name()).as_str(),
865 owner_nonce.to_be_bytes().as_slice(),
866 );
867
868 let properties = match self {
869 TokenEvent::Mint(mint_amount, recipient_id, public_note) => {
870 let mut properties = BTreeMap::from([
871 ("tokenId".to_string(), token_id.into()),
872 ("recipientId".to_string(), recipient_id.into()),
873 ("amount".to_string(), mint_amount.into()),
874 ]);
875 if let Some(note) = public_note {
876 properties.insert("note".to_string(), note.into());
877 }
878 properties
879 }
880 TokenEvent::Burn(burn_amount, burn_from_identifier, public_note) => {
881 let mut properties = BTreeMap::from([
882 ("tokenId".to_string(), token_id.into()),
883 ("burnFromId".to_string(), burn_from_identifier.into()),
884 ("amount".to_string(), burn_amount.into()),
885 ]);
886 if let Some(note) = public_note {
887 properties.insert("note".to_string(), note.into());
888 }
889 properties
890 }
891 TokenEvent::Transfer(
892 to,
893 public_note,
894 token_event_shared_encrypted_note,
895 token_event_personal_encrypted_note,
896 amount,
897 ) => {
898 let mut properties = BTreeMap::from([
899 ("tokenId".to_string(), token_id.into()),
900 ("amount".to_string(), amount.into()),
901 ("toIdentityId".to_string(), to.into()),
902 ]);
903 if let Some(note) = public_note {
904 properties.insert("publicNote".to_string(), note.into());
905 }
906 if let Some((sender_key_index, recipient_key_index, note)) =
907 token_event_shared_encrypted_note
908 {
909 properties.insert("encryptedSharedNote".to_string(), note.into());
910 properties.insert("senderKeyIndex".to_string(), sender_key_index.into());
911 properties.insert("recipientKeyIndex".to_string(), recipient_key_index.into());
912 }
913
914 if let Some((root_encryption_key_index, derivation_encryption_key_index, note)) =
915 token_event_personal_encrypted_note
916 {
917 properties.insert("encryptedPersonalNote".to_string(), note.into());
918 properties.insert(
919 "rootEncryptionKeyIndex".to_string(),
920 root_encryption_key_index.into(),
921 );
922 properties.insert(
923 "derivationEncryptionKeyIndex".to_string(),
924 derivation_encryption_key_index.into(),
925 );
926 }
927 properties
928 }
929 TokenEvent::Freeze(frozen_identity_id, public_note) => {
930 let mut properties = BTreeMap::from([
931 ("tokenId".to_string(), token_id.into()),
932 ("frozenIdentityId".to_string(), frozen_identity_id.into()),
933 ]);
934 if let Some(note) = public_note {
935 properties.insert("note".to_string(), note.into());
936 }
937 properties
938 }
939 TokenEvent::Unfreeze(frozen_identity_id, public_note) => {
940 let mut properties = BTreeMap::from([
941 ("tokenId".to_string(), token_id.into()),
942 ("frozenIdentityId".to_string(), frozen_identity_id.into()),
943 ]);
944 if let Some(note) = public_note {
945 properties.insert("note".to_string(), note.into());
946 }
947 properties
948 }
949 TokenEvent::DestroyFrozenFunds(frozen_identity_id, amount, public_note) => {
950 let mut properties = BTreeMap::from([
951 ("tokenId".to_string(), token_id.into()),
952 ("frozenIdentityId".to_string(), frozen_identity_id.into()),
953 ("destroyedAmount".to_string(), amount.into()),
954 ]);
955 if let Some(note) = public_note {
956 properties.insert("note".to_string(), note.into());
957 }
958 properties
959 }
960 TokenEvent::EmergencyAction(action, public_note) => {
961 let mut properties = BTreeMap::from([
962 ("tokenId".to_string(), token_id.into()),
963 ("action".to_string(), (action as u8).into()),
964 ]);
965 if let Some(note) = public_note {
966 properties.insert("note".to_string(), note.into());
967 }
968 properties
969 }
970 TokenEvent::ConfigUpdate(configuration_change_item, public_note) => {
971 let mut properties = BTreeMap::from([
972 ("tokenId".to_string(), token_id.into()),
973 (
974 "changeItemType".to_string(),
975 configuration_change_item.u8_item_index().into(),
976 ),
977 (
978 "changeItem".to_string(),
979 configuration_change_item
980 .serialize_consume_to_bytes_with_platform_version(platform_version)?
981 .into(),
982 ),
983 ]);
984 if let Some(note) = public_note {
985 properties.insert("note".to_string(), note.into());
986 }
987 properties
988 }
989 TokenEvent::Claim(recipient, amount, public_note) => {
990 let (recipient_type, recipient_id, distribution_type) = match recipient {
991 TokenDistributionTypeWithResolvedRecipient::PreProgrammed(identifier) => {
992 (1u8, identifier, 0u8)
993 }
994 TokenDistributionTypeWithResolvedRecipient::Perpetual(
995 TokenDistributionResolvedRecipient::ContractOwnerIdentity(identifier),
996 ) => (0, identifier, 1),
997 TokenDistributionTypeWithResolvedRecipient::Perpetual(
998 TokenDistributionResolvedRecipient::Identity(identifier),
999 ) => (1, identifier, 1),
1000 TokenDistributionTypeWithResolvedRecipient::Perpetual(
1001 TokenDistributionResolvedRecipient::Evonode(identifier),
1002 ) => (2, identifier, 1),
1003 TokenDistributionTypeWithResolvedRecipient::OncePerIdentity(identifier) => {
1004 (1, identifier, 2)
1005 }
1006 };
1007
1008 let mut properties = BTreeMap::from([
1009 ("tokenId".to_string(), token_id.into()),
1010 ("recipientType".to_string(), recipient_type.into()),
1011 ("recipientId".to_string(), recipient_id.into()),
1012 ("distributionType".to_string(), distribution_type.into()),
1013 ("amount".to_string(), amount.into()),
1014 ]);
1015
1016 if let Some(note) = public_note {
1017 properties.insert("note".to_string(), note.into());
1018 }
1019 properties
1020 }
1021 TokenEvent::ChangePriceForDirectPurchase(price, note) => {
1022 let mut properties = BTreeMap::from([("tokenId".to_string(), token_id.into())]);
1023
1024 if let Some(price_schedule) = price {
1025 properties.insert(
1026 "priceSchedule".to_string(),
1027 price_schedule
1028 .serialize_consume_to_bytes_with_platform_version(platform_version)?
1029 .into(),
1030 );
1031 }
1032
1033 if let Some(note) = note {
1034 properties.insert("note".to_string(), note.into());
1035 }
1036
1037 properties
1038 }
1039 TokenEvent::DirectPurchase(amount, total_cost) => BTreeMap::from([
1040 ("tokenId".to_string(), token_id.into()),
1041 ("tokenAmount".to_string(), amount.into()),
1042 ("purchaseCost".to_string(), total_cost.into()),
1043 ]),
1044 TokenEvent::Shield(amount) => BTreeMap::from([
1045 ("tokenId".to_string(), token_id.into()),
1046 ("amount".to_string(), amount.into()),
1047 ]),
1048 TokenEvent::Unshield(recipient_id, amount) => BTreeMap::from([
1049 ("tokenId".to_string(), token_id.into()),
1050 ("recipientId".to_string(), recipient_id.into()),
1051 ("amount".to_string(), amount.into()),
1052 ]),
1053 TokenEvent::ShieldedTransfer => {
1054 BTreeMap::from([("tokenId".to_string(), token_id.into())])
1055 }
1056 TokenEvent::MintToPool(amount, _, _) | TokenEvent::BurnFromPool(amount, _, _) => {
1057 BTreeMap::from([
1058 ("tokenId".to_string(), token_id.into()),
1059 ("amount".to_string(), amount.into()),
1060 ])
1061 }
1062 TokenEvent::ClaimToPool(amount) => BTreeMap::from([
1063 ("tokenId".to_string(), token_id.into()),
1064 ("amount".to_string(), amount.into()),
1065 ]),
1066 TokenEvent::DirectPurchaseToPool(amount, total_cost) => BTreeMap::from([
1067 ("tokenId".to_string(), token_id.into()),
1068 ("tokenAmount".to_string(), amount.into()),
1069 ("purchaseCost".to_string(), total_cost.into()),
1070 ]),
1071 };
1072
1073 let document: Document = DocumentV0 {
1074 contract_version: None,
1075 id: document_id,
1076 owner_id,
1077 properties,
1078 revision: None,
1079 created_at: Some(block_info.time_ms),
1080 updated_at: None,
1081 transferred_at: None,
1082 created_at_block_height: Some(block_info.height),
1083 updated_at_block_height: None,
1084 transferred_at_block_height: None,
1085 created_at_core_block_height: None,
1086 updated_at_core_block_height: None,
1087 transferred_at_core_block_height: None,
1088 creator_id: None,
1089 moderated_at: None,
1090 moderated_by: None,
1091 }
1092 .into();
1093
1094 Ok(document)
1095 }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100 use super::*;
1101
1102 fn test_id() -> Identifier {
1103 Identifier::from([1u8; 32])
1104 }
1105
1106 fn test_id_2() -> Identifier {
1107 Identifier::from([2u8; 32])
1108 }
1109
1110 #[test]
1113 fn associated_name_mint() {
1114 let event = TokenEvent::Mint(0, test_id(), None);
1115 assert_eq!(event.associated_document_type_name(), "mint");
1116 }
1117
1118 #[test]
1119 fn associated_name_burn() {
1120 let event = TokenEvent::Burn(0, test_id(), None);
1121 assert_eq!(event.associated_document_type_name(), "burn");
1122 }
1123
1124 #[test]
1125 fn associated_name_freeze() {
1126 let event = TokenEvent::Freeze(test_id(), None);
1127 assert_eq!(event.associated_document_type_name(), "freeze");
1128 }
1129
1130 #[test]
1131 fn associated_name_unfreeze() {
1132 let event = TokenEvent::Unfreeze(test_id(), None);
1133 assert_eq!(event.associated_document_type_name(), "unfreeze");
1134 }
1135
1136 #[test]
1137 fn associated_name_destroy_frozen_funds() {
1138 let event = TokenEvent::DestroyFrozenFunds(test_id(), 0, None);
1139 assert_eq!(event.associated_document_type_name(), "destroyFrozenFunds");
1140 }
1141
1142 #[test]
1143 fn associated_name_transfer() {
1144 let event = TokenEvent::Transfer(test_id(), None, None, None, 0);
1145 assert_eq!(event.associated_document_type_name(), "transfer");
1146 }
1147
1148 #[test]
1149 fn associated_name_claim() {
1150 let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id());
1151 let event = TokenEvent::Claim(recipient, 0, None);
1152 assert_eq!(event.associated_document_type_name(), "claim");
1153 }
1154
1155 #[test]
1156 fn associated_name_emergency_action() {
1157 let event = TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None);
1158 assert_eq!(event.associated_document_type_name(), "emergencyAction");
1159 }
1160
1161 #[test]
1162 fn associated_name_config_update() {
1163 let event = TokenEvent::ConfigUpdate(
1164 TokenConfigurationChangeItem::TokenConfigurationNoChange,
1165 None,
1166 );
1167 assert_eq!(event.associated_document_type_name(), "configUpdate");
1168 }
1169
1170 #[test]
1171 fn associated_name_direct_purchase() {
1172 let event = TokenEvent::DirectPurchase(0, 0);
1173 assert_eq!(event.associated_document_type_name(), "directPurchase");
1174 }
1175
1176 #[test]
1177 fn associated_name_change_price() {
1178 let event = TokenEvent::ChangePriceForDirectPurchase(None, None);
1179 assert_eq!(event.associated_document_type_name(), "directPricing");
1180 }
1181
1182 #[test]
1185 fn all_document_type_names_are_unique() {
1186 let recipient = TokenDistributionTypeWithResolvedRecipient::PreProgrammed(test_id());
1187 let events: Vec<TokenEvent> = vec![
1188 TokenEvent::Mint(0, test_id(), None),
1189 TokenEvent::Burn(0, test_id(), None),
1190 TokenEvent::Freeze(test_id(), None),
1191 TokenEvent::Unfreeze(test_id(), None),
1192 TokenEvent::DestroyFrozenFunds(test_id(), 0, None),
1193 TokenEvent::Transfer(test_id(), None, None, None, 0),
1194 TokenEvent::Claim(recipient, 0, None),
1195 TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None),
1196 TokenEvent::ConfigUpdate(
1197 TokenConfigurationChangeItem::TokenConfigurationNoChange,
1198 None,
1199 ),
1200 TokenEvent::DirectPurchase(0, 0),
1201 TokenEvent::ChangePriceForDirectPurchase(None, None),
1202 ];
1203 let names: Vec<&str> = events
1204 .iter()
1205 .map(|e| e.associated_document_type_name())
1206 .collect();
1207 let mut unique = names.clone();
1208 unique.sort();
1209 unique.dedup();
1210 assert_eq!(
1211 names.len(),
1212 unique.len(),
1213 "Duplicate document type names found"
1214 );
1215 }
1216
1217 #[test]
1220 fn format_note_none_returns_empty() {
1221 assert_eq!(format_note(&None), "");
1222 }
1223
1224 #[test]
1225 fn format_note_some_returns_formatted() {
1226 assert_eq!(format_note(&Some("hello".to_string())), " (note: hello)");
1227 }
1228}
1229
1230#[cfg(test)]
1231mod public_note_tests {
1232 use super::*;
1233 use crate::group::action_event::GroupActionEvent;
1234
1235 fn actions_digest() -> Identifier {
1236 Identifier::from([7u8; 32])
1237 }
1238
1239 #[test]
1242 fn should_show_the_proposers_note_on_a_pool_mint_or_burn() {
1243 let mint = TokenEvent::MintToPool(
1244 100,
1245 actions_digest(),
1246 Some("quarterly issuance".to_string()),
1247 );
1248 assert_eq!(mint.public_note(), Some("quarterly issuance"));
1249 assert_eq!(
1250 GroupActionEvent::TokenEvent(mint).public_note(),
1251 Some("quarterly issuance")
1252 );
1253
1254 let burn = TokenEvent::BurnFromPool(
1255 40,
1256 actions_digest(),
1257 Some("retiring treasury notes".to_string()),
1258 );
1259 assert_eq!(burn.public_note(), Some("retiring treasury notes"));
1260 assert_eq!(
1261 GroupActionEvent::TokenEvent(burn).public_note(),
1262 Some("retiring treasury notes")
1263 );
1264 }
1265
1266 #[test]
1268 fn should_report_no_note_on_an_unannotated_pool_mint_or_burn() {
1269 assert_eq!(
1270 TokenEvent::MintToPool(100, actions_digest(), None).public_note(),
1271 None
1272 );
1273 assert_eq!(
1274 TokenEvent::BurnFromPool(40, actions_digest(), None).public_note(),
1275 None
1276 );
1277 }
1278}