1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3use {
4 crate::{
5 extension::{Extension, ExtensionType},
6 trim_ui_amount_string,
7 },
8 bytemuck::{Pod, Zeroable},
9 solana_program_error::ProgramError,
10 spl_pod::{optional_keys::OptionalNonZeroPubkey, primitives::PodI64},
11};
12
13pub mod instruction;
15
16pub type UnixTimestamp = PodI64;
18
19#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21#[cfg_attr(feature = "serde", serde(from = "f64", into = "f64"))]
22#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
23#[repr(transparent)]
24pub struct PodF64(pub [u8; 8]);
25impl PodF64 {
26 fn from_primitive(n: f64) -> Self {
27 Self(n.to_le_bytes())
28 }
29}
30impl From<f64> for PodF64 {
31 fn from(n: f64) -> Self {
32 Self::from_primitive(n)
33 }
34}
35impl From<PodF64> for f64 {
36 fn from(pod: PodF64) -> Self {
37 Self::from_le_bytes(pod.0)
38 }
39}
40
41#[repr(C)]
43#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
44#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
45#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
46pub struct ScaledUiAmountConfig {
47 pub authority: OptionalNonZeroPubkey,
49 pub multiplier: PodF64,
51 pub new_multiplier_effective_timestamp: UnixTimestamp,
53 pub new_multiplier: PodF64,
55}
56impl ScaledUiAmountConfig {
57 fn current_multiplier(&self, unix_timestamp: i64) -> f64 {
58 if unix_timestamp >= self.new_multiplier_effective_timestamp.into() {
59 self.new_multiplier.into()
60 } else {
61 self.multiplier.into()
62 }
63 }
64
65 fn total_multiplier(&self, decimals: u8, unix_timestamp: i64) -> f64 {
66 self.current_multiplier(unix_timestamp) / 10_f64.powi(decimals as i32)
67 }
68
69 pub fn amount_to_ui_amount(
75 &self,
76 amount: u64,
77 decimals: u8,
78 unix_timestamp: i64,
79 ) -> Option<String> {
80 let scaled_amount = (amount as f64) * self.current_multiplier(unix_timestamp);
81 let truncated_amount = scaled_amount.trunc() / 10_f64.powi(decimals as i32);
82 let ui_amount = format!("{truncated_amount:.*}", decimals as usize);
83 Some(trim_ui_amount_string(ui_amount, decimals))
84 }
85
86 pub fn try_ui_amount_into_amount(
92 &self,
93 ui_amount: &str,
94 decimals: u8,
95 unix_timestamp: i64,
96 ) -> Result<u64, ProgramError> {
97 let scaled_amount = ui_amount
98 .parse::<f64>()
99 .map_err(|_| ProgramError::InvalidArgument)?;
100 let amount = scaled_amount / self.total_multiplier(decimals, unix_timestamp);
101 if amount > (u64::MAX as f64) || amount < (u64::MIN as f64) || amount.is_nan() {
102 Err(ProgramError::InvalidArgument)
103 } else {
104 Ok(amount.trunc() as u64)
107 }
108 }
109}
110impl Extension for ScaledUiAmountConfig {
111 const TYPE: ExtensionType = ExtensionType::ScaledUiAmount;
112}
113
114#[cfg(test)]
115mod tests {
116 use {super::*, proptest::prelude::*};
117
118 const TEST_DECIMALS: u8 = 2;
119
120 #[test]
121 fn multiplier_choice() {
122 let multiplier = 5.0;
123 let new_multiplier = 10.0;
124 let new_multiplier_effective_timestamp = 1;
125 let config = ScaledUiAmountConfig {
126 multiplier: PodF64::from(multiplier),
127 new_multiplier: PodF64::from(new_multiplier),
128 new_multiplier_effective_timestamp: UnixTimestamp::from(
129 new_multiplier_effective_timestamp,
130 ),
131 ..Default::default()
132 };
133 assert_eq!(
134 config.total_multiplier(0, new_multiplier_effective_timestamp),
135 new_multiplier
136 );
137 assert_eq!(
138 config.total_multiplier(0, new_multiplier_effective_timestamp - 1),
139 multiplier
140 );
141 assert_eq!(config.total_multiplier(0, 0), multiplier);
142 assert_eq!(config.total_multiplier(0, i64::MIN), multiplier);
143 assert_eq!(config.total_multiplier(0, i64::MAX), new_multiplier);
144 }
145
146 #[test]
147 fn specific_amount_to_ui_amount() {
148 let config = ScaledUiAmountConfig {
150 multiplier: PodF64::from(5.0),
151 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
152 ..Default::default()
153 };
154 let ui_amount = config.amount_to_ui_amount(1, 0, 0).unwrap();
155 assert_eq!(ui_amount, "5");
156 let ui_amount = config.amount_to_ui_amount(1, 1, 0).unwrap();
158 assert_eq!(ui_amount, "0.5");
159 let ui_amount = config.amount_to_ui_amount(1, 10, 0).unwrap();
161 assert_eq!(ui_amount, "0.0000000005");
162
163 let ui_amount = config.amount_to_ui_amount(10_000_000_000, 10, 0).unwrap();
165 assert_eq!(ui_amount, "5");
166
167 let config = ScaledUiAmountConfig {
169 multiplier: PodF64::from(f64::MAX),
170 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
171 ..Default::default()
172 };
173 let ui_amount = config.amount_to_ui_amount(u64::MAX, 0, 0).unwrap();
174 assert_eq!(ui_amount, "inf");
175
176 let config = ScaledUiAmountConfig {
178 multiplier: PodF64::from(0.99),
179 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
180 ..Default::default()
181 };
182 let ui_amount = config.amount_to_ui_amount(101, 2, 0).unwrap();
184 assert_eq!(ui_amount, "0.99");
185 }
186
187 #[test]
188 fn specific_ui_amount_to_amount() {
189 let config = ScaledUiAmountConfig {
191 multiplier: 5.0.into(),
192 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
193 ..Default::default()
194 };
195 let amount = config.try_ui_amount_into_amount("5.0", 0, 0).unwrap();
196 assert_eq!(1, amount);
197 let amount = config
199 .try_ui_amount_into_amount("0.500000000", 1, 0)
200 .unwrap();
201 assert_eq!(amount, 1);
202 let amount = config
204 .try_ui_amount_into_amount("0.00000000050000000000000000", 10, 0)
205 .unwrap();
206 assert_eq!(amount, 1);
207
208 let amount = config
210 .try_ui_amount_into_amount("5.0000000000000000", 10, 0)
211 .unwrap();
212 assert_eq!(amount, 10_000_000_000);
213
214 let config = ScaledUiAmountConfig {
216 multiplier: 5.0.into(),
217 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
218 ..Default::default()
219 };
220 let amount = config
221 .try_ui_amount_into_amount("92233720368547758075", 0, 0)
222 .unwrap();
223 assert_eq!(amount, u64::MAX);
224 let config = ScaledUiAmountConfig {
225 multiplier: f64::MAX.into(),
226 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
227 ..Default::default()
228 };
229 let amount = config
231 .try_ui_amount_into_amount("1.7976931348623157e308", 0, 0)
232 .unwrap();
233 assert_eq!(amount, 1);
234 let config = ScaledUiAmountConfig {
235 multiplier: 9.745314011399998e288.into(),
236 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
237 ..Default::default()
238 };
239 let amount = config
240 .try_ui_amount_into_amount("1.7976931348623157e308", 0, 0)
241 .unwrap();
242 assert_eq!(amount, u64::MAX);
243 let amount = config
245 .try_ui_amount_into_amount("1.7976931348623157E308", 0, 0)
246 .unwrap();
247 assert_eq!(amount, u64::MAX);
248
249 let config = ScaledUiAmountConfig {
251 multiplier: 1.0.into(),
252 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
253 ..Default::default()
254 };
255 assert_eq!(
256 u64::MAX,
257 config
258 .try_ui_amount_into_amount("18446744073709551616", 0, 0)
259 .unwrap() );
261
262 let config = ScaledUiAmountConfig {
264 multiplier: 0.1.into(),
265 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
266 ..Default::default()
267 };
268 assert_eq!(
269 Err(ProgramError::InvalidArgument),
270 config.try_ui_amount_into_amount("18446744073709551615", 0, 0) );
272
273 for fail_ui_amount in ["-0.0000000000000000000001", "inf", "-inf", "NaN"] {
274 assert_eq!(
275 Err(ProgramError::InvalidArgument),
276 config.try_ui_amount_into_amount(fail_ui_amount, 0, 0)
277 );
278 }
279
280 let config = ScaledUiAmountConfig {
282 multiplier: PodF64::from(0.99),
283 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
284 ..Default::default()
285 };
286 let amount = config.try_ui_amount_into_amount("0.99", 2, 0).unwrap();
290 assert_eq!(amount, 100);
291 }
292
293 #[test]
294 fn specific_amount_to_ui_amount_no_scale() {
295 let config = ScaledUiAmountConfig {
296 multiplier: 1.0.into(),
297 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
298 ..Default::default()
299 };
300 for (amount, expected) in [(23, "0.23"), (110, "1.1"), (4200, "42"), (0, "0")] {
301 let ui_amount = config
302 .amount_to_ui_amount(amount, TEST_DECIMALS, 0)
303 .unwrap();
304 assert_eq!(ui_amount, expected);
305 }
306 }
307
308 #[test]
309 fn specific_ui_amount_to_amount_no_scale() {
310 let config = ScaledUiAmountConfig {
311 multiplier: 1.0.into(),
312 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
313 ..Default::default()
314 };
315 for (ui_amount, expected) in [
316 ("0.23", 23),
317 ("0.20", 20),
318 ("0.2000", 20),
319 (".2", 20),
320 ("1.1", 110),
321 ("1.10", 110),
322 ("42", 4200),
323 ("42.", 4200),
324 ("0", 0),
325 ] {
326 let amount = config
327 .try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, 0)
328 .unwrap();
329 assert_eq!(expected, amount);
330 }
331
332 let amount = config
334 .try_ui_amount_into_amount("0.111", TEST_DECIMALS, 0)
335 .unwrap();
336 assert_eq!(11, amount);
337
338 for ui_amount in ["", ".", "0.t"] {
340 assert_eq!(
341 Err(ProgramError::InvalidArgument),
342 config.try_ui_amount_into_amount(ui_amount, TEST_DECIMALS, 0),
343 );
344 }
345 }
346
347 proptest! {
348 #[test]
349 fn amount_to_ui_amount(
350 scale in 0f64..=f64::MAX,
351 amount in 0..=u64::MAX,
352 decimals in 0u8..20u8,
353 ) {
354 let config = ScaledUiAmountConfig {
355 multiplier: scale.into(),
356 new_multiplier_effective_timestamp: UnixTimestamp::from(1),
357 ..Default::default()
358 };
359 let ui_amount = config.amount_to_ui_amount(amount, decimals, 0);
360 assert!(ui_amount.is_some());
361 }
362 }
363}