use async_trait::async_trait;
use chrono::{DateTime, Utc};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::conditions::OracleProvider;
use crate::error::{EscrowError, Result};
use crate::types::{ConditionResult, OracleId};
pub use crate::types::OracleId as OracleIdType;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OracleValue {
String(String),
Number(f64),
Boolean(bool),
Json(serde_json::Value),
}
impl OracleValue {
pub fn to_comparison_string(&self) -> String {
match self {
OracleValue::String(s) => s.clone(),
OracleValue::Number(n) => n.to_string(),
OracleValue::Boolean(b) => b.to_string(),
OracleValue::Json(v) => v.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HTTPOracleConfig {
pub id: OracleId,
pub base_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
pub endpoint_template: String,
pub value_path: String,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
}
fn default_timeout() -> u64 {
30
}
pub struct HTTPOracle {
config: HTTPOracleConfig,
client: Client,
}
impl HTTPOracle {
pub fn new(config: HTTPOracleConfig) -> Result<Self> {
let client = Client::builder()
.timeout(std::time::Duration::from_secs(config.timeout_seconds))
.build()
.map_err(|e| EscrowError::Oracle(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self { config, client })
}
fn build_url(&self, query: &str) -> String {
let endpoint = self.config.endpoint_template.replace("{}", query);
format!("{}{}", self.config.base_url, endpoint)
}
fn extract_value(&self, json: &serde_json::Value) -> Result<OracleValue> {
let path = &self.config.value_path;
if path == "$" || path.is_empty() {
return Ok(OracleValue::Json(json.clone()));
}
let path = path.trim_start_matches('$');
let parts: Vec<&str> = path.trim_start_matches('.')
.split('.')
.filter(|p| !p.is_empty())
.collect();
let mut current = json;
for part in &parts {
current = current
.get(part)
.ok_or_else(|| {
EscrowError::Oracle(format!("JSONPath '{}' not found in response", path))
})?;
}
if let Some(s) = current.as_str() {
Ok(OracleValue::String(s.to_string()))
} else if let Some(n) = current.as_f64() {
Ok(OracleValue::Number(n))
} else if let Some(b) = current.as_bool() {
Ok(OracleValue::Boolean(b))
} else {
Ok(OracleValue::Json(current.clone()))
}
}
}
#[async_trait]
impl OracleProvider for HTTPOracle {
async fn query(&self, query: &str, expected_value: &str) -> Result<ConditionResult> {
let url = self.build_url(query);
let mut request = self.client.get(&url);
if let Some(api_key) = &self.config.api_key {
request = request.header("Authorization", format!("Bearer {}", api_key));
}
let response = request
.send()
.await
.map_err(|e| EscrowError::Oracle(format!("HTTP request failed: {}", e)))?;
if !response.status().is_success() {
return Ok(ConditionResult {
satisfied: false,
reason: format!("Oracle returned HTTP {}", response.status()),
evaluated_at: Utc::now(),
data: Some(serde_json::json!({
"error": "http_error",
"status": response.status().as_u16(),
})),
});
}
let json: serde_json::Value = response
.json()
.await
.map_err(|e| EscrowError::Oracle(format!("Failed to parse JSON response: {}", e)))?;
let value = self.extract_value(&json)?;
let value_str = value.to_comparison_string();
let satisfied = &value_str == expected_value;
Ok(ConditionResult {
satisfied,
reason: if satisfied {
format!("Oracle returned expected value: {}", value_str)
} else {
format!("Oracle returned '{}', expected '{}'", value_str, expected_value)
},
evaluated_at: Utc::now(),
data: Some(serde_json::json!({
"oracle_id": self.config.id.to_string(),
"query": query,
"value": value,
"expected": expected_value,
"url": url,
})),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OracleEvent {
pub oracle_id: OracleId,
pub query: String,
pub value: OracleValue,
pub timestamp: DateTime<Utc>,
pub satisfied: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OracleRegistry {
oracles: HashMap<String, HTTPOracleConfig>,
}
impl OracleRegistry {
pub fn new() -> Self {
Self {
oracles: HashMap::new(),
}
}
pub fn register(&mut self, config: HTTPOracleConfig) {
self.oracles.insert(config.id.0.clone(), config);
}
pub fn get(&self, id: &str) -> Option<&HTTPOracleConfig> {
self.oracles.get(id)
}
pub fn list(&self) -> Vec<&HTTPOracleConfig> {
self.oracles.values().collect()
}
pub fn create_oracles(&self) -> Result<HashMap<String, HTTPOracle>> {
let mut oracles = HashMap::new();
for (id, config) in &self.oracles {
oracles.insert(id.clone(), HTTPOracle::new(config.clone())?);
}
Ok(oracles)
}
}
impl Default for OracleRegistry {
fn default() -> Self {
Self::new()
}
}
pub mod predefined {
use super::*;
pub fn bitcoin_price_oracle() -> HTTPOracleConfig {
HTTPOracleConfig {
id: OracleId::new("btc_price"),
base_url: "https://api.coingecko.com".to_string(),
api_key: None,
endpoint_template: "/api/v3/simple/price?ids=bitcoin&vs_currencies=usd".to_string(),
value_path: "$.bitcoin.usd".to_string(),
timeout_seconds: 10,
}
}
pub fn mock_oracle(id: &str) -> HTTPOracleConfig {
HTTPOracleConfig {
id: OracleId::new(id),
base_url: "http://localhost:9999".to_string(),
api_key: None,
endpoint_template: "/query/{}".to_string(),
value_path: "$.value".to_string(),
timeout_seconds: 5,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_oracle_value_conversion() {
assert_eq!(OracleValue::String("test".to_string()).to_comparison_string(), "test");
assert_eq!(OracleValue::Number(42.5).to_comparison_string(), "42.5");
assert_eq!(OracleValue::Boolean(true).to_comparison_string(), "true");
}
#[test]
fn test_oracle_registry() {
let mut registry = OracleRegistry::new();
registry.register(predefined::bitcoin_price_oracle());
assert!(registry.get("btc_price").is_some());
assert_eq!(registry.list().len(), 1);
}
#[test]
fn test_extract_value() {
let config = HTTPOracleConfig {
id: OracleId::new("test"),
base_url: "http://test".to_string(),
api_key: None,
endpoint_template: "/test".to_string(),
value_path: "$.data.price".to_string(),
timeout_seconds: 10,
};
let oracle = HTTPOracle::new(config).unwrap();
let json = serde_json::json!({
"data": {
"price": "50000"
}
});
let value = oracle.extract_value(&json).unwrap();
assert_eq!(value.to_comparison_string(), "50000");
}
}