forked from StellarSend/backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrate.rs
More file actions
159 lines (142 loc) · 4.96 KB
/
Copy pathrate.rs
File metadata and controls
159 lines (142 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use crate::{
error::AppResult,
services::stellar::StellarService,
};
use serde::Serialize;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
/// A single exchange rate entry.
#[derive(Debug, Clone, Serialize)]
pub struct ExchangeRate {
pub from: String,
pub to: String,
pub rate: f64,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
/// In-memory cache entry.
#[derive(Debug, Clone)]
struct CacheEntry {
rate: ExchangeRate,
expires_at: Instant,
}
/// RateService fetches exchange rates by querying the Stellar DEX via
/// Horizon's path-payment endpoint and caches results with a configurable TTL.
#[derive(Clone)]
pub struct RateService {
stellar: StellarService,
cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
ttl: Duration,
}
impl RateService {
pub fn new(stellar: StellarService, cache_ttl_secs: u64) -> Self {
Self {
stellar,
cache: Arc::new(Mutex::new(HashMap::new())),
ttl: Duration::from_secs(cache_ttl_secs),
}
}
/// Fetch (or return a cached) exchange rate between two assets.
///
/// We probe the DEX by pricing a 1-unit strict-send path payment and
/// reading the resulting destination amount as the implied rate.
pub async fn fetch_rate(&self, from: &str, to: &str) -> AppResult<ExchangeRate> {
let cache_key = format!("{from}:{to}");
// Check cache.
if let Some(entry) = self.cache.lock().unwrap().get(&cache_key) {
if entry.expires_at > Instant::now() {
return Ok(entry.rate.clone());
}
}
// Build minimal Asset structs for the query.
let from_asset = parse_asset_code(from);
let to_asset = parse_asset_code(to);
// Use a well-funded Horizon anchor account for path finding,
// or fall back to a synthetic destination placeholder.
// For rate queries the destination is not critical — we just need
// the DEX to return paths.
let dummy_destination = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN";
let paths = self
.stellar
.get_path_payment_paths(&from_asset, &to_asset, "1", dummy_destination)
.await;
let rate_value: f64 = match paths {
Ok(records) if !records.is_empty() => {
let best = records
.iter()
.map(|r| r.destination_amount.parse::<f64>().unwrap_or(0.0))
.fold(0.0_f64, f64::max);
best
}
_ => {
// Fallback: if the DEX query fails for XLM pairs, use a synthetic rate.
self.fallback_rate(from, to)
}
};
let entry_rate = ExchangeRate {
from: from.to_string(),
to: to.to_string(),
rate: rate_value,
timestamp: chrono::Utc::now(),
};
// Cache the result.
self.cache.lock().unwrap().insert(
cache_key,
CacheEntry {
rate: entry_rate.clone(),
expires_at: Instant::now() + self.ttl,
},
);
Ok(entry_rate)
}
/// Fetch multiple rates at once.
pub async fn fetch_rates(
&self,
pairs: &[(String, String)],
) -> AppResult<Vec<ExchangeRate>> {
let mut results = Vec::with_capacity(pairs.len());
for (from, to) in pairs {
results.push(self.fetch_rate(from, to).await?);
}
Ok(results)
}
/// Hard-coded fallback rates for common pairs when the DEX returns no paths.
/// In production these should come from a proper price oracle.
fn fallback_rate(&self, from: &str, to: &str) -> f64 {
match (from.to_uppercase().as_str(), to.to_uppercase().as_str()) {
("XLM", "USD") | ("XLM", "USDC") => 0.11,
("USD", "XLM") | ("USDC", "XLM") => 9.09,
("BTC", "XLM") => 700_000.0,
("XLM", "BTC") => 0.0000014,
(a, b) if a == b => 1.0,
_ => 0.0,
}
}
/// Invalidate the entire cache (useful after known market events).
pub fn invalidate_cache(&self) {
self.cache.lock().unwrap().clear();
}
}
/// Parse a simple asset code string into an `Asset`.
///
/// Supports:
/// - `"XLM"` → native
/// - `"USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"` → issued
fn parse_asset_code(code: &str) -> crate::models::payment::Asset {
if let Some((asset_code, issuer)) = code.split_once(':') {
crate::models::payment::Asset {
code: asset_code.to_uppercase(),
issuer: Some(issuer.to_string()),
}
} else if code.to_uppercase() == "XLM" {
crate::models::payment::Asset::native()
} else {
// Treat bare code as native-like (caller should pass full code:issuer).
crate::models::payment::Asset {
code: code.to_uppercase(),
issuer: None,
}
}
}