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