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, Low, Medium, High, VeryHigh, UnsafeMax, Default, }
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 }
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}