Skip to main content

spo_helius/
lib.rs

1use anyhow::{anyhow, bail, ensure};
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4use serde_with::skip_serializing_none;
5use std::sync::atomic::AtomicU64;
6
7pub use reqwest::Client as HttpClient;
8
9#[derive(Debug)]
10pub struct Helius {
11    client: HttpClient,
12    mainnet_url: String,
13    devnet_url: String,
14    id: AtomicU64,
15}
16
17pub fn is_pubkey(s: &str) -> Result<&str, anyhow::Error> {
18    let mut buf = [0u8; 32];
19    let written = bs58::decode(s).into(&mut buf)?;
20    ensure!(written == buf.len(), "invalid pubkey");
21    Ok(s)
22}
23
24#[skip_serializing_none]
25#[derive(Serialize, Debug, Default)]
26#[serde(rename_all = "camelCase")]
27pub struct GetPriorityFeeEstimateRequest {
28    pub transaction: Option<String>,
29    pub account_keys: Option<Vec<String>>,
30    pub options: Option<GetPriorityFeeEstimateOptions>,
31}
32
33#[skip_serializing_none]
34#[derive(Serialize, Debug, Default)]
35#[serde(rename_all = "camelCase")]
36pub struct GetPriorityFeeEstimateOptions {
37    pub priority_level: Option<PriorityLevel>,
38    pub include_all_priority_fee_levels: Option<bool>,
39    pub transaction_encoding: Option<String>,
40    pub lookback_slots: Option<u8>,
41}
42
43#[derive(Serialize, Debug)]
44pub enum PriorityLevel {
45    None,     // 0th percentile
46    Low,      // 25th percentile
47    Medium,   // 50th percentile
48    High,     // 75th percentile
49    VeryHigh, // 95th percentile
50    // labelled unsafe to prevent people using and draining their funds by accident
51    UnsafeMax, // 100th percentile
52    Default,   // 50th percentile
53}
54
55#[derive(Deserialize, Debug, Clone)]
56#[serde(rename_all = "camelCase")]
57pub struct GetPriorityFeeEstimateResponse {
58    pub priority_fee_estimate: Option<f64>,
59    pub priority_fee_levels: Option<MicroLamportPriorityFeeLevels>,
60}
61
62#[derive(Deserialize, Debug, Clone)]
63#[serde(rename_all = "camelCase")]
64pub struct MicroLamportPriorityFeeLevels {
65    pub none: f64,
66    pub low: f64,
67    pub medium: f64,
68    pub high: f64,
69    pub very_high: f64,
70    pub unsafe_max: f64,
71}
72
73impl Helius {
74    pub fn new(client: HttpClient, apikey: &str) -> Self {
75        Self {
76            client,
77            mainnet_url: format!("https://mainnet.helius-rpc.com/?api-key={apikey}"),
78            devnet_url: format!("https://devnet.helius-rpc.com/?api-key={apikey}"),
79            id: AtomicU64::new(0),
80        }
81    }
82
83    fn next_id(&self) -> String {
84        self.id
85            .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
86            .to_string()
87    }
88
89    pub fn get_url(&self, solana_net: &str) -> Result<&str, anyhow::Error> {
90        match solana_net {
91            "devnet" => Ok(&self.devnet_url),
92            "mainnet" | "mainnet-beta" => Ok(&self.mainnet_url),
93            _ => bail!("unknown solana_net: {}", solana_net),
94        }
95    }
96
97    pub async fn get_priority_fee_estimate(
98        &self,
99        solana_net: &str,
100        req: GetPriorityFeeEstimateRequest,
101    ) -> Result<GetPriorityFeeEstimateResponse, anyhow::Error> {
102        let req = serde_json::json!({
103            "jsonrpc": "2.0",
104            "id": self.next_id(),
105            "method": "getPriorityFeeEstimate",
106            "params": [req],
107        });
108
109        #[derive(Deserialize)]
110        struct HeliusResponse {
111            result: GetPriorityFeeEstimateResponse,
112        }
113
114        #[derive(Deserialize)]
115        struct ErrorBody {
116            message: String,
117        }
118
119        #[derive(Deserialize)]
120        struct HeliusError {
121            error: ErrorBody,
122        }
123
124        let url = self.get_url(solana_net)?;
125        let resp = self.client.post(url).json(&req).send().await?;
126        if resp.status().is_success() {
127            Ok(resp.json::<HeliusResponse>().await?.result)
128        } else {
129            Err(anyhow!(resp.json::<HeliusError>().await?.error.message))
130        }
131    }
132
133    pub async fn get_assets_by_group(
134        &self,
135        solana_net: &str,
136        collection: &str,
137    ) -> Result<Vec<JsonValue>, anyhow::Error> {
138        #[derive(Deserialize)]
139        struct HeliusResult {
140            items: Vec<JsonValue>,
141            total: u64,
142            // #[serde(flatten)]
143            // extra: JsonValue,
144        }
145
146        #[derive(Deserialize)]
147        struct HeliusResponse {
148            result: HeliusResult,
149        }
150
151        const LIMIT: u64 = 1000;
152
153        is_pubkey(collection)?;
154        let url = self.get_url(solana_net)?;
155
156        let mut page = 1;
157        let mut assets = Vec::new();
158
159        let mut req = serde_json::json!(
160            {
161                "jsonrpc": "2.0",
162                "id": "",
163                "method": "getAssetsByGroup",
164                "params": {
165                    "groupKey": "collection",
166                    "groupValue": collection,
167                    "page": 1,
168                    "limit": LIMIT,
169                    "sortBy": {
170                        "sortBy": "created"
171                    },
172                    "displayOptions": {
173                        "showUnverifiedCollections": false,
174                        "showCollectionMetadata": false,
175                        "showGrandTotal": false,
176                        "showInscription": false,
177                    }
178                }
179            }
180        );
181
182        loop {
183            req["id"] = JsonValue::from(self.next_id());
184            req["params"]["page"] = JsonValue::from(page);
185            let resp = self
186                .client
187                .post(url)
188                .json(&req)
189                .send()
190                .await?
191                .error_for_status()?
192                .json::<HeliusResponse>()
193                .await?;
194
195            assets.extend(resp.result.items);
196            if resp.result.total < LIMIT {
197                break;
198            } else {
199                page += 1;
200            }
201        }
202
203        Ok(assets)
204    }
205
206    pub async fn get_asset(
207        &self,
208        solana_net: &str,
209        mint_account: &str,
210    ) -> Result<JsonValue, anyhow::Error> {
211        #[derive(Deserialize)]
212        struct HeliusResponse {
213            result: JsonValue,
214        }
215
216        is_pubkey(mint_account)?;
217        let url = self.get_url(solana_net)?;
218
219        let req = serde_json::json!(
220            {
221                "jsonrpc": "2.0",
222                "id": self.next_id(),
223                "method": "getAsset",
224                "params": {
225                    "id": mint_account,
226                }
227            }
228        );
229
230        let resp = self
231            .client
232            .post(url)
233            .json(&req)
234            .send()
235            .await?
236            .error_for_status()?
237            .json::<HeliusResponse>()
238            .await?;
239
240        Ok(resp.result)
241    }
242}