Skip to main content

dpp/tokens/
token_pricing_schedule.rs

1use crate::balances::credits::TokenAmount;
2use crate::errors::ProtocolError;
3use crate::fee::Credits;
4use bincode::{Decode, Encode};
5use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize};
6#[cfg(feature = "serde-conversion")]
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::fmt::{self, Display, Formatter};
10
11/// Defines the pricing schedule for tokens in terms of credits.
12///
13/// A pricing schedule can either be a single, flat price applied to all
14/// token amounts, or a tiered pricing model where specific amounts
15/// correspond to specific credit values.
16#[derive(
17    Debug,
18    Clone,
19    Encode,
20    Decode,
21    Eq,
22    PartialEq,
23    Ord,
24    PartialOrd,
25    PlatformSerialize,
26    PlatformDeserialize,
27)]
28#[cfg_attr(
29    feature = "serde-conversion",
30    derive(Serialize, Deserialize),
31    serde(into = "TokenPricingScheduleRepr", from = "TokenPricingScheduleRepr")
32)]
33pub enum TokenPricingSchedule {
34    /// A single flat price in credits for all token amounts.
35    ///
36    /// This variant is used when the pricing does not depend on
37    /// the number of tokens being purchased or processed.
38    SinglePrice(Credits),
39
40    /// A tiered pricing model where specific token amounts map to credit prices.
41    ///
42    /// This allows for more complex pricing structures, such as
43    /// volume discounts or progressive pricing. The map keys
44    /// represent token amount thresholds, and the values are the
45    /// corresponding credit prices.
46    /// If the first token amount is greater than 1 this means that the user can only
47    /// purchase that amount as a minimum at a time.
48    SetPrices(BTreeMap<TokenAmount, Credits>),
49}
50
51// Internal-`$type` serde shape. The tuple-variant outer enum can neither
52// auto-derive internal tagging nor annotate its variant-internal u64s, so this
53// struct-variant helper does both: `json_safe_u64` / `json_safe_u64_u64_map`
54// keep the `Credits` and `TokenAmount` values JS-safe (string above
55// `Number.MAX_SAFE_INTEGER`) in human-readable JSON, with no effect on `Value`
56// or the bincode consensus path (which round-trips the outer enum directly).
57#[cfg(feature = "serde-conversion")]
58#[derive(Serialize, Deserialize)]
59#[serde(tag = "$type", rename_all = "camelCase")]
60enum TokenPricingScheduleRepr {
61    SinglePrice {
62        #[cfg_attr(
63            feature = "json-conversion",
64            serde(with = "crate::serialization::json_safe_u64")
65        )]
66        price: Credits,
67    },
68    SetPrices {
69        #[cfg_attr(
70            feature = "json-conversion",
71            serde(with = "crate::serialization::json::safe_integer_map::json_safe_u64_u64_map")
72        )]
73        prices: BTreeMap<TokenAmount, Credits>,
74    },
75}
76
77#[cfg(feature = "serde-conversion")]
78impl From<TokenPricingSchedule> for TokenPricingScheduleRepr {
79    fn from(schedule: TokenPricingSchedule) -> Self {
80        match schedule {
81            TokenPricingSchedule::SinglePrice(price) => Self::SinglePrice { price },
82            TokenPricingSchedule::SetPrices(prices) => Self::SetPrices { prices },
83        }
84    }
85}
86
87#[cfg(feature = "serde-conversion")]
88impl From<TokenPricingScheduleRepr> for TokenPricingSchedule {
89    fn from(repr: TokenPricingScheduleRepr) -> Self {
90        match repr {
91            TokenPricingScheduleRepr::SinglePrice { price } => Self::SinglePrice(price),
92            TokenPricingScheduleRepr::SetPrices { prices } => Self::SetPrices(prices),
93        }
94    }
95}
96
97#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
98impl crate::serialization::JsonConvertible for TokenPricingSchedule {}
99
100#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
101impl crate::serialization::ValueConvertible for TokenPricingSchedule {}
102
103impl TokenPricingSchedule {
104    pub fn minimum_purchase_amount_and_price(&self) -> (TokenAmount, Credits) {
105        match self {
106            TokenPricingSchedule::SinglePrice(price) => (1, *price),
107            TokenPricingSchedule::SetPrices(prices) => prices
108                .first_key_value()
109                .map(|(amount, cost)| (*amount, *cost))
110                .unwrap_or_default(),
111        }
112    }
113}
114
115impl Display for TokenPricingSchedule {
116    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
117        match self {
118            TokenPricingSchedule::SinglePrice(credits) => {
119                write!(f, "SinglePrice: {}", credits)
120            }
121            TokenPricingSchedule::SetPrices(prices) => {
122                write!(f, "SetPrices: [")?;
123                for (i, (amount, credits)) in prices.iter().enumerate() {
124                    if i > 0 {
125                        write!(f, ", ")?;
126                    }
127                    write!(f, "{} => {}", amount, credits)?;
128                }
129                write!(f, "]")
130            }
131        }
132    }
133}
134
135#[cfg(all(
136    test,
137    feature = "json-conversion",
138    feature = "value-conversion",
139    feature = "serde-conversion"
140))]
141mod json_convertible_tests {
142    use super::*;
143    use platform_value::{platform_value, Value};
144    use serde_json::json;
145
146    // Internally `$type`-tagged: `SinglePrice(u64)` → `{"$type":"singlePrice",
147    // "price": <n>}`, `SetPrices(BTreeMap<u64, u64>)` → `{"$type":"setPrices",
148    // "prices": {<k>: <v>, ...}}`. `Credits`/`TokenAmount` u64s are JS-safe
149    // (number below 2^53, string above); JSON forces map keys to strings while
150    // platform_value preserves typed keys.
151
152    #[test]
153    fn json_round_trip_single_price() {
154        use crate::serialization::JsonConvertible;
155        let original = TokenPricingSchedule::SinglePrice(1234);
156        let json = original.to_json().expect("to_json");
157        assert_eq!(json, json!({ "$type": "singlePrice", "price": 1234 }));
158        let recovered = TokenPricingSchedule::from_json(json).expect("from_json");
159        assert_eq!(original, recovered);
160    }
161
162    #[test]
163    fn json_single_price_above_max_safe_integer_is_string() {
164        use crate::serialization::JsonConvertible;
165        // Above Number.MAX_SAFE_INTEGER (2^53): json_safe_u64 must stringify the
166        // Credits so JS consumers can't silently round it. (Raw u64 before the
167        // Repr fix; string after.)
168        let big: Credits = (1u64 << 53) + 1;
169        let original = TokenPricingSchedule::SinglePrice(big);
170        let json = original.to_json().expect("to_json");
171        assert_eq!(
172            json,
173            json!({ "$type": "singlePrice", "price": big.to_string() })
174        );
175        let recovered = TokenPricingSchedule::from_json(json).expect("from_json");
176        assert_eq!(original, recovered);
177    }
178
179    #[test]
180    fn json_round_trip_set_prices() {
181        use crate::serialization::JsonConvertible;
182        let mut prices = BTreeMap::new();
183        prices.insert(5u64, 50u64);
184        prices.insert(10u64, 80u64);
185        let original = TokenPricingSchedule::SetPrices(prices);
186        let json = original.to_json().expect("to_json");
187        // JSON object keys must be strings — `serde_json` stringifies the
188        // u64 amount keys.
189        assert_eq!(
190            json,
191            json!({ "$type": "setPrices", "prices": { "5": 50, "10": 80 } })
192        );
193        let recovered = TokenPricingSchedule::from_json(json).expect("from_json");
194        assert_eq!(original, recovered);
195    }
196
197    #[test]
198    fn value_round_trip_single_price() {
199        use crate::serialization::ValueConvertible;
200        let original = TokenPricingSchedule::SinglePrice(1234);
201        let value = original.to_object().expect("to_object");
202        // `Credits` is `u64` → `Value::U64` (non-HR: json_safe_u64 stays typed).
203        assert_eq!(
204            value,
205            platform_value!({ "$type": "singlePrice", "price": 1234u64 })
206        );
207        let recovered = TokenPricingSchedule::from_object(value).expect("from_object");
208        assert_eq!(original, recovered);
209    }
210
211    #[test]
212    fn value_round_trip_set_prices() {
213        use crate::serialization::ValueConvertible;
214        let mut prices = BTreeMap::new();
215        prices.insert(5u64, 50u64);
216        prices.insert(10u64, 80u64);
217        let original = TokenPricingSchedule::SetPrices(prices);
218        let value = original.to_object().expect("to_object");
219        // platform_value preserves typed map keys: `BTreeMap<u64, u64>` →
220        // map of `(Value::U64, Value::U64)` pairs. Serialized `$type` first.
221        assert_eq!(
222            value,
223            Value::Map(vec![
224                (
225                    Value::Text("$type".to_string()),
226                    Value::Text("setPrices".to_string()),
227                ),
228                (
229                    Value::Text("prices".to_string()),
230                    Value::Map(vec![
231                        (Value::U64(5), Value::U64(50)),
232                        (Value::U64(10), Value::U64(80)),
233                    ]),
234                ),
235            ])
236        );
237        let recovered = TokenPricingSchedule::from_object(value).expect("from_object");
238        assert_eq!(original, recovered);
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn single_price_minimum_purchase_amount_and_price() {
248        let schedule = TokenPricingSchedule::SinglePrice(500);
249        let (amount, price) = schedule.minimum_purchase_amount_and_price();
250        assert_eq!(amount, 1);
251        assert_eq!(price, 500);
252    }
253
254    #[test]
255    fn single_price_zero_credits() {
256        let schedule = TokenPricingSchedule::SinglePrice(0);
257        let (amount, price) = schedule.minimum_purchase_amount_and_price();
258        assert_eq!(amount, 1);
259        assert_eq!(price, 0);
260    }
261
262    #[test]
263    fn set_prices_minimum_purchase_amount_and_price_single_entry() {
264        let mut prices = BTreeMap::new();
265        prices.insert(10u64, 100u64);
266        let schedule = TokenPricingSchedule::SetPrices(prices);
267        let (amount, price) = schedule.minimum_purchase_amount_and_price();
268        assert_eq!(amount, 10);
269        assert_eq!(price, 100);
270    }
271
272    #[test]
273    fn set_prices_minimum_purchase_amount_and_price_multiple_entries() {
274        let mut prices = BTreeMap::new();
275        prices.insert(5u64, 50u64);
276        prices.insert(10u64, 80u64);
277        prices.insert(100u64, 500u64);
278        let schedule = TokenPricingSchedule::SetPrices(prices);
279        // BTreeMap orders by key, so the first entry is the minimum amount
280        let (amount, price) = schedule.minimum_purchase_amount_and_price();
281        assert_eq!(amount, 5);
282        assert_eq!(price, 50);
283    }
284
285    #[test]
286    fn set_prices_empty_map_returns_default() {
287        let prices = BTreeMap::new();
288        let schedule = TokenPricingSchedule::SetPrices(prices);
289        let (amount, price) = schedule.minimum_purchase_amount_and_price();
290        // unwrap_or_default returns (0, 0) for empty map
291        assert_eq!(amount, 0);
292        assert_eq!(price, 0);
293    }
294
295    #[test]
296    fn display_single_price() {
297        let schedule = TokenPricingSchedule::SinglePrice(1234);
298        assert_eq!(format!("{}", schedule), "SinglePrice: 1234");
299    }
300
301    #[test]
302    fn display_set_prices_empty() {
303        let schedule = TokenPricingSchedule::SetPrices(BTreeMap::new());
304        assert_eq!(format!("{}", schedule), "SetPrices: []");
305    }
306
307    #[test]
308    fn display_set_prices_single_entry() {
309        let mut prices = BTreeMap::new();
310        prices.insert(10u64, 100u64);
311        let schedule = TokenPricingSchedule::SetPrices(prices);
312        assert_eq!(format!("{}", schedule), "SetPrices: [10 => 100]");
313    }
314
315    #[test]
316    fn display_set_prices_multiple_entries() {
317        let mut prices = BTreeMap::new();
318        prices.insert(5u64, 50u64);
319        prices.insert(10u64, 80u64);
320        let schedule = TokenPricingSchedule::SetPrices(prices);
321        // BTreeMap iterates in sorted key order
322        assert_eq!(format!("{}", schedule), "SetPrices: [5 => 50, 10 => 80]");
323    }
324}