1use evmlib::common::Amount;
24use evmlib::quoting_metrics::QuotingMetrics;
25
26const MIN_PRICE: u64 = 3;
28
29const SCALING_FACTOR: f64 = 1.0;
32
33const ANT_PRICE: f64 = 1.0;
35
36#[allow(
48 clippy::cast_precision_loss,
49 clippy::cast_possible_truncation,
50 clippy::cast_sign_loss
51)]
52#[must_use]
53pub fn calculate_price(metrics: &QuotingMetrics) -> Amount {
54 let min_price = Amount::from(MIN_PRICE);
55
56 if metrics.max_records == 0 {
58 return min_price;
59 }
60
61 let total_records = metrics.close_records_stored as u64;
63
64 let max_records = metrics.max_records as f64;
65
66 let r_lower = total_records as f64 / max_records;
68 let r_upper = (total_records + 1) as f64 / max_records;
70
71 if r_lower >= 1.0 || r_upper >= 1.0 {
73 return Amount::from(u64::MAX);
74 }
75 if (r_upper - r_lower).abs() < f64::EPSILON {
76 return min_price;
77 }
78
79 let upper_diff = (r_upper - 1.0).abs();
81 let lower_diff = (r_lower - 1.0).abs();
82
83 if upper_diff < f64::EPSILON || lower_diff < f64::EPSILON {
85 return min_price;
86 }
87
88 let log_upper = upper_diff.ln();
89 let log_lower = lower_diff.ln();
90 let log_diff = log_upper - log_lower;
91
92 let linear_part = r_upper - r_lower;
93
94 let part_one = (-SCALING_FACTOR / ANT_PRICE) * log_diff;
96 let part_two = MIN_PRICE as f64 * linear_part;
97 let part_three = linear_part / ANT_PRICE;
98
99 let price = part_one + part_two - part_three;
100
101 if price <= 0.0 || !price.is_finite() {
102 return min_price;
103 }
104
105 let data_size_factor = metrics.data_size.max(1) as f64;
107 let scaled_price = price * data_size_factor;
108
109 if !scaled_price.is_finite() {
110 return min_price;
111 }
112
113 let price_u64 = if scaled_price > u64::MAX as f64 {
115 u64::MAX
116 } else {
117 (scaled_price as u64).max(MIN_PRICE)
118 };
119
120 Amount::from(price_u64)
121}
122
123#[cfg(test)]
124#[allow(clippy::unwrap_used, clippy::expect_used)]
125mod tests {
126 use super::*;
127
128 fn make_metrics(
129 records_stored: usize,
130 max_records: usize,
131 data_size: usize,
132 data_type: u32,
133 ) -> QuotingMetrics {
134 let records_per_type = if records_stored > 0 {
135 vec![(data_type, u32::try_from(records_stored).unwrap_or(u32::MAX))]
136 } else {
137 vec![]
138 };
139 QuotingMetrics {
140 data_type,
141 data_size,
142 close_records_stored: records_stored,
143 records_per_type,
144 max_records,
145 received_payment_count: 0,
146 live_time: 0,
147 network_density: None,
148 network_size: Some(500),
149 }
150 }
151
152 #[test]
153 fn test_empty_node_gets_min_price() {
154 let metrics = make_metrics(0, 1000, 1, 0);
155 let price = calculate_price(&metrics);
156 assert_eq!(price, Amount::from(MIN_PRICE));
158 }
159
160 #[test]
161 fn test_half_full_node_costs_more() {
162 let empty = make_metrics(0, 1000, 1024, 0);
163 let half = make_metrics(500, 1000, 1024, 0);
164 let price_empty = calculate_price(&empty);
165 let price_half = calculate_price(&half);
166 assert!(
167 price_half > price_empty,
168 "Half-full price ({price_half}) should exceed empty price ({price_empty})"
169 );
170 }
171
172 #[test]
173 fn test_nearly_full_node_costs_much_more() {
174 let half = make_metrics(500, 1000, 1024, 0);
175 let nearly_full = make_metrics(900, 1000, 1024, 0);
176 let price_half = calculate_price(&half);
177 let price_nearly_full = calculate_price(&nearly_full);
178 assert!(
179 price_nearly_full > price_half,
180 "Nearly-full price ({price_nearly_full}) should far exceed half-full price ({price_half})"
181 );
182 }
183
184 #[test]
185 fn test_full_node_returns_max_price() {
186 let metrics = make_metrics(1000, 1000, 1024, 0);
188 let price = calculate_price(&metrics);
189 assert_eq!(price, Amount::from(u64::MAX));
190 }
191
192 #[test]
193 fn test_price_increases_monotonically() {
194 let max_records = 1000;
195 let data_size = 1024;
196 let mut prev_price = Amount::ZERO;
197
198 for pct in 0..100 {
200 let records = pct * max_records / 100;
201 let metrics = make_metrics(records, max_records, data_size, 0);
202 let price = calculate_price(&metrics);
203 assert!(
204 price >= prev_price,
205 "Price at {pct}% ({price}) should be >= price at previous step ({prev_price})"
206 );
207 prev_price = price;
208 }
209 }
210
211 #[test]
212 fn test_zero_max_records_returns_min_price() {
213 let metrics = make_metrics(0, 0, 1024, 0);
214 let price = calculate_price(&metrics);
215 assert_eq!(price, Amount::from(MIN_PRICE));
216 }
217
218 #[test]
219 fn test_different_data_sizes_same_fullness() {
220 let small = make_metrics(500, 1000, 100, 0);
221 let large = make_metrics(500, 1000, 10000, 0);
222 let price_small = calculate_price(&small);
223 let price_large = calculate_price(&large);
224 assert!(
225 price_large > price_small,
226 "Larger data ({price_large}) should cost more than smaller data ({price_small})"
227 );
228 }
229
230 #[test]
231 fn test_price_with_multiple_record_types() {
232 let metrics = QuotingMetrics {
234 data_type: 0,
235 data_size: 1024,
236 close_records_stored: 500,
237 records_per_type: vec![(0, 300), (1, 200)],
238 max_records: 1000,
239 received_payment_count: 0,
240 live_time: 0,
241 network_density: None,
242 network_size: Some(500),
243 };
244 let price_multi = calculate_price(&metrics);
245
246 let metrics_single = make_metrics(500, 1000, 1024, 0);
248 let price_single = calculate_price(&metrics_single);
249
250 assert_eq!(price_multi, price_single);
252 }
253
254 #[test]
255 fn test_price_at_95_percent() {
256 let metrics = make_metrics(950, 1000, 1024, 0);
257 let price = calculate_price(&metrics);
258 let min = Amount::from(MIN_PRICE);
259 assert!(
260 price > min,
261 "Price at 95% should be above minimum, got {price}"
262 );
263 }
264
265 #[test]
266 fn test_price_at_99_percent() {
267 let metrics = make_metrics(990, 1000, 1024, 0);
268 let price = calculate_price(&metrics);
269 let price_95 = calculate_price(&make_metrics(950, 1000, 1024, 0));
270 assert!(
271 price > price_95,
272 "Price at 99% ({price}) should exceed price at 95% ({price_95})"
273 );
274 }
275
276 #[test]
277 fn test_over_capacity_returns_max_price() {
278 let metrics = make_metrics(1100, 1000, 1024, 0);
280 let price = calculate_price(&metrics);
281 assert_eq!(
282 price,
283 Amount::from(u64::MAX),
284 "Over-capacity should return max price"
285 );
286 }
287
288 #[test]
289 fn test_price_deterministic() {
290 let metrics = make_metrics(500, 1000, 1024, 0);
291 let price1 = calculate_price(&metrics);
292 let price2 = calculate_price(&metrics);
293 let price3 = calculate_price(&metrics);
294 assert_eq!(price1, price2);
295 assert_eq!(price2, price3);
296 }
297}