1use std::cmp;
2
3use crate::error::{MathError, MathResult};
4use crate::spend_limits::get_remaining_timeframe_limit;
5
6pub const USDC_BASE_UNITS_PER_CENT: u64 = 10_000;
9
10pub fn usdc_base_units_to_cents(base_units: u64) -> MathResult<u64> {
14 base_units
15 .checked_div(USDC_BASE_UNITS_PER_CENT)
16 .ok_or(MathError::Overflow)
17}
18
19pub fn calculate_spend_limit_cents(
25 vault: &pyra_types::Vault,
26 max_transaction_limit_cents: u64,
27 now_unix: u64,
28) -> MathResult<u64> {
29 let timeframe_base_units = get_remaining_timeframe_limit(vault, now_unix);
30 let transaction_limit_cents = usdc_base_units_to_cents(vault.spend_limit_per_transaction)?;
31 let timeframe_remaining_cents = usdc_base_units_to_cents(timeframe_base_units)?;
32
33 let spend_limit_no_cap = cmp::min(transaction_limit_cents, timeframe_remaining_cents);
34 Ok(cmp::min(spend_limit_no_cap, max_transaction_limit_cents))
35}
36
37#[cfg(test)]
38#[allow(
39 clippy::unwrap_used,
40 clippy::expect_used,
41 clippy::panic,
42 clippy::arithmetic_side_effects
43)]
44mod tests {
45 use super::*;
46 use pyra_types::Vault;
47
48 fn make_vault(
49 spend_limit_per_transaction: u64,
50 spend_limit_per_timeframe: u64,
51 remaining_spend_limit_per_timeframe: u64,
52 next_timeframe_reset_timestamp: u64,
53 timeframe_in_seconds: u64,
54 ) -> Vault {
55 Vault {
56 owner: vec![0; 32],
57 bump: 0,
58 spend_limit_per_transaction,
59 spend_limit_per_timeframe,
60 remaining_spend_limit_per_timeframe,
61 next_timeframe_reset_timestamp,
62 timeframe_in_seconds,
63 }
64 }
65
66 #[test]
69 fn cents_basic() {
70 assert_eq!(usdc_base_units_to_cents(1_000_000).unwrap(), 100); }
72
73 #[test]
74 fn cents_zero() {
75 assert_eq!(usdc_base_units_to_cents(0).unwrap(), 0);
76 }
77
78 #[test]
79 fn cents_sub_cent_truncates() {
80 assert_eq!(usdc_base_units_to_cents(9_999).unwrap(), 0); assert_eq!(usdc_base_units_to_cents(10_000).unwrap(), 1); }
83
84 const NOW: u64 = 1_700_000_000;
87
88 #[test]
89 fn spend_limit_basic() {
90 let vault = make_vault(
91 10_000_000, 50_000_000, 30_000_000, NOW + 3600, 86_400,
96 );
97 let limit = calculate_spend_limit_cents(&vault, 10_000, NOW).unwrap();
98 assert_eq!(limit, 1000);
99 }
100
101 #[test]
102 fn spend_limit_expired_timeframe_uses_full() {
103 let vault = make_vault(
104 100_000_000, 50_000_000, 10_000_000, NOW - 100, 86_400,
109 );
110 let limit = calculate_spend_limit_cents(&vault, 100_000, NOW).unwrap();
111 assert_eq!(limit, 5000);
112 }
113
114 #[test]
115 fn spend_limit_capped_by_max() {
116 let vault = make_vault(
117 1_000_000_000, 1_000_000_000, 1_000_000_000, NOW + 3600,
121 86_400,
122 );
123 let limit = calculate_spend_limit_cents(&vault, 500, NOW).unwrap();
124 assert_eq!(limit, 500);
125 }
126
127 #[test]
128 fn spend_limit_zero_timeframe() {
129 let vault = make_vault(10_000_000, 50_000_000, 30_000_000, NOW + 3600, 0);
130 let limit = calculate_spend_limit_cents(&vault, 10_000, NOW).unwrap();
131 assert_eq!(limit, 0);
132 }
133}
134
135#[cfg(test)]
136#[allow(
137 clippy::unwrap_used,
138 clippy::expect_used,
139 clippy::panic,
140 clippy::arithmetic_side_effects
141)]
142mod proptests {
143 use super::*;
144 use proptest::prelude::*;
145
146 proptest! {
147 #[test]
148 fn usdc_cents_never_exceeds_base_units(base_units in 0u64..=u64::MAX) {
149 let cents = usdc_base_units_to_cents(base_units).unwrap();
150 prop_assert!(cents <= base_units, "cents {} > base_units {}", cents, base_units);
151 }
152 }
153}