Skip to main content

dpp/group/
action_event.rs

1#[cfg(feature = "json-conversion")]
2use crate::serialization::JsonConvertible;
3#[cfg(feature = "value-conversion")]
4use crate::serialization::ValueConvertible;
5use crate::tokens::token_event::TokenEvent;
6use crate::ProtocolError;
7use bincode::{Decode, DecodeUntrusted, Encode};
8use platform_serialization_derive::{
9    PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize,
10};
11#[cfg(feature = "serde-conversion")]
12use serde::{Deserialize, Serialize};
13
14#[derive(
15    Debug,
16    PartialEq,
17    PartialOrd,
18    Clone,
19    Eq,
20    Encode,
21    Decode,
22    PlatformDeserializeTrusted,
23    PlatformDeserializeUntrusted,
24    PlatformSerialize,
25    DecodeUntrusted,
26)]
27#[cfg_attr(
28    feature = "serde-conversion",
29    derive(Serialize, Deserialize),
30    // Internally tagged with `$kind`. The inner `TokenEvent` is itself
31    // internally tagged with `$type`, so its fields flatten alongside `$kind`
32    // into one object. `$kind` keeps the two discriminators distinct. Wire
33    // shape, e.g.:
34    //   {"$kind": "tokenEvent", "$type": "mint", "amount": <n>, "recipient": <id>}
35    serde(tag = "$kind", rename_all = "camelCase")
36)]
37#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
38#[platform_serialize(unversioned)] //versioned directly, no need to use platform_version
39pub enum GroupActionEvent {
40    TokenEvent(TokenEvent),
41}
42
43// Manual impl because GroupActionEvent is a flat enum (not versioned V0/V1).
44// Its inner type TokenEvent also has a manual impl — see token_event.rs.
45#[cfg(feature = "json-conversion")]
46impl JsonConvertible for GroupActionEvent {}
47
48#[cfg(all(
49    test,
50    feature = "json-conversion",
51    feature = "value-conversion",
52    feature = "serde-conversion"
53))]
54mod json_convertible_tests {
55    use super::*;
56    use platform_value::platform_value;
57    use serde_json::json;
58
59    // `GroupActionEvent` uses `tag = "$kind"` (internal). Plain `kind`
60    // (no `$` prefix) because the wire level has no other `$`-prefixed
61    // fields. Distinct from the inner `TokenEvent`'s `type` discriminator
62    // — both keys coexist at the flattened top level without collision.
63
64    #[test]
65    fn json_round_trip_token_event_mint() {
66        use crate::serialization::JsonConvertible;
67        let original = GroupActionEvent::TokenEvent(
68            crate::tokens::token_event::json_convertible_tests::mint_fixture(),
69        );
70        let json = original.to_json().expect("to_json");
71        // Outer `kind: "tokenEvent"` from GroupActionEvent. Inner TokenEvent
72        // (custom serde) flattens its named fields at the same level.
73        assert_eq!(
74            json,
75            json!({
76                "$kind": "tokenEvent",
77                "$type": "mint",
78                "amount": 5_000,
79                "recipient": "Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp",
80                "publicNote": "genesis mint",
81            })
82        );
83        let recovered = GroupActionEvent::from_json(json).expect("from_json");
84        assert_eq!(original, recovered);
85    }
86
87    #[test]
88    fn value_round_trip_token_event_mint() {
89        use crate::serialization::ValueConvertible;
90        let original = GroupActionEvent::TokenEvent(
91            crate::tokens::token_event::json_convertible_tests::mint_fixture(),
92        );
93        let value = original.to_object().expect("to_object");
94        assert_eq!(
95            value,
96            platform_value!({
97                "$kind": "tokenEvent",
98                "$type": "mint",
99                "amount": 5_000u64,
100                "recipient": platform_value::Identifier::new([0xa1; 32]),
101                "publicNote": "genesis mint",
102            })
103        );
104        let recovered = GroupActionEvent::from_object(value).expect("from_object");
105        assert_eq!(original, recovered);
106    }
107
108    // The two tests below pin the deepest custom-serde composition in the
109    // crate: GroupActionEvent (`tag = "$kind"`, derive — buffers inner content
110    // through serde's ContentDeserializer) → TokenEvent (custom impl) →
111    // externally-tagged TokenConfigurationChangeItem /
112    // TokenDistributionTypeWithResolvedRecipient → the custom
113    // internally-tagged AuthorizedActionTakers /
114    // TokenDistributionResolvedRecipient. The Identity-carrying variants
115    // specifically exercise the Identifier dual-shape path (HR base58 string
116    // vs buffered bytes) under the ContentDeserializer HR-quirk. A one-sided
117    // edit to any custom Serialize/Deserialize pair in the chain fails here.
118
119    fn change_item_fixture() -> GroupActionEvent {
120        use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem;
121        use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers;
122        GroupActionEvent::TokenEvent(TokenEvent::ConfigUpdate(
123            TokenConfigurationChangeItem::ConventionsControlGroup(
124                AuthorizedActionTakers::Identity(platform_value::Identifier::from([0x42u8; 32])),
125            ),
126            Some("rotate control".to_string()),
127        ))
128    }
129
130    fn claim_fixture() -> GroupActionEvent {
131        use crate::data_contract::associated_token::token_distribution_key::TokenDistributionTypeWithResolvedRecipient;
132        use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionResolvedRecipient;
133        GroupActionEvent::TokenEvent(TokenEvent::Claim(
134            TokenDistributionTypeWithResolvedRecipient::Perpetual(
135                TokenDistributionResolvedRecipient::Evonode(platform_value::Identifier::from(
136                    [0x42u8; 32],
137                )),
138            ),
139            750,
140            Some("payout".to_string()),
141        ))
142    }
143
144    #[test]
145    fn json_round_trip_config_update_with_identity_action_taker() {
146        use crate::serialization::JsonConvertible;
147        let original = change_item_fixture();
148        let json = original.to_json().expect("to_json");
149        assert_eq!(
150            json,
151            json!({
152                "$kind": "tokenEvent",
153                "$type": "configUpdate",
154                "configurationChange": {
155                    "$type": "conventionsControlGroup",
156                    "value": {
157                        "$type": "identity",
158                        "identity": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf",
159                    },
160                },
161                "publicNote": "rotate control",
162            })
163        );
164        let recovered = GroupActionEvent::from_json(json).expect("from_json");
165        assert_eq!(original, recovered);
166    }
167
168    #[test]
169    fn value_round_trip_config_update_with_identity_action_taker() {
170        use crate::serialization::ValueConvertible;
171        let original = change_item_fixture();
172        let value = original.to_object().expect("to_object");
173        assert_eq!(
174            value,
175            platform_value!({
176                "$kind": "tokenEvent",
177                "$type": "configUpdate",
178                "configurationChange": {
179                    "$type": "conventionsControlGroup",
180                    "value": {
181                        "$type": "identity",
182                        "identity": platform_value::Identifier::from([0x42u8; 32]),
183                    },
184                },
185                "publicNote": "rotate control",
186            })
187        );
188        let recovered = GroupActionEvent::from_object(value).expect("from_object");
189        assert_eq!(original, recovered);
190    }
191
192    #[test]
193    fn json_round_trip_claim_with_resolved_recipient() {
194        use crate::serialization::JsonConvertible;
195        let original = claim_fixture();
196        let json = original.to_json().expect("to_json");
197        assert_eq!(
198            json,
199            json!({
200                "$kind": "tokenEvent",
201                "$type": "claim",
202                "distributionType": {
203                    "$type": "perpetual",
204                    "value": {
205                        "$type": "evonode",
206                        "identity": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf",
207                    },
208                },
209                "amount": 750,
210                "publicNote": "payout",
211            })
212        );
213        let recovered = GroupActionEvent::from_json(json).expect("from_json");
214        assert_eq!(original, recovered);
215    }
216
217    #[test]
218    fn value_round_trip_claim_with_resolved_recipient() {
219        use crate::serialization::ValueConvertible;
220        let original = claim_fixture();
221        let value = original.to_object().expect("to_object");
222        assert_eq!(
223            value,
224            platform_value!({
225                "$kind": "tokenEvent",
226                "$type": "claim",
227                "distributionType": {
228                    "$type": "perpetual",
229                    "value": {
230                        "$type": "evonode",
231                        "identity": platform_value::Identifier::from([0x42u8; 32]),
232                    },
233                },
234                "amount": 750u64,
235                "publicNote": "payout",
236            })
237        );
238        let recovered = GroupActionEvent::from_object(value).expect("from_object");
239        assert_eq!(original, recovered);
240    }
241}
242
243use std::fmt;
244
245impl fmt::Display for GroupActionEvent {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        match self {
248            GroupActionEvent::TokenEvent(event) => write!(f, "Token event: {}", event),
249        }
250    }
251}
252
253impl GroupActionEvent {
254    /// Returns a reference to the public note if the variant includes one.
255    pub fn public_note(&self) -> Option<&str> {
256        match self {
257            GroupActionEvent::TokenEvent(token_event) => token_event.public_note(),
258        }
259    }
260
261    /// Returns a name of the event
262    pub fn event_name(&self) -> String {
263        match self {
264            GroupActionEvent::TokenEvent(token_event) => {
265                format!("Token: {}", token_event.associated_document_type_name())
266            }
267        }
268    }
269}