1#![deny(missing_docs)]
33
34use serde_json::Value;
35use thiserror::Error;
36
37const DEFAULT_BASE_URL: &str = "https://noesisapi.dev";
38
39#[derive(Debug, Error)]
41pub enum Error {
42 #[error("HTTP error: {0}")]
44 Http(#[from] reqwest::Error),
45 #[error("Noesis API error {status}: {message}")]
48 Api {
49 status: u16,
51 message: String,
53 details: Option<Value>,
55 },
56 #[error("JSON error: {0}")]
58 Json(#[from] serde_json::Error),
59}
60
61pub type Result<T> = std::result::Result<T, Error>;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Chain {
67 Sol,
69 Base,
71}
72
73impl Chain {
74 fn as_str(self) -> &'static str {
75 match self {
76 Chain::Sol => "sol",
77 Chain::Base => "base",
78 }
79 }
80}
81
82impl Default for Chain {
83 fn default() -> Self { Chain::Sol }
84}
85
86#[allow(missing_docs)]
88#[derive(Debug, Clone, Copy)]
89pub enum TxType {
90 Swap, Transfer, NftSale, NftListing, CompressedNftMint, TokenMint, Unknown,
91}
92
93impl TxType {
94 fn as_str(self) -> &'static str {
95 match self {
96 TxType::Swap => "SWAP",
97 TxType::Transfer => "TRANSFER",
98 TxType::NftSale => "NFT_SALE",
99 TxType::NftListing => "NFT_LISTING",
100 TxType::CompressedNftMint => "COMPRESSED_NFT_MINT",
101 TxType::TokenMint => "TOKEN_MINT",
102 TxType::Unknown => "UNKNOWN",
103 }
104 }
105}
106
107#[allow(missing_docs)]
109#[derive(Debug, Clone, Copy)]
110pub enum TxSource {
111 Jupiter, Raydium, Orca, Meteora, PumpFun, SystemProgram, TokenProgram,
112}
113
114impl TxSource {
115 fn as_str(self) -> &'static str {
116 match self {
117 TxSource::Jupiter => "JUPITER",
118 TxSource::Raydium => "RAYDIUM",
119 TxSource::Orca => "ORCA",
120 TxSource::Meteora => "METEORA",
121 TxSource::PumpFun => "PUMP_FUN",
122 TxSource::SystemProgram => "SYSTEM_PROGRAM",
123 TxSource::TokenProgram => "TOKEN_PROGRAM",
124 }
125 }
126}
127
128#[derive(Debug, Default, Clone)]
130pub struct HistoryOptions {
131 pub chain: Option<Chain>,
133 pub limit: Option<u32>,
135 pub ty: Option<TxType>,
137 pub source: Option<TxSource>,
139 pub before: Option<String>,
141}
142
143#[derive(Debug, Default, Clone)]
145pub struct HoldersOptions {
146 pub chain: Option<Chain>,
148 pub limit: Option<u32>,
150 pub cursor: Option<String>,
152}
153
154#[derive(Debug, Default, Clone)]
156pub struct ConnectionsOptions {
157 pub min_sol: Option<f64>,
159 pub max_pages: Option<u32>,
161}
162
163#[derive(Clone)]
167pub struct Noesis {
168 http: reqwest::Client,
169 base_url: String,
170 api_key: String,
171}
172
173impl Noesis {
174 pub fn new(api_key: impl Into<String>) -> Self {
176 Self::with_base_url(api_key, DEFAULT_BASE_URL)
177 }
178
179 pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
182 Self {
183 http: reqwest::Client::new(),
184 base_url: base_url.into().trim_end_matches('/').to_string(),
185 api_key: api_key.into(),
186 }
187 }
188
189 async fn get(&self, path: &str, query: &[(&str, String)]) -> Result<Value> {
190 let url = format!("{}/api/v1{}", self.base_url, path);
191 let res = self.http.get(&url)
192 .header("X-API-Key", &self.api_key)
193 .query(query)
194 .send()
195 .await?;
196 Self::handle(res).await
197 }
198
199 async fn post(&self, path: &str, body: &Value) -> Result<Value> {
200 let url = format!("{}/api/v1{}", self.base_url, path);
201 let res = self.http.post(&url)
202 .header("X-API-Key", &self.api_key)
203 .json(body)
204 .send()
205 .await?;
206 Self::handle(res).await
207 }
208
209 async fn handle(res: reqwest::Response) -> Result<Value> {
210 let status = res.status();
211 if status.is_success() {
212 Ok(res.json().await?)
213 } else {
214 let details = res.json::<Value>().await.ok();
215 Err(Error::Api {
216 status: status.as_u16(),
217 message: format!("Noesis API error {}", status.as_u16()),
218 details,
219 })
220 }
221 }
222
223 pub async fn token_preview(&self, mint: &str) -> Result<Value> {
227 self.token_preview_on(mint, Chain::Sol).await
228 }
229
230 pub async fn token_preview_on(&self, mint: &str, chain: Chain) -> Result<Value> {
232 self.get(&format!("/token/{mint}/preview"), &[("chain", chain.as_str().into())]).await
233 }
234
235 pub async fn token_scan(&self, mint: &str) -> Result<Value> {
237 self.token_scan_on(mint, Chain::Sol).await
238 }
239
240 pub async fn token_scan_on(&self, mint: &str, chain: Chain) -> Result<Value> {
242 self.get(&format!("/token/{mint}/scan"), &[("chain", chain.as_str().into())]).await
243 }
244
245 pub async fn token_info(&self, mint: &str, chain: Chain) -> Result<Value> {
247 self.get(&format!("/token/{mint}/info"), &[("chain", chain.as_str().into())]).await
248 }
249
250 pub async fn token_top_holders(&self, mint: &str) -> Result<Value> {
252 self.get(&format!("/token/{mint}/top-holders"), &[]).await
253 }
254
255 pub async fn token_holders(&self, mint: &str, opts: HoldersOptions) -> Result<Value> {
257 let mut q: Vec<(&str, String)> = vec![
258 ("chain", opts.chain.unwrap_or_default().as_str().into()),
259 ];
260 if let Some(limit) = opts.limit { q.push(("limit", limit.to_string())); }
261 if let Some(cursor) = opts.cursor { q.push(("cursor", cursor)); }
262 self.get(&format!("/token/{mint}/holders"), &q).await
263 }
264
265 pub async fn token_bundles(&self, mint: &str) -> Result<Value> {
267 self.get(&format!("/token/{mint}/bundles"), &[]).await
268 }
269
270 pub async fn token_fresh_wallets(&self, mint: &str) -> Result<Value> {
272 self.get(&format!("/token/{mint}/fresh-wallets"), &[]).await
273 }
274
275 pub async fn token_team_supply(&self, mint: &str, chain: Chain) -> Result<Value> {
277 self.get(&format!("/token/{mint}/team-supply"), &[("chain", chain.as_str().into())]).await
278 }
279
280 pub async fn token_entry_price(&self, mint: &str, chain: Chain) -> Result<Value> {
282 self.get(&format!("/token/{mint}/entry-price"), &[("chain", chain.as_str().into())]).await
283 }
284
285 pub async fn token_dev_profile(&self, mint: &str) -> Result<Value> {
287 self.get(&format!("/token/{mint}/dev-profile"), &[]).await
288 }
289
290 pub async fn token_best_traders(&self, mint: &str) -> Result<Value> {
292 self.get(&format!("/token/{mint}/best-traders"), &[]).await
293 }
294
295 pub async fn token_early_buyers(&self, mint: &str, hours: f32) -> Result<Value> {
297 self.get(&format!("/token/{mint}/early-buyers"), &[("hours", hours.to_string())]).await
298 }
299
300 pub async fn wallet_profile(&self, addr: &str) -> Result<Value> {
304 self.get(&format!("/wallet/{addr}"), &[]).await
305 }
306
307 pub async fn wallet_history(&self, addr: &str, opts: HistoryOptions) -> Result<Value> {
309 let mut q: Vec<(&str, String)> = vec![
310 ("chain", opts.chain.unwrap_or_default().as_str().into()),
311 ];
312 if let Some(limit) = opts.limit { q.push(("limit", limit.to_string())); }
313 if let Some(ty) = opts.ty { q.push(("type", ty.as_str().into())); }
314 if let Some(source) = opts.source { q.push(("source", source.as_str().into())); }
315 if let Some(before) = opts.before { q.push(("before", before)); }
316 self.get(&format!("/wallet/{addr}/history"), &q).await
317 }
318
319 pub async fn wallet_connections(&self, addr: &str, opts: ConnectionsOptions) -> Result<Value> {
321 let mut q: Vec<(&str, String)> = vec![];
322 if let Some(min_sol) = opts.min_sol { q.push(("min_sol", min_sol.to_string())); }
323 if let Some(max_pages) = opts.max_pages { q.push(("max_pages", max_pages.to_string())); }
324 self.get(&format!("/wallet/{addr}/connections"), &q).await
325 }
326
327 pub async fn wallets_batch_identity(&self, addresses: &[String]) -> Result<Value> {
329 self.post("/wallets/batch-identity", &serde_json::json!({ "addresses": addresses })).await
330 }
331
332 pub async fn cross_holders(&self, tokens: &[String]) -> Result<Value> {
334 self.post("/tokens/cross-holders", &serde_json::json!({ "tokens": tokens })).await
335 }
336
337 pub async fn cross_traders(&self, tokens: &[String]) -> Result<Value> {
339 self.post("/tokens/cross-traders", &serde_json::json!({ "tokens": tokens })).await
340 }
341
342 pub async fn chain_status(&self) -> Result<Value> {
346 self.get("/chain/status", &[]).await
347 }
348
349 pub async fn account(&self, addr: &str) -> Result<Value> {
351 self.get(&format!("/account/{addr}"), &[]).await
352 }
353
354 pub async fn accounts_batch(&self, addresses: &[String]) -> Result<Value> {
356 self.post("/accounts/batch", &serde_json::json!({ "addresses": addresses })).await
357 }
358
359 pub async fn parse_transactions(&self, signatures: &[String]) -> Result<Value> {
361 self.post("/transactions/parse", &serde_json::json!({ "transactions": signatures })).await
362 }
363}