kora_lib/oracle/
jupiter.rs

1use 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        // Get price in SOL using vsToken parameter
37        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        // Convert price from string to f64
64        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}