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