#![deny(missing_docs)]
use serde_json::Value;
use thiserror::Error;
const DEFAULT_BASE_URL: &str = "https://noesisapi.dev";
#[derive(Debug, Error)]
pub enum Error {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Noesis API error {status}: {message}")]
Api {
status: u16,
message: String,
details: Option<Value>,
},
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Chain {
Sol,
Base,
}
impl Chain {
fn as_str(self) -> &'static str {
match self {
Chain::Sol => "sol",
Chain::Base => "base",
}
}
}
impl Default for Chain {
fn default() -> Self { Chain::Sol }
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy)]
pub enum TxType {
Swap, Transfer, NftSale, NftListing, CompressedNftMint, TokenMint, Unknown,
}
impl TxType {
fn as_str(self) -> &'static str {
match self {
TxType::Swap => "SWAP",
TxType::Transfer => "TRANSFER",
TxType::NftSale => "NFT_SALE",
TxType::NftListing => "NFT_LISTING",
TxType::CompressedNftMint => "COMPRESSED_NFT_MINT",
TxType::TokenMint => "TOKEN_MINT",
TxType::Unknown => "UNKNOWN",
}
}
}
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy)]
pub enum TxSource {
Jupiter, Raydium, Orca, Meteora, PumpFun, SystemProgram, TokenProgram,
}
impl TxSource {
fn as_str(self) -> &'static str {
match self {
TxSource::Jupiter => "JUPITER",
TxSource::Raydium => "RAYDIUM",
TxSource::Orca => "ORCA",
TxSource::Meteora => "METEORA",
TxSource::PumpFun => "PUMP_FUN",
TxSource::SystemProgram => "SYSTEM_PROGRAM",
TxSource::TokenProgram => "TOKEN_PROGRAM",
}
}
}
#[derive(Debug, Default, Clone)]
pub struct HistoryOptions {
pub chain: Option<Chain>,
pub limit: Option<u32>,
pub ty: Option<TxType>,
pub source: Option<TxSource>,
pub before: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct HoldersOptions {
pub chain: Option<Chain>,
pub limit: Option<u32>,
pub cursor: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct ConnectionsOptions {
pub min_sol: Option<f64>,
pub max_pages: Option<u32>,
}
#[derive(Clone)]
pub struct Noesis {
http: reqwest::Client,
base_url: String,
api_key: String,
}
impl Noesis {
pub fn new(api_key: impl Into<String>) -> Self {
Self::with_base_url(api_key, DEFAULT_BASE_URL)
}
pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
http: reqwest::Client::new(),
base_url: base_url.into().trim_end_matches('/').to_string(),
api_key: api_key.into(),
}
}
async fn get(&self, path: &str, query: &[(&str, String)]) -> Result<Value> {
let url = format!("{}/api/v1{}", self.base_url, path);
let res = self.http.get(&url)
.header("X-API-Key", &self.api_key)
.query(query)
.send()
.await?;
Self::handle(res).await
}
async fn post(&self, path: &str, body: &Value) -> Result<Value> {
let url = format!("{}/api/v1{}", self.base_url, path);
let res = self.http.post(&url)
.header("X-API-Key", &self.api_key)
.json(body)
.send()
.await?;
Self::handle(res).await
}
async fn handle(res: reqwest::Response) -> Result<Value> {
let status = res.status();
if status.is_success() {
Ok(res.json().await?)
} else {
let details = res.json::<Value>().await.ok();
Err(Error::Api {
status: status.as_u16(),
message: format!("Noesis API error {}", status.as_u16()),
details,
})
}
}
pub async fn token_preview(&self, mint: &str) -> Result<Value> {
self.token_preview_on(mint, Chain::Sol).await
}
pub async fn token_preview_on(&self, mint: &str, chain: Chain) -> Result<Value> {
self.get(&format!("/token/{mint}/preview"), &[("chain", chain.as_str().into())]).await
}
pub async fn token_scan(&self, mint: &str) -> Result<Value> {
self.token_scan_on(mint, Chain::Sol).await
}
pub async fn token_scan_on(&self, mint: &str, chain: Chain) -> Result<Value> {
self.get(&format!("/token/{mint}/scan"), &[("chain", chain.as_str().into())]).await
}
pub async fn token_info(&self, mint: &str, chain: Chain) -> Result<Value> {
self.get(&format!("/token/{mint}/info"), &[("chain", chain.as_str().into())]).await
}
pub async fn token_top_holders(&self, mint: &str) -> Result<Value> {
self.get(&format!("/token/{mint}/top-holders"), &[]).await
}
pub async fn token_holders(&self, mint: &str, opts: HoldersOptions) -> Result<Value> {
let mut q: Vec<(&str, String)> = vec![
("chain", opts.chain.unwrap_or_default().as_str().into()),
];
if let Some(limit) = opts.limit { q.push(("limit", limit.to_string())); }
if let Some(cursor) = opts.cursor { q.push(("cursor", cursor)); }
self.get(&format!("/token/{mint}/holders"), &q).await
}
pub async fn token_bundles(&self, mint: &str) -> Result<Value> {
self.get(&format!("/token/{mint}/bundles"), &[]).await
}
pub async fn token_fresh_wallets(&self, mint: &str) -> Result<Value> {
self.get(&format!("/token/{mint}/fresh-wallets"), &[]).await
}
pub async fn token_team_supply(&self, mint: &str, chain: Chain) -> Result<Value> {
self.get(&format!("/token/{mint}/team-supply"), &[("chain", chain.as_str().into())]).await
}
pub async fn token_entry_price(&self, mint: &str, chain: Chain) -> Result<Value> {
self.get(&format!("/token/{mint}/entry-price"), &[("chain", chain.as_str().into())]).await
}
pub async fn token_dev_profile(&self, mint: &str) -> Result<Value> {
self.get(&format!("/token/{mint}/dev-profile"), &[]).await
}
pub async fn token_best_traders(&self, mint: &str) -> Result<Value> {
self.get(&format!("/token/{mint}/best-traders"), &[]).await
}
pub async fn token_early_buyers(&self, mint: &str, hours: f32) -> Result<Value> {
self.get(&format!("/token/{mint}/early-buyers"), &[("hours", hours.to_string())]).await
}
pub async fn wallet_profile(&self, addr: &str) -> Result<Value> {
self.get(&format!("/wallet/{addr}"), &[]).await
}
pub async fn wallet_history(&self, addr: &str, opts: HistoryOptions) -> Result<Value> {
let mut q: Vec<(&str, String)> = vec![
("chain", opts.chain.unwrap_or_default().as_str().into()),
];
if let Some(limit) = opts.limit { q.push(("limit", limit.to_string())); }
if let Some(ty) = opts.ty { q.push(("type", ty.as_str().into())); }
if let Some(source) = opts.source { q.push(("source", source.as_str().into())); }
if let Some(before) = opts.before { q.push(("before", before)); }
self.get(&format!("/wallet/{addr}/history"), &q).await
}
pub async fn wallet_connections(&self, addr: &str, opts: ConnectionsOptions) -> Result<Value> {
let mut q: Vec<(&str, String)> = vec![];
if let Some(min_sol) = opts.min_sol { q.push(("min_sol", min_sol.to_string())); }
if let Some(max_pages) = opts.max_pages { q.push(("max_pages", max_pages.to_string())); }
self.get(&format!("/wallet/{addr}/connections"), &q).await
}
pub async fn wallets_batch_identity(&self, addresses: &[String]) -> Result<Value> {
self.post("/wallets/batch-identity", &serde_json::json!({ "addresses": addresses })).await
}
pub async fn cross_holders(&self, tokens: &[String]) -> Result<Value> {
self.post("/tokens/cross-holders", &serde_json::json!({ "tokens": tokens })).await
}
pub async fn cross_traders(&self, tokens: &[String]) -> Result<Value> {
self.post("/tokens/cross-traders", &serde_json::json!({ "tokens": tokens })).await
}
pub async fn chain_status(&self) -> Result<Value> {
self.get("/chain/status", &[]).await
}
pub async fn account(&self, addr: &str) -> Result<Value> {
self.get(&format!("/account/{addr}"), &[]).await
}
pub async fn accounts_batch(&self, addresses: &[String]) -> Result<Value> {
self.post("/accounts/batch", &serde_json::json!({ "addresses": addresses })).await
}
pub async fn parse_transactions(&self, signatures: &[String]) -> Result<Value> {
self.post("/transactions/parse", &serde_json::json!({ "transactions": signatures })).await
}
}