1use std::time::Duration;
4
5use bytes::Bytes;
6use http_body_util::BodyExt as _;
7use hyper::body::Incoming;
8use hyper::{
9 Method,
10 Request,
11 Response,
12};
13use hyper_openssl::client::legacy::HttpsConnector;
14use hyper_util::client::legacy::connect::HttpConnector;
15use hyper_util::client::legacy::Client as HttpClient;
16use hyper_util::rt::TokioExecutor;
17use openssl::ssl::{
18 SslConnector,
19 SslMethod,
20 SslVerifyMode,
21};
22
23use crate::fee_estimate_types::{
24 FeeEstimate,
25 FeeEstimateResponse,
26 FeeExtra,
27 NetworkFee,
28};
29use crate::transaction::TransactionExecute;
30use crate::{
31 Client,
32 Error,
33 FeeEstimateMode,
34 Transaction,
35};
36
37const DEFAULT_MAX_ATTEMPTS: usize = 10;
39
40const DEFAULT_MAX_BACKOFF: Duration = Duration::from_secs(8);
42
43const INITIAL_BACKOFF: Duration = Duration::from_millis(500);
45
46pub struct FeeEstimateQuery {
73 mode: FeeEstimateMode,
74 transaction_bytes: Option<Vec<u8>>,
75 high_volume_throttle: Option<u32>,
76 max_attempts: usize,
77 max_backoff: Duration,
78}
79
80impl Default for FeeEstimateQuery {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl FeeEstimateQuery {
87 #[must_use]
89 pub fn new() -> Self {
90 Self {
91 mode: FeeEstimateMode::default(),
92 transaction_bytes: None,
93 high_volume_throttle: None,
94 max_attempts: DEFAULT_MAX_ATTEMPTS,
95 max_backoff: DEFAULT_MAX_BACKOFF,
96 }
97 }
98
99 #[must_use]
101 pub fn set_mode(mut self, mode: FeeEstimateMode) -> Self {
102 self.mode = mode;
103 self
104 }
105
106 #[must_use]
108 pub fn get_mode(&self) -> FeeEstimateMode {
109 self.mode
110 }
111
112 pub fn set_transaction<D: TransactionExecute>(
121 mut self,
122 transaction: &mut Transaction<D>,
123 client: &Client,
124 ) -> crate::Result<Self> {
125 transaction.freeze_with(Some(client))?;
126 self.transaction_bytes = Some(transaction.to_bytes()?);
127 Ok(self)
128 }
129
130 #[must_use]
134 pub fn set_transaction_bytes(mut self, bytes: Vec<u8>) -> Self {
135 self.transaction_bytes = Some(bytes);
136 self
137 }
138
139 #[must_use]
141 pub fn get_transaction_bytes(&self) -> Option<&[u8]> {
142 self.transaction_bytes.as_deref()
143 }
144
145 #[must_use]
153 pub fn set_high_volume_throttle(mut self, basis_points: u32) -> Self {
154 assert!(basis_points <= 10000, "high_volume_throttle must be between 0 and 10000");
155 self.high_volume_throttle = Some(basis_points);
156 self
157 }
158
159 #[must_use]
161 pub fn get_high_volume_throttle(&self) -> Option<u32> {
162 self.high_volume_throttle
163 }
164
165 #[must_use]
167 pub fn set_max_attempts(mut self, max_attempts: usize) -> Self {
168 self.max_attempts = max_attempts;
169 self
170 }
171
172 #[must_use]
174 pub fn get_max_attempts(&self) -> usize {
175 self.max_attempts
176 }
177
178 #[must_use]
183 pub fn set_max_backoff(mut self, max_backoff: Duration) -> Self {
184 assert!(max_backoff >= Duration::from_millis(500), "max_backoff must be at least 500ms");
185 self.max_backoff = max_backoff;
186 self
187 }
188
189 #[must_use]
191 pub fn get_max_backoff(&self) -> Duration {
192 self.max_backoff
193 }
194
195 pub async fn execute(&self, client: &Client) -> crate::Result<FeeEstimateResponse> {
202 let transaction_bytes = self.transaction_bytes.as_ref().ok_or_else(|| {
203 Error::basic_parse("transaction bytes must be set on FeeEstimateQuery")
204 })?;
205
206 let base_url = mirror_rest_base_url(client);
207 let url = self.build_url(&base_url);
208
209 let http_client = build_http_client(&base_url);
210
211 let mut attempt = 0;
212 loop {
213 let request = Request::builder()
214 .method(Method::POST)
215 .uri(&url)
216 .header("Content-Type", "application/x-protobuf")
217 .header("Accept", "application/json")
218 .body(http_body_util::Full::new(Bytes::from(transaction_bytes.clone())))
219 .map_err(|e| Error::basic_parse(e.to_string()))?;
220
221 let result = http_client.request(request).await;
222
223 match result {
224 Ok(response) => {
225 let status = response.status();
226 if status.is_success() {
227 return parse_response(response).await;
228 }
229
230 if should_retry_status(status) && attempt < self.max_attempts {
231 attempt += 1;
232 let delay = compute_backoff(attempt, self.max_backoff);
233 tokio::time::sleep(delay).await;
234 continue;
235 }
236
237 let body = read_body(response).await.unwrap_or_default();
238 return Err(Error::basic_parse(format!(
239 "fee estimate query failed with HTTP {status}: {body}"
240 )));
241 }
242 Err(e) => {
243 if attempt < self.max_attempts {
244 attempt += 1;
245 let delay = compute_backoff(attempt, self.max_backoff);
246 tokio::time::sleep(delay).await;
247 continue;
248 }
249 return Err(Error::basic_parse(format!(
250 "fee estimate query failed after {attempt} attempts: {e}"
251 )));
252 }
253 }
254 }
255 }
256
257 fn build_url(&self, base_url: &str) -> String {
258 let mut url = format!("{base_url}/network/fees?mode={}", self.mode.as_str());
259 if let Some(throttle) = self.high_volume_throttle {
260 url.push_str(&format!("&highVolumeThrottle={throttle}"));
261 }
262 url
263 }
264}
265
266fn should_retry_status(status: hyper::StatusCode) -> bool {
268 matches!(status.as_u16(), 408 | 429 | 500 | 502 | 503 | 504)
269}
270
271fn compute_backoff(attempt: usize, max_backoff: Duration) -> Duration {
273 let delay = INITIAL_BACKOFF.saturating_mul(1u32.wrapping_shl(attempt as u32));
274 delay.min(max_backoff)
275}
276
277fn mirror_rest_base_url(client: &Client) -> String {
279 let addresses = client.mirror_network();
280 let address = addresses.first().expect("mirror network must have at least one address");
281
282 let (host, port_str) = if let Some(idx) = address.rfind(':') {
284 (&address[..idx], &address[idx + 1..])
285 } else {
286 (address.as_str(), "443")
287 };
288
289 let port: u16 = port_str.parse().unwrap_or(443);
290
291 let is_localhost = host.contains("localhost") || host.contains("127.0.0.1");
292
293 if is_localhost {
294 let rest_port = if port == 5600 { 5551 } else { port };
296 format!("http://{host}:{rest_port}/api/v1")
297 } else {
298 let scheme = if port == 80 { "http" } else { "https" };
299 if (scheme == "https" && port == 443) || (scheme == "http" && port == 80) {
300 format!("{scheme}://{host}/api/v1")
301 } else {
302 format!("{scheme}://{host}:{port}/api/v1")
303 }
304 }
305}
306
307fn build_http_client(
309 base_url: &str,
310) -> HttpClient<HttpsConnector<HttpConnector>, http_body_util::Full<Bytes>> {
311 let mut http = HttpConnector::new();
312 http.enforce_http(false);
313
314 if base_url.starts_with("https") {
315 let mut ssl_builder = SslConnector::builder(SslMethod::tls()).unwrap();
316 ssl_builder.set_verify(SslVerifyMode::PEER);
317 let https = HttpsConnector::with_connector(http, ssl_builder).unwrap();
318 HttpClient::builder(TokioExecutor::new()).build(https)
319 } else {
320 let mut ssl_builder = SslConnector::builder(SslMethod::tls()).unwrap();
322 ssl_builder.set_verify(SslVerifyMode::NONE);
323 let https = HttpsConnector::with_connector(http, ssl_builder).unwrap();
324 HttpClient::builder(TokioExecutor::new()).build(https)
325 }
326}
327
328async fn read_body(response: Response<Incoming>) -> Result<String, String> {
330 let body_bytes = response.into_body().collect().await.map_err(|e| e.to_string())?.to_bytes();
331 String::from_utf8(body_bytes.to_vec()).map_err(|e| e.to_string())
332}
333
334async fn parse_response(response: Response<Incoming>) -> crate::Result<FeeEstimateResponse> {
336 let body = read_body(response)
337 .await
338 .map_err(|e| Error::basic_parse(format!("failed to read response body: {e}")))?;
339
340 parse_fee_estimate_response_json(&body)
341}
342
343fn parse_fee_estimate_response_json(json: &str) -> crate::Result<FeeEstimateResponse> {
344 let value: serde_json::Value = serde_json::from_str(json)
345 .map_err(|e| Error::basic_parse(format!("failed to parse fee estimate JSON: {e}")))?;
346
347 let high_volume_multiplier = value
348 .get("high_volume_multiplier")
349 .or_else(|| value.get("highVolumeMultiplier"))
350 .and_then(|v| v.as_u64())
351 .unwrap_or(1);
352
353 let network = value.get("network").and_then(|v| parse_network_fee(v));
354
355 let node = value.get("node").and_then(|v| parse_fee_estimate(v));
356
357 let service = value.get("service").and_then(|v| parse_fee_estimate(v));
358
359 let total = value.get("total").and_then(|v| v.as_u64()).unwrap_or(0);
360
361 Ok(FeeEstimateResponse { high_volume_multiplier, network, node, service, total })
362}
363
364fn parse_network_fee(value: &serde_json::Value) -> Option<NetworkFee> {
365 if value.is_null() {
366 return None;
367 }
368 Some(NetworkFee {
369 multiplier: value.get("multiplier").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
370 subtotal: value.get("subtotal").and_then(|v| v.as_u64()).unwrap_or(0),
371 })
372}
373
374fn parse_fee_estimate(value: &serde_json::Value) -> Option<FeeEstimate> {
375 if value.is_null() {
376 return None;
377 }
378 let base = value.get("base").and_then(|v| v.as_u64()).unwrap_or(0);
379 let extras = value
380 .get("extras")
381 .and_then(|v| v.as_array())
382 .map(|arr| arr.iter().filter_map(parse_fee_extra).collect())
383 .unwrap_or_default();
384
385 Some(FeeEstimate { base, extras })
386}
387
388fn parse_fee_extra(value: &serde_json::Value) -> Option<FeeExtra> {
389 if !value.is_object() {
390 return None;
391 }
392 Some(FeeExtra {
393 name: value.get("name").and_then(|v| v.as_str()).map(String::from),
394 included: value.get("included").and_then(|v| v.as_u64()).unwrap_or(0),
395 count: value.get("count").and_then(|v| v.as_u64()).unwrap_or(0),
396 charged: value.get("charged").and_then(|v| v.as_u64()).unwrap_or(0),
397 fee_per_unit: value
398 .get("fee_per_unit")
399 .or_else(|| value.get("feePerUnit"))
400 .and_then(|v| v.as_u64())
401 .unwrap_or(0),
402 subtotal: value.get("subtotal").and_then(|v| v.as_u64()).unwrap_or(0),
403 })
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn parse_fee_estimate_response() {
412 let json = r#"{
413 "high_volume_multiplier": 1,
414 "network": {
415 "multiplier": 2,
416 "subtotal": 1000
417 },
418 "node": {
419 "base": 500,
420 "extras": [
421 {
422 "name": "SIGNATURE_VERIFICATION",
423 "included": 1,
424 "count": 2,
425 "charged": 1,
426 "fee_per_unit": 100,
427 "subtotal": 100
428 }
429 ]
430 },
431 "service": {
432 "base": 300,
433 "extras": []
434 },
435 "total": 1800
436 }"#;
437
438 let response = parse_fee_estimate_response_json(json).unwrap();
439 assert_eq!(response.high_volume_multiplier, 1);
440 assert_eq!(response.total, 1800);
441
442 let network = response.network.unwrap();
443 assert_eq!(network.multiplier, 2);
444 assert_eq!(network.subtotal, 1000);
445
446 let node = response.node.unwrap();
447 assert_eq!(node.base, 500);
448 assert_eq!(node.extras.len(), 1);
449 assert_eq!(node.extras[0].name.as_deref(), Some("SIGNATURE_VERIFICATION"));
450 assert_eq!(node.extras[0].charged, 1);
451 assert_eq!(node.extras[0].fee_per_unit, 100);
452
453 let service = response.service.unwrap();
454 assert_eq!(service.base, 300);
455 assert!(service.extras.is_empty());
456 }
457
458 #[test]
459 fn parse_camel_case_response() {
460 let json = r#"{
461 "highVolumeMultiplier": 2,
462 "network": { "multiplier": 1, "subtotal": 500 },
463 "node": { "base": 100, "extras": [{ "name": "SIG", "included": 0, "count": 1, "charged": 1, "feePerUnit": 50, "subtotal": 50 }] },
464 "service": { "base": 200, "extras": [] },
465 "total": 750
466 }"#;
467
468 let response = parse_fee_estimate_response_json(json).unwrap();
469 assert_eq!(response.high_volume_multiplier, 2);
470 assert_eq!(response.total, 750);
471 assert_eq!(response.node.unwrap().extras[0].fee_per_unit, 50);
472 }
473
474 #[test]
475 fn fee_estimate_subtotal() {
476 let estimate = FeeEstimate {
477 base: 100,
478 extras: vec![
479 FeeExtra {
480 name: Some("a".into()),
481 included: 0,
482 count: 1,
483 charged: 1,
484 fee_per_unit: 50,
485 subtotal: 50,
486 },
487 FeeExtra {
488 name: None,
489 included: 0,
490 count: 2,
491 charged: 2,
492 fee_per_unit: 25,
493 subtotal: 50,
494 },
495 ],
496 };
497 assert_eq!(estimate.subtotal(), 200);
498 }
499
500 #[test]
501 fn build_url_intrinsic() {
502 let query = FeeEstimateQuery::new();
503 let url = query.build_url("https://mirror.example.com/api/v1");
504 assert_eq!(url, "https://mirror.example.com/api/v1/network/fees?mode=INTRINSIC");
505 }
506
507 #[test]
508 fn build_url_state_with_throttle() {
509 let query =
510 FeeEstimateQuery::new().set_mode(FeeEstimateMode::State).set_high_volume_throttle(5000);
511 let url = query.build_url("https://mirror.example.com/api/v1");
512 assert_eq!(
513 url,
514 "https://mirror.example.com/api/v1/network/fees?mode=STATE&highVolumeThrottle=5000"
515 );
516 }
517
518 #[test]
519 fn mirror_rest_url_mainnet() {
520 let url = mirror_rest_base_url_from_address("mainnet-public.mirrornode.hedera.com:443");
521 assert_eq!(url, "https://mainnet-public.mirrornode.hedera.com/api/v1");
522 }
523
524 #[test]
525 fn mirror_rest_url_localhost() {
526 let url = mirror_rest_base_url_from_address("127.0.0.1:5600");
527 assert_eq!(url, "http://127.0.0.1:5551/api/v1");
528 }
529
530 #[test]
531 fn mirror_rest_url_localhost_custom_port() {
532 let url = mirror_rest_base_url_from_address("localhost:8080");
533 assert_eq!(url, "http://localhost:8080/api/v1");
534 }
535
536 fn mirror_rest_base_url_from_address(address: &str) -> String {
538 let (host, port_str) = if let Some(idx) = address.rfind(':') {
539 (&address[..idx], &address[idx + 1..])
540 } else {
541 (address, "443")
542 };
543
544 let port: u16 = port_str.parse().unwrap_or(443);
545 let is_localhost = host.contains("localhost") || host.contains("127.0.0.1");
546
547 if is_localhost {
548 let rest_port = if port == 5600 { 5551 } else { port };
549 format!("http://{host}:{rest_port}/api/v1")
550 } else {
551 let scheme = if port == 80 { "http" } else { "https" };
552 if (scheme == "https" && port == 443) || (scheme == "http" && port == 80) {
553 format!("{scheme}://{host}/api/v1")
554 } else {
555 format!("{scheme}://{host}:{port}/api/v1")
556 }
557 }
558 }
559
560 #[test]
561 fn default_settings() {
562 let query = FeeEstimateQuery::new();
563 assert_eq!(query.get_mode(), FeeEstimateMode::Intrinsic);
564 assert_eq!(query.get_max_attempts(), 10);
565 assert!(query.get_transaction_bytes().is_none());
566 assert!(query.get_high_volume_throttle().is_none());
567 }
568
569 #[test]
570 #[should_panic(expected = "high_volume_throttle must be between 0 and 10000")]
571 fn high_volume_throttle_out_of_range() {
572 let _ = FeeEstimateQuery::new().set_high_volume_throttle(10001);
573 }
574
575 #[test]
576 fn backoff_computation() {
577 assert_eq!(compute_backoff(0, Duration::from_secs(8)), Duration::from_millis(500));
578 assert_eq!(compute_backoff(1, Duration::from_secs(8)), Duration::from_millis(1000));
579 assert_eq!(compute_backoff(2, Duration::from_secs(8)), Duration::from_millis(2000));
580 assert_eq!(compute_backoff(3, Duration::from_secs(8)), Duration::from_millis(4000));
581 assert_eq!(compute_backoff(4, Duration::from_secs(8)), Duration::from_millis(8000));
582 assert_eq!(compute_backoff(5, Duration::from_secs(8)), Duration::from_millis(8000));
583 }
584}