Skip to main content

dpp/data_contract/associated_token/
token_distribution_key.rs

1use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::{TokenDistributionRecipient, TokenDistributionResolvedRecipient};
2use crate::errors::ProtocolError;
3use bincode::{Decode, Encode, DecodeUntrusted};
4use platform_serialization_derive::{PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize};
5use platform_value::Identifier;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment;
9use crate::prelude::TimestampMillis;
10
11/// Represents the type of token distribution.
12///
13/// - `PreProgrammed`: A scheduled distribution with predefined rules.
14/// - `Perpetual`: A continuous or recurring distribution.
15#[derive(
16    Serialize,
17    Deserialize,
18    Decode,
19    Encode,
20    Debug,
21    Clone,
22    Copy,
23    PartialEq,
24    Eq,
25    PartialOrd,
26    Default,
27    DecodeUntrusted,
28)]
29pub enum TokenDistributionType {
30    /// A pre-programmed distribution scheduled for a specific time.
31    #[default]
32    PreProgrammed = 0,
33
34    /// A perpetual distribution that occurs at regular intervals.
35    Perpetual = 1,
36}
37
38/// Represents a token distribution with a resolved recipient.
39///
40/// - `PreProgrammed(Identifier)`: A predefined recipient for a scheduled distribution.
41/// - `Perpetual(TokenDistributionResolvedRecipient)`: A resolved recipient for an ongoing distribution.
42#[derive(
43    Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, PartialOrd, DecodeUntrusted,
44)]
45#[serde(
46    into = "TokenDistributionTypeWithResolvedRecipientRepr",
47    from = "TokenDistributionTypeWithResolvedRecipientRepr"
48)]
49pub enum TokenDistributionTypeWithResolvedRecipient {
50    /// A scheduled distribution with a known recipient.
51    PreProgrammed(Identifier),
52
53    /// A perpetual distribution with a resolved recipient.
54    Perpetual(TokenDistributionResolvedRecipient),
55}
56
57// Internal-`$type` serde shape with a uniform `value` payload (single-payload
58// variants). Bincode `Encode`/`Decode` on the outer enum are untouched.
59#[derive(Serialize, Deserialize)]
60#[serde(tag = "$type", rename_all = "camelCase")]
61enum TokenDistributionTypeWithResolvedRecipientRepr {
62    PreProgrammed {
63        value: Identifier,
64    },
65    Perpetual {
66        value: TokenDistributionResolvedRecipient,
67    },
68}
69
70impl From<TokenDistributionTypeWithResolvedRecipient>
71    for TokenDistributionTypeWithResolvedRecipientRepr
72{
73    fn from(m: TokenDistributionTypeWithResolvedRecipient) -> Self {
74        match m {
75            TokenDistributionTypeWithResolvedRecipient::PreProgrammed(value) => {
76                Self::PreProgrammed { value }
77            }
78            TokenDistributionTypeWithResolvedRecipient::Perpetual(value) => {
79                Self::Perpetual { value }
80            }
81        }
82    }
83}
84
85impl From<TokenDistributionTypeWithResolvedRecipientRepr>
86    for TokenDistributionTypeWithResolvedRecipient
87{
88    fn from(r: TokenDistributionTypeWithResolvedRecipientRepr) -> Self {
89        match r {
90            TokenDistributionTypeWithResolvedRecipientRepr::PreProgrammed { value } => {
91                Self::PreProgrammed(value)
92            }
93            TokenDistributionTypeWithResolvedRecipientRepr::Perpetual { value } => {
94                Self::Perpetual(value)
95            }
96        }
97    }
98}
99
100/// Contains information about a specific token distribution instance.
101///
102/// - `PreProgrammed(TimestampMillis, Identifier)`: A scheduled distribution with a timestamp and recipient.
103/// - `Perpetual(RewardDistributionMoment, RewardDistributionMoment, TokenDistributionResolvedRecipient)`:
104///   A perpetual distribution with previous and next distribution moments, along with the resolved recipient.
105#[derive(
106    Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, PartialOrd, DecodeUntrusted,
107)]
108#[serde(into = "TokenDistributionInfoRepr", from = "TokenDistributionInfoRepr")]
109pub enum TokenDistributionInfo {
110    /// A pre-programmed token distribution set for a specific time.
111    /// Contains the scheduled timestamp and the recipient’s identifier.
112    PreProgrammed(TimestampMillis, Identifier),
113
114    /// A perpetual token distribution with moment for distribution.
115    /// The moment is the beginning of the perpetual distribution cycle
116    /// Includes the last and next distribution times and the resolved recipient.
117    Perpetual(RewardDistributionMoment, TokenDistributionResolvedRecipient),
118}
119
120// Internal-`$type` serde shape with named fields (multi-field variants).
121// `TimestampMillis` (u64) carries `json_safe_u64` on the Repr field — JS-safe
122// (string above MAX_SAFE_INTEGER in HR JSON), Content-safe (never u128).
123// `RewardDistributionMoment` is itself internally tagged; bincode untouched.
124#[derive(Serialize, Deserialize)]
125#[serde(tag = "$type", rename_all = "camelCase")]
126enum TokenDistributionInfoRepr {
127    PreProgrammed {
128        #[cfg_attr(
129            feature = "json-conversion",
130            serde(with = "crate::serialization::json_safe_u64")
131        )]
132        timestamp: TimestampMillis,
133        identity: Identifier,
134    },
135    Perpetual {
136        moment: RewardDistributionMoment,
137        recipient: TokenDistributionResolvedRecipient,
138    },
139}
140
141impl From<TokenDistributionInfo> for TokenDistributionInfoRepr {
142    fn from(m: TokenDistributionInfo) -> Self {
143        match m {
144            TokenDistributionInfo::PreProgrammed(timestamp, identity) => Self::PreProgrammed {
145                timestamp,
146                identity,
147            },
148            TokenDistributionInfo::Perpetual(moment, recipient) => {
149                Self::Perpetual { moment, recipient }
150            }
151        }
152    }
153}
154
155impl From<TokenDistributionInfoRepr> for TokenDistributionInfo {
156    fn from(r: TokenDistributionInfoRepr) -> Self {
157        match r {
158            TokenDistributionInfoRepr::PreProgrammed {
159                timestamp,
160                identity,
161            } => Self::PreProgrammed(timestamp, identity),
162            TokenDistributionInfoRepr::Perpetual { moment, recipient } => {
163                Self::Perpetual(moment, recipient)
164            }
165        }
166    }
167}
168
169impl From<TokenDistributionInfo> for TokenDistributionTypeWithResolvedRecipient {
170    fn from(info: TokenDistributionInfo) -> Self {
171        match info {
172            TokenDistributionInfo::PreProgrammed(_, recipient) => {
173                TokenDistributionTypeWithResolvedRecipient::PreProgrammed(recipient)
174            }
175            TokenDistributionInfo::Perpetual(_, recipient) => {
176                TokenDistributionTypeWithResolvedRecipient::Perpetual(recipient)
177            }
178        }
179    }
180}
181
182impl From<&TokenDistributionInfo> for TokenDistributionTypeWithResolvedRecipient {
183    fn from(info: &TokenDistributionInfo) -> Self {
184        match info {
185            TokenDistributionInfo::PreProgrammed(_, recipient) => {
186                TokenDistributionTypeWithResolvedRecipient::PreProgrammed(*recipient)
187            }
188            TokenDistributionInfo::Perpetual(_, recipient) => {
189                TokenDistributionTypeWithResolvedRecipient::Perpetual(recipient.clone())
190            }
191        }
192    }
193}
194
195impl fmt::Display for TokenDistributionType {
196    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
197        match self {
198            TokenDistributionType::PreProgrammed => write!(f, "PreProgrammed"),
199            TokenDistributionType::Perpetual => write!(f, "Perpetual"),
200        }
201    }
202}
203
204#[derive(
205    Serialize,
206    Deserialize,
207    Decode,
208    Encode,
209    PlatformSerialize,
210    PlatformDeserializeTrusted,
211    PlatformDeserializeUntrusted,
212    Debug,
213    Clone,
214    PartialEq,
215    Eq,
216    DecodeUntrusted,
217)]
218#[platform_serialize(unversioned)]
219pub struct TokenDistributionKey {
220    pub token_id: Identifier,
221    pub recipient: TokenDistributionRecipient,
222    pub distribution_type: TokenDistributionType,
223}
224
225// --- canonical conversion trait impls (unification pass 1) ---
226#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
227impl crate::serialization::JsonConvertible for TokenDistributionTypeWithResolvedRecipient {}
228
229#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
230impl crate::serialization::ValueConvertible for TokenDistributionTypeWithResolvedRecipient {}
231
232#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
233impl crate::serialization::JsonConvertible for TokenDistributionInfo {}
234
235#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
236impl crate::serialization::ValueConvertible for TokenDistributionInfo {}
237
238#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
239impl crate::serialization::JsonConvertible for TokenDistributionType {}
240
241#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
242impl crate::serialization::ValueConvertible for TokenDistributionType {}
243
244#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
245impl crate::serialization::JsonConvertible for TokenDistributionKey {}
246
247#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
248impl crate::serialization::ValueConvertible for TokenDistributionKey {}
249
250#[cfg(all(
251    test,
252    feature = "json-conversion",
253    feature = "value-conversion",
254    feature = "serde-conversion"
255))]
256mod json_convertible_tests_token_distribution_type_and_key {
257    use super::*;
258    use crate::serialization::{JsonConvertible, ValueConvertible};
259    use platform_value::{platform_value, Value};
260    use serde_json::json;
261
262    #[test]
263    fn token_distribution_type_round_trips_all_variants() {
264        // Unit-only enum: serde default emits bare PascalCase strings on both
265        // wire formats.
266        let cases = [
267            (TokenDistributionType::PreProgrammed, "PreProgrammed"),
268            (TokenDistributionType::Perpetual, "Perpetual"),
269        ];
270        for (original, expected) in cases {
271            let json_v = original.to_json().expect("to_json");
272            assert_eq!(json_v, json!(expected));
273            assert_eq!(
274                TokenDistributionType::from_json(json_v).expect("from_json"),
275                original
276            );
277            let value = original.to_object().expect("to_object");
278            assert_eq!(value, platform_value!(expected));
279            assert_eq!(
280                TokenDistributionType::from_object(value).expect("from_object"),
281                original
282            );
283        }
284    }
285
286    fn key_fixture() -> TokenDistributionKey {
287        TokenDistributionKey {
288            token_id: Identifier::new([0x42; 32]),
289            recipient: TokenDistributionRecipient::EvonodesByParticipation,
290            distribution_type: TokenDistributionType::Perpetual,
291        }
292    }
293
294    #[test]
295    fn token_distribution_key_json_round_trip_with_full_wire_shape() {
296        let original = key_fixture();
297        let json = original.to_json().expect("to_json");
298        // `recipient` uses TokenDistributionRecipient's custom internally-tagged
299        // shape; `token_id` renders as base58. Field names are snake_case (no
300        // rename_all on this struct — internal key type, not user-authored JSON).
301        assert_eq!(
302            json,
303            json!({
304                "token_id": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf",
305                "recipient": {"$type": "evonodesByParticipation"},
306                "distribution_type": "Perpetual",
307            })
308        );
309        let recovered = TokenDistributionKey::from_json(json).expect("from_json");
310        assert_eq!(original, recovered);
311    }
312
313    #[test]
314    fn token_distribution_key_value_round_trip_with_full_wire_shape() {
315        let original = key_fixture();
316        let value = original.to_object().expect("to_object");
317        let expected = Value::Map(vec![
318            (
319                Value::Text("token_id".to_string()),
320                Value::Identifier([0x42; 32]),
321            ),
322            (
323                Value::Text("recipient".to_string()),
324                Value::Map(vec![(
325                    Value::Text("$type".to_string()),
326                    Value::Text("evonodesByParticipation".to_string()),
327                )]),
328            ),
329            (
330                Value::Text("distribution_type".to_string()),
331                Value::Text("Perpetual".to_string()),
332            ),
333        ]);
334        assert_eq!(value, expected);
335        let recovered = TokenDistributionKey::from_object(value).expect("from_object");
336        assert_eq!(original, recovered);
337    }
338}
339
340#[cfg(all(
341    test,
342    feature = "json-conversion",
343    feature = "value-conversion",
344    feature = "serde-conversion"
345))]
346mod json_convertible_tests_token_distribution_info {
347    use super::*;
348    use platform_value::{Identifier, Value};
349    use serde_json::json;
350
351    /// Non-default `PreProgrammed` variant with distinct timestamp + identifier
352    /// so the wire-shape assertion catches a silent variant flip or inner-zero
353    /// on round-trip.
354    fn fixture() -> TokenDistributionInfo {
355        TokenDistributionInfo::PreProgrammed(1_700_000_000_000, Identifier::new([0x42; 32]))
356    }
357
358    #[test]
359    fn json_round_trip_with_full_wire_shape() {
360        use crate::serialization::JsonConvertible;
361        let original = fixture();
362        let json = original.to_json().expect("to_json");
363        // Internally tagged with named fields:
364        // `{ "$type":"preProgrammed", "timestamp":<ts>, "identity":<id> }`.
365        // `TimestampMillis` is `u64`; JSON erases the size — see the value-
366        // path assertion which uses `Value::U64` to lock it in.
367        // `Identifier` is rendered as the base58-encoded string in JSON.
368        assert_eq!(
369            json,
370            json!({
371                "$type": "preProgrammed",
372                "timestamp": 1_700_000_000_000u64,
373                "identity": "5TeWSsjg2gbxCyWVniXeCmwM7UtHTCK7svzJr5xYJzHf",
374            })
375        );
376        let recovered = TokenDistributionInfo::from_json(json).expect("from_json");
377        assert_eq!(original, recovered);
378    }
379
380    #[test]
381    fn value_round_trip_with_full_wire_shape() {
382        use crate::serialization::ValueConvertible;
383        let original = fixture();
384        let value = original.to_object().expect("to_object");
385        // Internally tagged with named fields. `Identifier`'s Serialize emits
386        // the typed `Value::Identifier` variant (NOT `Value::Bytes32`), which
387        // survives serde's internal-tag Content buffer. Built by hand so the
388        // typed-bytes variant is preserved exactly.
389        let expected = Value::Map(vec![
390            (
391                Value::Text("$type".to_string()),
392                Value::Text("preProgrammed".to_string()),
393            ),
394            (
395                Value::Text("timestamp".to_string()),
396                Value::U64(1_700_000_000_000),
397            ),
398            (
399                Value::Text("identity".to_string()),
400                Value::Identifier([0x42; 32]),
401            ),
402        ]);
403        assert_eq!(value, expected);
404        let recovered = TokenDistributionInfo::from_object(value).expect("from_object");
405        assert_eq!(original, recovered);
406    }
407
408    #[test]
409    fn json_round_trip_perpetual_variant() {
410        use crate::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionResolvedRecipient;
411        use crate::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment;
412        use crate::serialization::JsonConvertible;
413        // The Perpetual variant (moment + resolved recipient) complements the
414        // PreProgrammed wire-shape test above; pin its `$type` discriminator and
415        // full round-trip so a silent variant flip is caught.
416        let original = TokenDistributionInfo::Perpetual(
417            RewardDistributionMoment::BlockBasedMoment(500),
418            TokenDistributionResolvedRecipient::Identity(Identifier::new([0x77; 32])),
419        );
420        let json = original.to_json().expect("to_json");
421        assert_eq!(json["$type"], json!("perpetual"));
422        let recovered = TokenDistributionInfo::from_json(json).expect("from_json");
423        assert_eq!(original, recovered);
424    }
425}