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