use anyhow::{anyhow, bail, ensure};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use serde_with::skip_serializing_none;
use std::sync::atomic::AtomicU64;
pub use reqwest::Client as HttpClient;
#[derive(Debug)]
pub struct Helius {
client: HttpClient,
mainnet_url: String,
devnet_url: String,
id: AtomicU64,
}
pub fn is_pubkey(s: &str) -> Result<&str, anyhow::Error> {
let mut buf = [0u8; 32];
let written = bs58::decode(s).into(&mut buf)?;
ensure!(written == buf.len(), "invalid pubkey");
Ok(s)
}
#[skip_serializing_none]
#[derive(Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct GetPriorityFeeEstimateRequest {
pub transaction: Option<String>,
pub account_keys: Option<Vec<String>>,
pub options: Option<GetPriorityFeeEstimateOptions>,
}
#[skip_serializing_none]
#[derive(Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct GetPriorityFeeEstimateOptions {
pub priority_level: Option<PriorityLevel>,
pub include_all_priority_fee_levels: Option<bool>,
pub transaction_encoding: Option<String>,
pub lookback_slots: Option<u8>,
}
#[derive(Serialize, Debug)]
pub enum PriorityLevel {
None, Low, Medium, High, VeryHigh, UnsafeMax, Default, }
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct GetPriorityFeeEstimateResponse {
pub priority_fee_estimate: Option<f64>,
pub priority_fee_levels: Option<MicroLamportPriorityFeeLevels>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct MicroLamportPriorityFeeLevels {
pub none: f64,
pub low: f64,
pub medium: f64,
pub high: f64,
pub very_high: f64,
pub unsafe_max: f64,
}
impl Helius {
pub fn new(client: HttpClient, apikey: &str) -> Self {
Self {
client,
mainnet_url: format!("https://mainnet.helius-rpc.com/?api-key={apikey}"),
devnet_url: format!("https://devnet.helius-rpc.com/?api-key={apikey}"),
id: AtomicU64::new(0),
}
}
fn next_id(&self) -> String {
self.id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
.to_string()
}
pub fn get_url(&self, solana_net: &str) -> Result<&str, anyhow::Error> {
match solana_net {
"devnet" => Ok(&self.devnet_url),
"mainnet" | "mainnet-beta" => Ok(&self.mainnet_url),
_ => bail!("unknown solana_net: {}", solana_net),
}
}
pub async fn get_priority_fee_estimate(
&self,
solana_net: &str,
req: GetPriorityFeeEstimateRequest,
) -> Result<GetPriorityFeeEstimateResponse, anyhow::Error> {
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": self.next_id(),
"method": "getPriorityFeeEstimate",
"params": [req],
});
#[derive(Deserialize)]
struct HeliusResponse {
result: GetPriorityFeeEstimateResponse,
}
#[derive(Deserialize)]
struct ErrorBody {
message: String,
}
#[derive(Deserialize)]
struct HeliusError {
error: ErrorBody,
}
let url = self.get_url(solana_net)?;
let resp = self.client.post(url).json(&req).send().await?;
if resp.status().is_success() {
Ok(resp.json::<HeliusResponse>().await?.result)
} else {
Err(anyhow!(resp.json::<HeliusError>().await?.error.message))
}
}
pub async fn get_assets_by_group(
&self,
solana_net: &str,
collection: &str,
) -> Result<Vec<JsonValue>, anyhow::Error> {
#[derive(Deserialize)]
struct HeliusResult {
items: Vec<JsonValue>,
total: u64,
}
#[derive(Deserialize)]
struct HeliusResponse {
result: HeliusResult,
}
const LIMIT: u64 = 1000;
is_pubkey(collection)?;
let url = self.get_url(solana_net)?;
let mut page = 1;
let mut assets = Vec::new();
let mut req = serde_json::json!(
{
"jsonrpc": "2.0",
"id": "",
"method": "getAssetsByGroup",
"params": {
"groupKey": "collection",
"groupValue": collection,
"page": 1,
"limit": LIMIT,
"sortBy": {
"sortBy": "created"
},
"displayOptions": {
"showUnverifiedCollections": false,
"showCollectionMetadata": false,
"showGrandTotal": false,
"showInscription": false,
}
}
}
);
loop {
req["id"] = JsonValue::from(self.next_id());
req["params"]["page"] = JsonValue::from(page);
let resp = self
.client
.post(url)
.json(&req)
.send()
.await?
.error_for_status()?
.json::<HeliusResponse>()
.await?;
assets.extend(resp.result.items);
if resp.result.total < LIMIT {
break;
} else {
page += 1;
}
}
Ok(assets)
}
pub async fn get_asset(
&self,
solana_net: &str,
mint_account: &str,
) -> Result<JsonValue, anyhow::Error> {
#[derive(Deserialize)]
struct HeliusResponse {
result: JsonValue,
}
is_pubkey(mint_account)?;
let url = self.get_url(solana_net)?;
let req = serde_json::json!(
{
"jsonrpc": "2.0",
"id": self.next_id(),
"method": "getAsset",
"params": {
"id": mint_account,
}
}
);
let resp = self
.client
.post(url)
.json(&req)
.send()
.await?
.error_for_status()?
.json::<HeliusResponse>()
.await?;
Ok(resp.result)
}
}