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::allow_attributes,
40 clippy::allow_attributes_without_reason,
41 clippy::unwrap_used,
42 clippy::expect_used,
43 clippy::panic,
44 clippy::arithmetic_side_effects
45)]
46mod tests {
47 use super::*;
48 use pyra_types::Vault;
49
50 fn make_vault(
51 spend_limit_per_transaction: u64,
52 spend_limit_per_timeframe: u64,
53 remaining_spend_limit_per_timeframe: u64,
54 next_timeframe_reset_timestamp: u64,
55 timeframe_in_seconds: u64,
56 ) -> Vault {
57 Vault {
58 owner: vec![0; 32],
59 bump: 0,
60 spend_limit_per_transaction,
61 spend_limit_per_timeframe,
62 remaining_spend_limit_per_timeframe,
63 next_timeframe_reset_timestamp,
64 timeframe_in_seconds,
65 }
66 }
67
68 #[test]
71 fn cents_basic() {
72 assert_eq!(usdc_base_units_to_cents(1_000_000).unwrap(), 100); }
74
75 #[test]
76 fn cents_zero() {
77 assert_eq!(usdc_base_units_to_cents(0).unwrap(), 0);
78 }
79
80 #[test]
81 fn cents_sub_cent_truncates() {
82 assert_eq!(usdc_base_units_to_cents(9_999).unwrap(), 0); assert_eq!(usdc_base_units_to_cents(10_000).unwrap(), 1); }
85
86 const NOW: u64 = 1_700_000_000;
89
90 #[test]
91 fn spend_limit_basic() {
92 let vault = make_vault(
93 10_000_000, 50_000_000, 30_000_000, NOW + 3600, 86_400,
98 );
99 let limit = calculate_spend_limit_cents(&vault, 10_000, NOW).unwrap();
100 assert_eq!(limit, 1000);
101 }
102
103 #[test]
104 fn spend_limit_expired_timeframe_uses_full() {
105 let vault = make_vault(
106 100_000_000, 50_000_000, 10_000_000, NOW - 100, 86_400,
111 );
112 let limit = calculate_spend_limit_cents(&vault, 100_000, NOW).unwrap();
113 assert_eq!(limit, 5000);
114 }
115
116 #[test]
117 fn spend_limit_capped_by_max() {
118 let vault = make_vault(
119 1_000_000_000, 1_000_000_000, 1_000_000_000, NOW + 3600,
123 86_400,
124 );
125 let limit = calculate_spend_limit_cents(&vault, 500, NOW).unwrap();
126 assert_eq!(limit, 500);
127 }
128
129 #[test]
130 fn spend_limit_zero_timeframe() {
131 let vault = make_vault(10_000_000, 50_000_000, 30_000_000, NOW + 3600, 0);
132 let limit = calculate_spend_limit_cents(&vault, 10_000, NOW).unwrap();
133 assert_eq!(limit, 0);
134 }
135}
136
137#[cfg(test)]
138#[allow(
139 clippy::allow_attributes,
140 clippy::allow_attributes_without_reason,
141 clippy::unwrap_used,
142 clippy::expect_used,
143 clippy::panic,
144 clippy::arithmetic_side_effects
145)]
146mod proptests {
147 use super::*;
148 use proptest::prelude::*;
149
150 proptest! {
151 #[test]
152 fn usdc_cents_never_exceeds_base_units(base_units in 0u64..=u64::MAX) {
153 let cents = usdc_base_units_to_cents(base_units).unwrap();
154 prop_assert!(cents <= base_units, "cents {} > base_units {}", cents, base_units);
155 }
156 }
157}