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