kora_lib/oracle/
jupiter.rs1use super::{PriceOracle, PriceSource, TokenPrice};
2use crate::error::KoraError;
3use reqwest::Client;
4use serde::Deserialize;
5use std::collections::HashMap;
6
7const JUPITER_API_URL: &str = "https://api.jup.ag/price/v2";
8
9#[derive(Debug, Deserialize)]
10#[allow(dead_code)]
11struct JupiterResponse {
12 data: HashMap<String, JupiterPriceData>,
13 #[serde(rename = "timeTaken")]
14 #[allow(dead_code)]
15 time_taken: f64,
16}
17
18#[derive(Debug, Deserialize)]
19#[allow(dead_code)]
20struct JupiterPriceData {
21 id: String,
22 #[serde(rename = "type")]
23 price_type: String,
24 price: String,
25}
26
27pub struct JupiterPriceOracle;
28
29#[async_trait::async_trait]
30impl PriceOracle for JupiterPriceOracle {
31 async fn get_price(
32 &self,
33 client: &Client,
34 mint_address: &str,
35 ) -> Result<TokenPrice, KoraError> {
36 let url = format!(
38 "{}/price?ids={}&vsToken=So11111111111111111111111111111111111111112",
39 std::env::var("JUPITER_API_URL").unwrap_or(JUPITER_API_URL.to_string()),
40 mint_address
41 );
42
43 let response = client
44 .get(&url)
45 .send()
46 .await
47 .map_err(|e| KoraError::RpcError(format!("Jupiter API request failed: {}", e)))?;
48
49 if !response.status().is_success() {
50 return Err(KoraError::RpcError(format!("Jupiter API error: {}", response.status())));
51 }
52
53 let jupiter_response: JupiterResponse = response
54 .json()
55 .await
56 .map_err(|e| KoraError::RpcError(format!("Failed to parse Jupiter response: {}", e)))?;
57
58 let price_data = jupiter_response
59 .data
60 .get(mint_address)
61 .ok_or_else(|| KoraError::RpcError("No price data from Jupiter".to_string()))?;
62
63 let price = price_data
65 .price
66 .parse::<f64>()
67 .map_err(|e| KoraError::RpcError(format!("Failed to parse price: {}", e)))?;
68
69 Ok(TokenPrice { price, confidence: 0.95, source: PriceSource::Jupiter })
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use mockito::{Matcher, Server};
77
78 #[tokio::test]
79 async fn test_jupiter_price_fetch() {
80 let mock_response = r#"{
81 "data": {
82 "So11111111111111111111111111111111111111112": {
83 "id": "So11111111111111111111111111111111111111112",
84 "type": "derivedPrice",
85 "price": "1"
86 },
87 "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN": {
88 "id": "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
89 "type": "derivedPrice",
90 "price": "0.005321503266927636"
91 }
92 },
93 "timeTaken": 0.003297425
94 }"#;
95 let mut server = Server::new_async().await;
96 let _m = server
97 .mock("GET", "/price")
98 .match_query(Matcher::Any)
99 .with_status(200)
100 .with_header("content-type", "application/json")
101 .with_body(mock_response)
102 .create();
103 let url = server.url();
104 std::env::set_var("JUPITER_API_URL", &url);
105
106 let client = Client::new();
107 let oracle = JupiterPriceOracle;
108 let result = oracle.get_price(&client, "So11111111111111111111111111111111111111112").await;
109
110 assert!(result.is_ok());
111 let price = result.unwrap();
112 assert_eq!(price.price, 1.0);
113 assert_eq!(price.source, PriceSource::Jupiter);
114 }
115}