Skip to main content

dpp/tokens/
token_pricing_schedule.rs

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