tokenomics_simulator/
token.rs

1//! # Token module
2//!
3//! This module contains the token related structs and methods, such as air drops, unlock events, and processing unlocks.
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11/// Token.
12#[derive(Debug, Clone, PartialEq)]
13#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
14pub struct Token {
15    /// ID for the token.
16    pub id: Uuid,
17
18    /// Name of the token.
19    /// The name is a human-readable identifier for the token.
20    pub name: String,
21
22    /// Symbol of the token.
23    /// The symbol is a short identifier for the token, usually 3-4 characters long.
24    pub symbol: String,
25
26    /// Total supply of the token.
27    /// The total supply is the maximum number of tokens that can ever exist.
28    #[cfg_attr(
29        feature = "serde",
30        serde(with = "rust_decimal::serde::arbitrary_precision")
31    )]
32    pub total_supply: Decimal,
33
34    /// Current supply of the token.
35    /// The current supply is the number of tokens that have been minted or airdropped.
36    #[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::float"))]
37    pub current_supply: Decimal,
38
39    /// Initial supply of the token, in percentage of total supply.
40    /// The initial supply is the number of tokens that are minted at the start of the simulation.
41    #[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::float"))]
42    pub initial_supply_percentage: Decimal,
43
44    /// Annual percentage increase in supply, if supply is inflationary.
45    /// The inflation rate is the percentage by which the total supply increases each year.
46    #[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::float_option"))]
47    pub inflation_rate: Option<Decimal>,
48
49    /// Percentage of tokens burned during each transaction, if deflationary.
50    /// The burn rate is the percentage of tokens that are destroyed during each transaction.
51    #[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::float_option"))]
52    pub burn_rate: Option<Decimal>,
53
54    /// Initial price of the token in simulation.
55    /// The initial price is the price of the token at the start of the simulation.
56    #[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::float"))]
57    pub initial_price: Decimal,
58
59    /// Airdrop amount of the token, in percentage of total supply.
60    /// The airdrop percentage is the percentage of the total supply that is airdropped at the start of the simulation.
61    #[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::float_option"))]
62    pub airdrop_percentage: Option<Decimal>,
63
64    /// Unlock schedule.
65    /// The unlock schedule is a list of unlock events, each with a date and amount of tokens to unlock.
66    pub unlock_schedule: Option<Vec<UnlockEvent>>,
67}
68
69/// Unlock event.
70/// An unlock event is a scheduled event that unlocks a certain amount of tokens at a certain date.
71#[derive(Debug, Clone, PartialEq)]
72#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
73pub struct UnlockEvent {
74    /// Date and time of the unlock event.
75    pub date: DateTime<Utc>,
76
77    /// Amount of tokens to unlock.
78    #[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::float"))]
79    pub amount: Decimal,
80}
81
82impl Token {
83    /// Perform an airdrop.
84    ///
85    /// # Arguments
86    ///
87    /// * `percentage` - The percentage of the total supply to airdrop.
88    ///
89    /// # Returns
90    ///
91    /// The amount of tokens airdropped.
92    pub fn airdrop(&mut self, percentage: Decimal) -> Decimal {
93        #[cfg(feature = "log")]
94        log::debug!(
95            "Airdropping {}% of total supply for token {}",
96            percentage,
97            self.name
98        );
99
100        let airdrop_amount = (self.total_supply * percentage / Decimal::new(100, 0)).round();
101        let remaining_supply = self.total_supply - self.current_supply;
102        let final_airdrop_amount = if airdrop_amount > remaining_supply {
103            remaining_supply
104        } else {
105            airdrop_amount
106        };
107
108        self.current_supply += final_airdrop_amount;
109
110        final_airdrop_amount
111    }
112
113    /// Add an unlock event to the schedule.
114    /// The unlock event will unlock a certain amount of tokens at a certain date.
115    ///
116    /// # Arguments
117    ///
118    /// * `date` - The date and time of the unlock event.
119    /// * `amount` - The amount of tokens to unlock.
120    pub fn add_unlock_event(&mut self, date: DateTime<Utc>, amount: Decimal) {
121        #[cfg(feature = "log")]
122        log::debug!(
123            "Adding unlock event for token {} on {} for {} tokens",
124            self.name,
125            date,
126            amount
127        );
128
129        let event = UnlockEvent { date, amount };
130
131        if let Some(schedule) = &mut self.unlock_schedule {
132            schedule.push(event);
133        } else {
134            self.unlock_schedule = Some(vec![event]);
135        }
136    }
137
138    /// Process unlock events up to the current date.
139    /// Unlocks tokens and removes events that have already occurred.
140    ///
141    /// # Arguments
142    ///
143    /// * `current_date` - The current date and time.
144    pub fn process_unlocks(&mut self, current_date: DateTime<Utc>) {
145        if let Some(schedule) = &mut self.unlock_schedule {
146            #[cfg(feature = "log")]
147            log::debug!("Processing unlock events for token {}", self.name);
148
149            schedule.retain(|event| {
150                if event.date <= current_date {
151                    self.current_supply += event.amount;
152                    false
153                } else {
154                    true
155                }
156            });
157        }
158    }
159
160    /// Calculate the initial supply based on the initial supply percentage.
161    /// The initial supply is the number of tokens that are minted at the start of the simulation.
162    ///
163    /// # Returns
164    ///
165    /// Initial supply of the token.
166    pub fn initial_supply(&self) -> Decimal {
167        (self.total_supply * self.initial_supply_percentage / Decimal::new(100, 0)).round()
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use crate::TokenBuilder;
174
175    use super::*;
176
177    #[test]
178    fn test_token_airdrop() {
179        let mut token = TokenBuilder::new()
180            .name("Test Token".to_string())
181            .total_supply(1_000_000)
182            .build()
183            .unwrap();
184        let final_amount = Decimal::new(100000, 0);
185
186        let airdrop_amount = token.airdrop(Decimal::new(10, 0));
187
188        assert_eq!(airdrop_amount, final_amount);
189        assert_eq!(token.current_supply, final_amount);
190
191        let airdrop_amount = token.airdrop(Decimal::new(100, 0));
192
193        assert_eq!(airdrop_amount, Decimal::new(900000, 0));
194        assert_eq!(token.current_supply, Decimal::new(1_000_000, 0));
195    }
196
197    #[test]
198    fn test_add_unlock_event() {
199        let mut token = TokenBuilder::new()
200            .name("Test Token".to_string())
201            .total_supply(1_000_000)
202            .build()
203            .unwrap();
204        let date = Utc::now();
205        let amount = Decimal::new(100000, 0);
206
207        token.add_unlock_event(date, amount);
208        token.add_unlock_event(date, amount);
209
210        assert_eq!(token.unlock_schedule.unwrap().len(), 2);
211    }
212
213    #[test]
214    fn test_process_unlock() {
215        let mut token = TokenBuilder::new()
216            .name("Test Token".to_string())
217            .total_supply(1_000_000)
218            .build()
219            .unwrap();
220        let date = Utc::now();
221        let amount = Decimal::new(100000, 0);
222        token.add_unlock_event(date, amount);
223
224        let current_date = date + chrono::Duration::days(1);
225        token.process_unlocks(current_date);
226
227        assert_eq!(token.current_supply, amount);
228        assert!(token.unlock_schedule.unwrap().is_empty());
229    }
230}