tally_sdk/utils.rs
1//! General utility functions for Solana operations and data formatting
2//!
3//! This module provides commonly used utility functions for working with
4//! Solana data types, currency conversions, time formatting, and other
5//! helper functions used across the Tally ecosystem.
6
7#![forbid(unsafe_code)]
8
9use anchor_client::solana_sdk::{pubkey::Pubkey, sysvar};
10use std::str::FromStr;
11// Note: system_program is deprecated but still used for compatibility
12#[allow(deprecated)]
13use anchor_lang::system_program;
14
15/// Convert micro-lamports to USDC decimal amount
16///
17/// USDC uses 6 decimal places, so 1 USDC = 1,000,000 micro-lamports.
18///
19/// # Arguments
20/// * `micro_lamports` - Amount in micro-lamports (6 decimal places)
21///
22/// # Returns
23/// USDC amount as f64
24///
25/// # Examples
26/// ```
27/// use tally_sdk::utils::micro_lamports_to_usdc;
28///
29/// assert_eq!(micro_lamports_to_usdc(1_000_000), 1.0);
30/// assert_eq!(micro_lamports_to_usdc(5_500_000), 5.5);
31/// ```
32#[must_use]
33pub fn micro_lamports_to_usdc(micro_lamports: u64) -> f64 {
34 // Note: This conversion may lose precision for very large values
35 // but is acceptable for USDC amounts (max supply ~80B = 80_000_000_000_000_000 micro-lamports)
36 // which is well within f64's 52-bit mantissa precision
37 #[allow(clippy::cast_precision_loss)]
38 {
39 micro_lamports as f64 / 1_000_000.0
40 }
41}
42
43/// Convert USDC decimal amount to micro-lamports
44///
45/// USDC uses 6 decimal places, so 1 USDC = 1,000,000 micro-lamports.
46///
47/// # Arguments
48/// * `usdc_amount` - USDC amount as f64
49///
50/// # Returns
51/// Amount in micro-lamports
52///
53/// # Examples
54/// ```
55/// use tally_sdk::utils::usdc_to_micro_lamports;
56///
57/// assert_eq!(usdc_to_micro_lamports(1.0), 1_000_000);
58/// assert_eq!(usdc_to_micro_lamports(5.5), 5_500_000);
59/// ```
60#[must_use]
61pub fn usdc_to_micro_lamports(usdc_amount: f64) -> u64 {
62 // Ensure non-negative values and safe conversion
63 let result = usdc_amount.max(0.0) * 1_000_000.0;
64 // Round to avoid precision issues and ensure we don't exceed u64::MAX
65 // Safe cast: result is clamped to non-negative values and u64::MAX range
66 #[allow(
67 clippy::cast_possible_truncation,
68 clippy::cast_sign_loss,
69 clippy::cast_precision_loss
70 )]
71 {
72 result.round().min(18_446_744_073_709_551_615.0) as u64
73 }
74}
75
76/// Check if a pubkey is a valid Solana address
77///
78/// # Arguments
79/// * `address` - Base58 encoded address string
80///
81/// # Returns
82/// True if valid, false otherwise
83///
84/// # Examples
85/// ```
86/// use tally_sdk::utils::is_valid_pubkey;
87///
88/// assert!(is_valid_pubkey("11111111111111111111111111111112")); // System program
89/// assert!(!is_valid_pubkey("invalid_address"));
90/// ```
91#[must_use]
92pub fn is_valid_pubkey(address: &str) -> bool {
93 Pubkey::from_str(address).is_ok()
94}
95
96/// Get system program addresses for validation
97///
98/// Returns a list of well-known system program addresses that are
99/// commonly used in Solana operations.
100///
101/// # Returns
102/// Vector of system program pubkeys
103#[must_use]
104pub fn system_programs() -> Vec<Pubkey> {
105 vec![
106 system_program::ID,
107 spl_token::id(),
108 spl_associated_token_account::id(),
109 sysvar::rent::id(),
110 sysvar::clock::id(),
111 ]
112}
113
114/// Format duration in seconds to human readable string
115///
116/// # Arguments
117/// * `seconds` - Duration in seconds
118///
119/// # Returns
120/// Human readable duration string
121///
122/// # Examples
123/// ```
124/// use tally_sdk::utils::format_duration;
125///
126/// assert_eq!(format_duration(30), "30s");
127/// assert_eq!(format_duration(90), "1m 30s");
128/// assert_eq!(format_duration(3661), "1h 1m 1s");
129/// assert_eq!(format_duration(90061), "1d 1h 1m 1s");
130/// ```
131#[must_use]
132pub fn format_duration(seconds: u64) -> String {
133 let days = seconds / 86400;
134 let hours = (seconds % 86400) / 3600;
135 let minutes = (seconds % 3600) / 60;
136 let secs = seconds % 60;
137
138 if days > 0 {
139 format!("{days}d {hours}h {minutes}m {secs}s")
140 } else if hours > 0 {
141 format!("{hours}h {minutes}m {secs}s")
142 } else if minutes > 0 {
143 format!("{minutes}m {secs}s")
144 } else {
145 format!("{secs}s")
146 }
147}
148
149/// Calculate subscription renewal timestamp
150///
151/// # Arguments
152/// * `start_timestamp` - Subscription start time (Unix timestamp)
153/// * `period_seconds` - Subscription period in seconds
154/// * `periods_elapsed` - Number of periods that have elapsed
155///
156/// # Returns
157/// Next renewal timestamp
158///
159/// # Examples
160/// ```
161/// use tally_sdk::utils::calculate_next_renewal;
162///
163/// // Starting at Unix timestamp 1000, with 30-day periods (2592000 seconds)
164/// // After 0 periods elapsed, next renewal should be at 1000 + 2592000 = 2593000
165/// let next = calculate_next_renewal(1000, 2592000, 0);
166/// assert_eq!(next, 2593000);
167/// ```
168#[must_use]
169pub fn calculate_next_renewal(
170 start_timestamp: i64,
171 period_seconds: u64,
172 periods_elapsed: u32,
173) -> i64 {
174 start_timestamp.saturating_add(
175 period_seconds
176 .saturating_mul(u64::from(periods_elapsed.saturating_add(1)))
177 .try_into()
178 .unwrap_or(i64::MAX),
179 )
180}
181
182/// Check if subscription is due for renewal
183///
184/// A subscription is due for renewal if the current time is past the renewal
185/// time but still within the grace period.
186///
187/// # Arguments
188/// * `next_renewal_timestamp` - Next renewal time (Unix timestamp)
189/// * `grace_period_seconds` - Grace period in seconds
190///
191/// # Returns
192/// True if due for renewal (including grace period)
193#[must_use]
194pub fn is_renewal_due(next_renewal_timestamp: i64, grace_period_seconds: u64) -> bool {
195 let current_timestamp = chrono::Utc::now().timestamp();
196 let grace_end =
197 next_renewal_timestamp.saturating_add(grace_period_seconds.try_into().unwrap_or(i64::MAX));
198 current_timestamp >= next_renewal_timestamp && current_timestamp <= grace_end
199}
200
201/// Check if subscription is overdue (past grace period)
202///
203/// A subscription is overdue if the current time is past both the renewal
204/// time and the grace period.
205///
206/// # Arguments
207/// * `next_renewal_timestamp` - Next renewal time (Unix timestamp)
208/// * `grace_period_seconds` - Grace period in seconds
209///
210/// # Returns
211/// True if overdue (past grace period)
212#[must_use]
213pub fn is_subscription_overdue(next_renewal_timestamp: i64, grace_period_seconds: u64) -> bool {
214 let current_timestamp = chrono::Utc::now().timestamp();
215 let grace_end =
216 next_renewal_timestamp.saturating_add(grace_period_seconds.try_into().unwrap_or(i64::MAX));
217 current_timestamp > grace_end
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_micro_lamports_to_usdc() {
226 const EPSILON: f64 = 1e-10;
227 assert!((micro_lamports_to_usdc(1_000_000) - 1.0).abs() < EPSILON);
228 assert!((micro_lamports_to_usdc(5_500_000) - 5.5).abs() < EPSILON);
229 assert!((micro_lamports_to_usdc(0) - 0.0).abs() < EPSILON);
230 assert!((micro_lamports_to_usdc(500_000) - 0.5).abs() < EPSILON);
231 }
232
233 #[test]
234 fn test_usdc_to_micro_lamports() {
235 assert_eq!(usdc_to_micro_lamports(1.0), 1_000_000);
236 assert_eq!(usdc_to_micro_lamports(5.5), 5_500_000);
237 assert_eq!(usdc_to_micro_lamports(0.0), 0);
238 assert_eq!(usdc_to_micro_lamports(0.5), 500_000);
239
240 // Test negative values are clamped to 0
241 assert_eq!(usdc_to_micro_lamports(-1.0), 0);
242 }
243
244 #[test]
245 fn test_is_valid_pubkey() {
246 // Valid system program address
247 assert!(is_valid_pubkey("11111111111111111111111111111112"));
248
249 // Invalid addresses
250 assert!(!is_valid_pubkey("invalid_address"));
251 assert!(!is_valid_pubkey(""));
252 assert!(!is_valid_pubkey("too_short"));
253 }
254
255 #[test]
256 fn test_system_programs() {
257 let programs = system_programs();
258 assert!(!programs.is_empty());
259 assert!(programs.contains(&system_program::ID));
260 assert!(programs.contains(&spl_token::id()));
261 }
262
263 #[test]
264 fn test_format_duration() {
265 assert_eq!(format_duration(30), "30s");
266 assert_eq!(format_duration(90), "1m 30s");
267 assert_eq!(format_duration(3661), "1h 1m 1s");
268 assert_eq!(format_duration(90061), "1d 1h 1m 1s");
269
270 // Edge cases
271 assert_eq!(format_duration(0), "0s");
272 assert_eq!(format_duration(60), "1m 0s");
273 assert_eq!(format_duration(3600), "1h 0m 0s");
274 assert_eq!(format_duration(86400), "1d 0h 0m 0s");
275 }
276
277 #[test]
278 fn test_calculate_next_renewal() {
279 let start = 1000_i64;
280 let period = 2_592_000_u64; // 30 days in seconds
281
282 // First renewal (0 periods elapsed)
283 assert_eq!(
284 calculate_next_renewal(start, period, 0),
285 start + i64::try_from(period).unwrap()
286 );
287
288 // Second renewal (1 period elapsed)
289 assert_eq!(
290 calculate_next_renewal(start, period, 1),
291 start + i64::try_from(2 * period).unwrap()
292 );
293 }
294
295 #[test]
296 fn test_is_renewal_due() {
297 let now = chrono::Utc::now().timestamp();
298 let grace_period = 86400; // 1 day
299
300 // Past due but within grace period
301 let past_renewal = now - 3600; // 1 hour ago
302 assert!(is_renewal_due(past_renewal, grace_period));
303
304 // Future renewal
305 let future_renewal = now + 3600; // 1 hour from now
306 assert!(!is_renewal_due(future_renewal, grace_period));
307 }
308
309 #[test]
310 fn test_is_subscription_overdue() {
311 let now = chrono::Utc::now().timestamp();
312 let grace_period = 86400; // 1 day
313
314 // Way past due (beyond grace period)
315 let way_past_renewal = now - (2 * 86400); // 2 days ago
316 assert!(is_subscription_overdue(way_past_renewal, grace_period));
317
318 // Within grace period
319 let recent_past_renewal = now - 3600; // 1 hour ago
320 assert!(!is_subscription_overdue(recent_past_renewal, grace_period));
321 }
322}