orca_config_api_parser/
lib.rs

1//! providers helper functions for parsing Orca's configuration api
2//! and generating rust types corresponding to the emitted JSON
3
4use anyhow::{anyhow, Result};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8pub const ORCA_CONFIGS_API: &str = "https://api.orca.so/configs";
9
10/// the resposne body from orca's configuration api
11#[derive(Clone, Debug, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct OrcaConfigsApiResponse {
14    pub aquafarms: HashMap<String, AquaFarm>,
15    pub collectibles: HashMap<String, Collectible>,
16    pub double_dips: HashMap<String, DoubleDip>,
17    pub pools: HashMap<String, Pool>,
18    pub program_ids: ProgramIds,
19    pub tokens: HashMap<String, Token>,
20    pub coingecko_ids: HashMap<String, String>,
21    pub ftx_ids: HashMap<String, String>,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct AquaFarm {
27    pub account: String,
28    pub nonce: u8,
29    pub token_program_id: String,
30    pub emissions_authority: String,
31    pub remove_rewards_authority: String,
32    pub base_token_mint: String,
33    pub base_token_vault: String,
34    pub reward_token_mint: String,
35    pub reward_token_vault: String,
36    pub farm_token_mint: String,
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct DoubleDip {
42    pub account: String,
43    pub nonce: u8,
44    pub token_program_id: String,
45    pub emissions_authority: String,
46    pub remove_rewards_authority: String,
47    pub base_token_mint: String,
48    pub base_token_vault: String,
49    pub reward_token_mint: String,
50    pub reward_token_vault: String,
51    pub farm_token_mint: String,
52    pub date_start: String,
53    pub date_end: String,
54    pub total_emissions: String,
55    pub custom_gradient_start_color: String,
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct Collectible {
61    pub mint: String,
62    pub decimals: u8,
63}
64
65#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
66#[serde(rename_all = "camelCase")]
67pub struct Pool {
68    pub account: String,
69    pub authority: String,
70    pub nonce: u8,
71    pub pool_token_mint: String,
72    pub token_account_a: String,
73    pub token_account_b: String,
74    pub fee_account: String,
75    pub fee_numerator: u64,
76    pub fee_denominator: u64,
77    pub owner_trade_fee_numerator: u64,
78    pub owner_trade_fee_denominator: u64,
79    pub owner_withdraw_fee_numerator: u64,
80    pub host_fee_numerator: u64,
81    pub token_a_name: String,
82    pub token_b_name: String,
83    pub curve_type: String,
84    pub deprecated: Option<bool>,
85    pub program_version: Option<u64>,
86}
87
88#[derive(Clone, Debug, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct ProgramIds {
91    pub serum_token_swap: String,
92    pub token_swap_v2: String,
93    pub token_swap: String,
94    pub token: String,
95    pub aquafarm: String,
96}
97
98#[derive(Clone, Debug, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct Token {
101    pub mint: String,
102    pub name: String,
103    pub decimals: u8,
104    pub fetch_price: Option<bool>,
105}
106
107impl OrcaConfigsApiResponse {
108    pub async fn fetch_orca_config() -> Result<Self> {
109        let client = reqwest::Client::builder().build()?;
110        let res = client.get(ORCA_CONFIGS_API).send().await?;
111        let data = res.json::<Self>().await?;
112        Ok(data)
113    }
114    /// used to lookup pool information for the given pool matching `name`
115    /// if aquafarm is true, search query becomes `name[aquafarm]`
116    /// if stable is true, search query becomes `name[stable]`
117    /// if stable & aquafarm is true, search query becomes `name[stable][aquafarm]`
118    pub fn find_pool(&self, name: &str, stable: bool, aquafarm: bool) -> Result<Pool> {
119        let name = format_orca_amm_name(name, stable, aquafarm);
120        for pool in self.pools.iter() {
121            if pool.0.eq(&name) {
122                return Ok(pool.1.clone());
123            }
124        }
125        Err(anyhow!("failed to find pool for {}", name))
126    }
127    /// used to lookup information for an aquafarm, returning a tuple of the
128    /// pool and aquafarm config
129    pub fn find_aquafarm(&self, name: &str, stable: bool) -> Result<(Pool, AquaFarm)> {
130        let pool = self.find_pool(name, stable, true)?;
131        for farm in self.aquafarms.iter() {
132            if farm.0.eq(&pool.account) {
133                return Ok((pool, farm.1.clone()));
134            }
135        }
136        Err(anyhow!("failed to find aquafarm for {}", name))
137    }
138    /// used to lookup information for a double dip
139    pub fn find_double_dip(&self, name: &str, stable: bool) -> Result<(Pool, DoubleDip, AquaFarm)> {
140        let pool = self.find_pool(name, stable, true)?;
141        for doubledip in self.double_dips.iter() {
142            if doubledip.0.eq(&pool.account) {
143                for aquafarm in self.aquafarms.iter() {
144                    if aquafarm.0.eq(&pool.account) {
145                        return Ok((pool, doubledip.1.clone(), aquafarm.1.clone()));
146                    }
147                }
148            }
149        }
150        Err(anyhow!("failed to find doubledip for {}", name))
151    }
152}
153
154pub fn format_orca_amm_name(name: &str, stable: bool, aquafarm: bool) -> String {
155    // orca uses a platform specifier for pairs with names that would conflict
156    // with other vaults
157    let name = if name.split('-').count() == 3 {
158        let lp_name_str = name.to_string();
159        let parts: Vec<_> = lp_name_str.split('-').collect();
160        let mut lp_name_parsed = String::with_capacity(name.len() - 5); // 5 for '-ORCA'
161        for (idx, part) in parts.iter().enumerate() {
162            if idx == parts.len() - 1 {
163                break;
164            }
165            lp_name_parsed.push_str(*part);
166            if idx != parts.len() - 2 {
167                lp_name_parsed.push('/');
168            }
169        }
170        lp_name_parsed
171    } else {
172        name.replace("-", "/")
173    };
174    // scnSOL previously used to be labeled as SOCN
175    // so handle that edgecase, todo(bonedaddy): test
176    let name = name.replace("SOCN", "scnSOL");
177    let name = if stable && !name.contains("[stable]") {
178        format!("{}[stable]", name)
179    } else {
180        name
181    };
182    let name = if aquafarm && !name.contains("[aquafarm]") {
183        format!("{}[aquafarm]", name)
184    } else {
185        name
186    };
187    name
188}
189
190#[cfg(test)]
191mod test {
192    use super::*;
193    #[test]
194    fn test_format_orca_amm_name() {
195        let name_one = "SAMO-USDC[stable][aquafarm]".to_string();
196        let name_two = "SAMO-USDC".to_string();
197        assert_eq!(
198            format_orca_amm_name(&name_one, true, true),
199            "SAMO/USDC[stable][aquafarm]"
200        );
201        assert_eq!(
202            format_orca_amm_name(&name_two, false, true),
203            "SAMO/USDC[aquafarm]"
204        );
205        assert_eq!(
206            format_orca_amm_name(&name_two, true, false),
207            "SAMO/USDC[stable]"
208        );
209    }
210    #[tokio::test]
211    async fn test_orca_config() {
212        let orca_config = OrcaConfigsApiResponse::fetch_orca_config().await.unwrap();
213
214        let pool_config = orca_config
215            .find_pool(&"SOL/USDC".to_string(), false, false)
216            .unwrap();
217        assert_eq!(
218            pool_config.account,
219            "6fTRDD7sYxCN7oyoSQaN1AWC3P2m8A6gVZzGrpej9DvL".to_string()
220        );
221
222        let pool_config = orca_config
223            .find_pool(&"SOL/USDC".to_string(), false, true)
224            .unwrap();
225        assert_eq!(
226            pool_config.account,
227            "EGZ7tiLeH62TPV1gL8WwbXGzEPa9zmcpVnnkPKKnrE2U".to_string()
228        );
229
230        let aquafarm_config = orca_config
231            .find_aquafarm(&"SOL/USDC".to_string(), false)
232            .unwrap();
233        assert_eq!(aquafarm_config.0, pool_config);
234        println!(
235            "sol/usdc aquafarm information\npool {:#?}\nfarm {:#?}",
236            aquafarm_config.0, aquafarm_config.1
237        );
238
239        let pool_config = orca_config
240            .find_pool(&"LIQ/USDC".to_string(), false, true)
241            .unwrap();
242        let doubledip_config = orca_config
243            .find_double_dip(&"LIQ/USDC".to_string(), false)
244            .unwrap();
245        assert_eq!(doubledip_config.0, pool_config);
246        println!(
247            "liq/usdc doubledip information\npool {:#?}\nfarm {:#?}\ndouble dip {:#?}",
248            doubledip_config.0, doubledip_config.1, doubledip_config.0,
249        );
250    }
251}