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