1use serde::{Deserialize, Serialize};
11use std::fmt;
12use std::str::FromStr;
13
14pub const STROOPS_PER_XLM: u64 = 10_000_000;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
19pub enum NetworkKind {
20 #[default]
21 Testnet,
22 Mainnet,
23 Standalone,
24}
25
26impl fmt::Display for NetworkKind {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 let s = match self {
29 NetworkKind::Testnet => "testnet",
30 NetworkKind::Mainnet => "mainnet",
31 NetworkKind::Standalone => "standalone",
32 };
33 write!(f, "{s}")
34 }
35}
36
37impl FromStr for NetworkKind {
38 type Err = FeeError;
39 fn from_str(s: &str) -> Result<Self, Self::Err> {
40 match s.to_ascii_lowercase().as_str() {
41 "testnet" => Ok(NetworkKind::Testnet),
42 "mainnet" => Ok(NetworkKind::Mainnet),
43 "standalone" => Ok(NetworkKind::Standalone),
44 other => Err(FeeError::UnknownNetwork(other.to_string())),
45 }
46 }
47}
48
49impl NetworkKind {
50 pub fn fee_multiplier(self) -> f64 {
52 match self {
53 NetworkKind::Testnet => 1.0,
54 NetworkKind::Mainnet => 1.25,
55 NetworkKind::Standalone => 0.10,
56 }
57 }
58}
59
60#[derive(Debug, PartialEq)]
62pub enum FeeError {
63 EmptyLedger,
64 UnknownNetwork(String),
65 Conversion,
66}
67
68impl fmt::Display for FeeError {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 FeeError::EmptyLedger => {
72 write!(f, "no ledger data supplied for fee estimation")
73 }
74 FeeError::UnknownNetwork(n) => {
75 write!(f, "unknown Soroban network: {n}")
76 }
77 FeeError::Conversion => {
78 write!(f, "integer conversion failure")
79 }
80 }
81 }
82}
83
84impl std::error::Error for FeeError {}
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
88pub struct FeeConfig {
89 pub network: NetworkKind,
90 pub multiplier_override: Option<f64>,
91}
92
93impl FeeConfig {
94 pub fn testnet() -> Self {
95 Self {
96 network: NetworkKind::Testnet,
97 multiplier_override: None,
98 }
99 }
100 pub fn mainnet() -> Self {
101 Self {
102 network: NetworkKind::Mainnet,
103 multiplier_override: None,
104 }
105 }
106 pub fn standalone() -> Self {
107 Self {
108 network: NetworkKind::Standalone,
109 multiplier_override: None,
110 }
111 }
112
113 pub fn multiplier(&self) -> f64 {
115 self.multiplier_override
116 .unwrap_or_else(|| self.network.fee_multiplier())
117 }
118}
119
120#[derive(Debug, Clone, PartialEq)]
122pub struct LedgerFeeSample {
123 pub base_fee: u32, }
125
126pub struct FeeEstimator {
128 config: FeeConfig,
129}
130
131impl FeeEstimator {
132 pub fn new(config: FeeConfig) -> Self {
133 Self { config }
134 }
135
136 pub fn estimate_base_fee(samples: &[LedgerFeeSample]) -> Result<u32, FeeError> {
138 if samples.is_empty() {
139 return Err(FeeError::EmptyLedger);
140 }
141 let mut sorted: Vec<u32> = samples.iter().map(|s| s.base_fee).collect();
142 sorted.sort_unstable();
143 let mid = (sorted.len() - 1) / 2;
145 Ok(sorted[mid])
146 }
147
148 pub fn estimate_stroops(&self, samples: &[LedgerFeeSample]) -> Result<u64, FeeError> {
150 let base = Self::estimate_base_fee(samples)? as u64;
151 let mult = self.config.multiplier();
152 let fee = base as f64 * mult;
153 if fee > u64::MAX as f64 {
154 return Err(FeeError::Conversion);
155 }
156 Ok(fee.round() as u64)
157 }
158
159 pub fn estimate_xlm(&self, samples: &[LedgerFeeSample]) -> Result<String, FeeError> {
161 let stroops = self.estimate_stroops(samples)?;
162 let whole = stroops / STROOPS_PER_XLM;
163 let frac = stroops % STROOPS_PER_XLM;
164 if frac == 0 {
165 Ok(format!("{whole}"))
166 } else {
167 let s = format!("{whole}.{frac:07}");
168 Ok(s.trim_end_matches('0').trim_end_matches('.').to_string())
169 }
170 }
171
172 pub fn estimate(&self, samples: &[LedgerFeeSample]) -> Result<(u64, String), FeeError> {
174 let stroops = self.estimate_stroops(samples)?;
175 let xlm = self.estimate_xlm(samples)?;
176 Ok((stroops, xlm))
177 }
178}
179
180impl Default for FeeEstimator {
181 fn default() -> Self {
182 Self::new(FeeConfig::default())
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn sample(s: u32) -> LedgerFeeSample {
191 LedgerFeeSample { base_fee: s }
192 }
193
194 #[test]
195 fn test_stroops_per_xlm() {
196 assert_eq!(STROOPS_PER_XLM, 10_000_000);
197 }
198
199 #[test]
200 fn test_network_display() {
201 assert_eq!(NetworkKind::Testnet.to_string(), "testnet");
202 assert_eq!(NetworkKind::Mainnet.to_string(), "mainnet");
203 assert_eq!(NetworkKind::Standalone.to_string(), "standalone");
204 }
205
206 #[test]
207 fn test_network_from_str() {
208 assert_eq!(
209 "testnet".parse::<NetworkKind>().unwrap(),
210 NetworkKind::Testnet
211 );
212 assert_eq!(
213 "MAINNET".parse::<NetworkKind>().unwrap(),
214 NetworkKind::Mainnet
215 );
216 assert!("fakenet".parse::<NetworkKind>().is_err());
217 }
218
219 #[test]
220 fn test_network_multipliers() {
221 assert_eq!(NetworkKind::Testnet.fee_multiplier(), 1.0);
222 assert_eq!(NetworkKind::Mainnet.fee_multiplier(), 1.25);
223 assert_eq!(NetworkKind::Standalone.fee_multiplier(), 0.10);
224 }
225
226 #[test]
227 fn test_estimate_base_fee_median() {
228 let samples = vec![sample(100), sample(200), sample(300)];
229 assert_eq!(FeeEstimator::estimate_base_fee(&samples).unwrap(), 200);
230 }
231
232 #[test]
233 fn test_estimate_base_fee_even_count() {
234 let samples = vec![sample(100), sample(200), sample(300), sample(400)];
235 assert_eq!(FeeEstimator::estimate_base_fee(&samples).unwrap(), 200);
236 }
237
238 #[test]
239 fn test_estimate_base_fee_empty() {
240 let empty: [LedgerFeeSample; 0] = [];
241 let err = FeeEstimator::estimate_base_fee(&empty).unwrap_err();
242 assert_eq!(err, FeeError::EmptyLedger);
243 }
244
245 #[test]
246 fn test_testnet_fee_stroops() {
247 let est = FeeEstimator::new(FeeConfig::testnet());
248 let samples = vec![sample(100)];
249 assert_eq!(est.estimate_stroops(&samples).unwrap(), 100);
250 }
251
252 #[test]
253 fn test_mainnet_fee_stroops() {
254 let est = FeeEstimator::new(FeeConfig::mainnet());
255 let samples = vec![sample(100)];
256 assert_eq!(est.estimate_stroops(&samples).unwrap(), 125);
257 }
258
259 #[test]
260 fn test_standalone_fee_stroops() {
261 let est = FeeEstimator::new(FeeConfig::standalone());
262 let samples = vec![sample(100)];
263 assert_eq!(est.estimate_stroops(&samples).unwrap(), 10);
264 }
265
266 #[test]
267 fn test_override_multiplier() {
268 let cfg = FeeConfig {
269 network: NetworkKind::Testnet,
270 multiplier_override: Some(2.5),
271 };
272 let est = FeeEstimator::new(cfg);
273 let samples = vec![sample(100)];
274 assert_eq!(est.estimate_stroops(&samples).unwrap(), 250);
275 }
276
277 #[test]
278 fn test_estimate_xlm_exact() {
279 let est = FeeEstimator::new(FeeConfig::standalone());
280 let samples = vec![sample(100)];
281 assert_eq!(est.estimate_xlm(&samples).unwrap(), "0.000001");
282 }
283
284 #[test]
285 fn test_estimate_xlm_whole() {
286 let est = FeeEstimator::new(FeeConfig::mainnet());
287 let samples = vec![sample(8_000_000)];
288 assert_eq!(est.estimate_xlm(&samples).unwrap(), "1");
289 }
290
291 #[test]
292 fn test_estimate_combined() {
293 let est = FeeEstimator::new(FeeConfig::mainnet());
294 let samples = vec![sample(100)];
295 let (stroops, xlm) = est.estimate(&samples).unwrap();
296 assert_eq!(stroops, 125);
297 assert_eq!(xlm, "0.0000125");
298 }
299
300 #[test]
301 fn test_real_testnet_sample() {
302 let samples = vec![sample(100)];
303 let est = FeeEstimator::new(FeeConfig::testnet());
304 let (stroops, xlm) = est.estimate(&samples).unwrap();
305 assert_eq!(stroops, 100);
306 assert_eq!(xlm, "0.00001");
307 }
308
309 #[test]
310 fn test_multiple_samples() {
311 let samples = vec![sample(100), sample(120), sample(90), sample(110)];
312 let est = FeeEstimator::new(FeeConfig::testnet());
313 let (stroops, _xlm) = est.estimate(&samples).unwrap();
314 assert_eq!(stroops, 100); }
316}