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