1use reqwest::{
2 header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE},
3 Client,
4};
5use solana_sdk::pubkey::Pubkey;
6
7const BASE_URL: &str = "https://public-api.birdeye.so";
8
9#[derive(Debug, Clone)]
10pub struct BirdeyeClient {
11 api_key: String,
12 client: Client,
13}
14
15impl BirdeyeClient {
16 pub fn new(api_key: String) -> Self {
17 Self {
18 api_key,
19 client: Client::new(),
20 }
21 }
22
23 fn get_headers(&self) -> HeaderMap {
24 let mut headers = HeaderMap::new();
25 headers.insert("X-API-KEY", HeaderValue::from_str(&self.api_key).unwrap());
26 headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
27 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
28 headers
29 }
30
31 async fn make_request(&self, endpoint: &str) -> Result<String, String> {
32 let url = format!("{}{}", BASE_URL, endpoint);
33 println!("Making request to {}", url);
34 let response = self
35 .client
36 .get(&url)
37 .headers(self.get_headers())
38 .send()
39 .await
40 .map_err(|e| e.to_string())?;
41
42 if response.status().is_success() {
43 response.text().await.map_err(|e| e.to_string())
44 } else {
45 Err(format!("Request failed with status: {}", response.status()))
46 }
47 }
48
49 fn format_resolution(resolution: String) -> String {
50 if resolution.chars().all(|c| c.is_numeric()) {
52 format!("{}m", resolution)
53 } else {
54 resolution
55 }
56 }
57
58 fn validate_solana_address(address: &str) -> Result<Pubkey, String> {
59 address
60 .parse::<Pubkey>()
61 .map_err(|e| format!("Invalid Solana address: {}", e))
62 }
63
64 pub async fn get_token_price(&self, address: String) -> Result<String, String> {
65 let pubkey = Self::validate_solana_address(&address)?;
66 self.make_request(&format!("/defi/price?address={}", pubkey.to_string()))
67 .await
68 }
69
70 pub async fn get_token_price_history(
71 &self,
72 address: String,
73 resolution: String,
74 time_from: Option<i64>,
75 time_to: Option<i64>,
76 limit: Option<i32>,
77 ) -> Result<String, String> {
78 let pubkey = Self::validate_solana_address(&address)?;
79 let formatted_resolution = Self::format_resolution(resolution);
80 let mut endpoint = format!(
81 "/defi/history_price?address={}&address_type=token&type={}",
82 pubkey.to_string(),
83 formatted_resolution
84 );
85
86 if let Some(from) = time_from {
87 endpoint.push_str(&format!("&time_from={}", from));
88 }
89 if let Some(to) = time_to {
90 endpoint.push_str(&format!("&time_to={}", to));
91 }
92 if let Some(limit) = limit {
93 endpoint.push_str(&format!("&limit={}", limit));
94 }
95 self.make_request(&endpoint).await
96 }
97
98 pub async fn get_multi_token_price(&self, addresses: String) -> Result<String, String> {
99 let pubkeys: Result<Vec<Pubkey>, String> = addresses
100 .split(',')
101 .map(|addr| Self::validate_solana_address(addr.trim()))
102 .collect();
103 let pubkeys = pubkeys?;
104
105 let formatted_addresses = pubkeys
106 .iter()
107 .map(|pubkey| pubkey.to_string())
108 .collect::<Vec<String>>()
109 .join(",");
110
111 self.make_request(&format!(
112 "/defi/multi_price?list_address={}",
113 formatted_addresses
114 ))
115 .await
116 }
117
118 pub async fn get_token_trending(&self, limit: Option<i32>) -> Result<String, String> {
119 let mut endpoint = "/defi/token_trending".to_string();
120 if let Some(limit) = limit {
121 endpoint.push_str(&format!("?limit={}", limit));
122 }
123 self.make_request(&endpoint).await
124 }
125
126 pub async fn get_token_ohlcv(
127 &self,
128 address: String,
129 resolution: String,
130 time_from: i64,
131 time_to: i64,
132 ) -> Result<String, String> {
133 let pubkey = Self::validate_solana_address(&address)?;
134 let formatted_resolution = Self::format_resolution(resolution);
135 self.make_request(&format!(
136 "/defi/ohlcv?address={}&type={}&time_from={}&time_to={}",
137 pubkey.to_string(),
138 formatted_resolution,
139 time_from,
140 time_to
141 ))
142 .await
143 }
144
145 pub async fn get_pair_ohlcv(
146 &self,
147 pair_address: String,
148 resolution: String,
149 time_from: i64,
150 time_to: i64,
151 ) -> Result<String, String> {
152 let pubkey = Self::validate_solana_address(&pair_address)?;
153 let formatted_resolution = Self::format_resolution(resolution);
154 self.make_request(&format!(
155 "/defi/ohlcv/pair?address={}&type={}&time_from={}&time_to={}",
156 pubkey.to_string(),
157 formatted_resolution,
158 time_from,
159 time_to
160 ))
161 .await
162 }
163
164 pub async fn get_token_trades(
165 &self,
166 address: String,
167 limit: Option<i32>,
168 offset: Option<i32>,
169 ) -> Result<String, String> {
170 let pubkey = Self::validate_solana_address(&address)?;
171 println!("Pubkey: {:?}", pubkey);
172 let mut endpoint = format!(
173 "/defi/txs/token?address={}&sort_type=desc",
174 pubkey.to_string()
175 );
176 if let Some(limit) = limit {
177 endpoint.push_str(&format!("&limit={}", limit));
178 }
179 if let Some(offset) = offset {
180 endpoint.push_str(&format!("&offset={}", offset));
181 }
182 self.make_request(&endpoint).await
183 }
184
185 pub async fn get_pair_trades(
186 &self,
187 pair_address: String,
188 limit: Option<i32>,
189 offset: Option<i32>,
190 ) -> Result<String, String> {
191 let pubkey = Self::validate_solana_address(&pair_address)?;
192 println!("Pubkey: {:?}", pubkey);
193 let mut endpoint = format!(
194 "/defi/txs/pair?address={}&tx_type=swap&sort_type=desc",
195 pubkey.to_string()
196 );
197 if let Some(limit) = limit {
198 if limit >= 50 {
199 endpoint.push_str("&limit=50");
200 } else {
201 endpoint.push_str(&format!("&limit={}", limit));
202 }
203 }
204 if let Some(offset) = offset {
205 endpoint.push_str(&format!("&offset={}", offset));
206 }
207 self.make_request(&endpoint).await
208 }
209
210 pub async fn get_token_overview(&self, address: String) -> Result<String, String> {
211 let pubkey = Self::validate_solana_address(&address)?;
212 self.make_request(&format!(
213 "/defi/token_overview?address={}",
214 pubkey.to_string()
215 ))
216 .await
217 }
218
219 pub async fn get_token_list(
220 &self,
221 limit: Option<i32>,
222 offset: Option<i32>,
223 ) -> Result<String, String> {
224 let mut endpoint = "/defi/tokenList".to_string();
225 let mut has_param = false;
226 if let Some(limit) = limit {
227 endpoint.push_str(&format!("?limit={}", limit));
228 has_param = true;
229 }
230 if let Some(offset) = offset {
231 endpoint.push_str(&format!(
232 "{}offset={}",
233 if has_param { "&" } else { "?" },
234 offset
235 ));
236 }
237 self.make_request(&endpoint).await
238 }
239
240 pub async fn get_token_security(&self, address: String) -> Result<String, String> {
241 let pubkey = Self::validate_solana_address(&address)?;
242 self.make_request(&format!(
243 "/defi/token_security?address={}",
244 pubkey.to_string()
245 ))
246 .await
247 }
248
249 pub async fn get_token_market_list(&self, address: String) -> Result<String, String> {
250 let pubkey = Self::validate_solana_address(&address)?;
251 self.make_request(&format!("/defi/v2/markets?address={}", pubkey.to_string()))
252 .await
253 }
254
255 pub async fn get_token_new_listing(
256 &self,
257 limit: Option<i32>,
258 offset: Option<i32>,
259 ) -> Result<String, String> {
260 let mut endpoint = "/defi/v2/tokens/new_listing".to_string();
261 let mut has_param = false;
262 if let Some(limit) = limit {
263 endpoint.push_str(&format!("?limit={}", limit));
264 has_param = true;
265 }
266 if let Some(offset) = offset {
267 endpoint.push_str(&format!(
268 "{}offset={}",
269 if has_param { "&" } else { "?" },
270 offset
271 ));
272 }
273 self.make_request(&endpoint).await
274 }
275
276 pub async fn get_token_top_traders(
277 &self,
278 address: String,
279 limit: Option<i32>,
280 ) -> Result<String, String> {
281 let pubkey = Self::validate_solana_address(&address)?;
282 let mut endpoint = format!("/defi/v2/tokens/top_traders?address={}", pubkey.to_string());
283 if let Some(limit) = limit {
284 endpoint.push_str(&format!("&limit={}", limit));
285 }
286 self.make_request(&endpoint).await
287 }
288
289 pub async fn get_gainers_losers(&self) -> Result<String, String> {
291 self.make_request("/trader/gainers-losers").await
292 }
293
294 pub async fn get_trader_txs_by_time(
295 &self,
296 address: String,
297 time_from: i64,
298 time_to: i64,
299 limit: Option<i32>,
300 ) -> Result<String, String> {
301 let pubkey = Self::validate_solana_address(&address)?;
302 let mut endpoint = format!(
303 "/trader/txs/seek_by_time?address={}&from={}&to={}",
304 pubkey.to_string(),
305 time_from,
306 time_to
307 );
308 if let Some(limit) = limit {
309 endpoint.push_str(&format!("&limit={}", limit));
310 }
311 self.make_request(&endpoint).await
312 }
313
314 pub async fn list_supported_chains(&self) -> Result<String, String> {
316 self.make_request("/v1/wallet/list_supported_chain").await
317 }
318
319 pub async fn get_wallet_portfolio(
320 &self,
321 wallet_address: String,
322 chain_id: String,
323 ) -> Result<String, String> {
324 self.make_request(&format!(
325 "/v1/wallet/token_list?wallet={}&chain_id={}",
326 wallet_address, chain_id
327 ))
328 .await
329 }
330
331 pub async fn get_wallet_portfolio_multichain(
332 &self,
333 wallet_address: String,
334 ) -> Result<String, String> {
335 self.make_request(&format!(
336 "/v1/wallet/multichain_token_list?wallet={}",
337 wallet_address
338 ))
339 .await
340 }
341
342 pub async fn get_wallet_transaction_history(
356 &self,
357 wallet_address: String,
358 chain_id: String,
359 limit: Option<i32>,
360 offset: Option<i32>,
361 ) -> Result<String, String> {
362 let mut endpoint = format!(
363 "/v1/wallet/tx_list?wallet={}&chain_id={}",
364 wallet_address, chain_id
365 );
366 if let Some(limit) = limit {
367 endpoint.push_str(&format!("&limit={}", limit));
368 }
369 if let Some(offset) = offset {
370 endpoint.push_str(&format!("&offset={}", offset));
371 }
372 self.make_request(&endpoint).await
373 }
374
375 pub async fn get_wallet_transaction_history_multichain(
376 &self,
377 wallet_address: String,
378 limit: Option<i32>,
379 offset: Option<i32>,
380 ) -> Result<String, String> {
381 let mut endpoint = format!("/v1/wallet/multichain_tx_list?wallet={}", wallet_address);
382 if let Some(limit) = limit {
383 endpoint.push_str(&format!("&limit={}", limit));
384 }
385 if let Some(offset) = offset {
386 endpoint.push_str(&format!("&offset={}", offset));
387 }
388 self.make_request(&endpoint).await
389 }
390
391 pub async fn simulate_transaction(
392 &self,
393 chain_id: String,
394 tx_data: String,
395 ) -> Result<String, String> {
396 self.make_request(&format!(
397 "/v1/wallet/simulate?chain_id={}&tx_data={}",
398 chain_id, tx_data
399 ))
400 .await
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn setup_client() -> BirdeyeClient {
409 let api_key = std::env::var("BIRDEYE_API_KEY")
410 .expect("BIRDEYE_API_KEY must be set in .env for tests");
411 BirdeyeClient::new(api_key)
412 }
413
414 const SOL_ADDRESS: &str = "So11111111111111111111111111111111111111112";
415 const USDC_ADDRESS: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
416 const TEST_WALLET: &str = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"; const TEST_CHAIN_ID: &str = "solana";
418
419 #[tokio::test]
420 async fn test_get_token_price() {
421 let client = setup_client();
422 let result = client.get_token_price(SOL_ADDRESS.to_string()).await;
423 println!("Token price result: {:?}", result);
424 assert!(result.is_ok());
425 }
426
427 #[tokio::test]
428 async fn test_get_token_price_history() {
429 let client = setup_client();
430 let result = client
431 .get_token_price_history(
432 SOL_ADDRESS.to_string(),
433 "15m".to_string(),
434 Some(1677652288),
435 Some(1677738688),
436 Some(100),
437 )
438 .await;
439 println!("Price history result: {:?}", result);
440 assert!(result.is_ok());
441 }
442
443 #[tokio::test]
444 async fn test_get_multi_token_price() {
445 let client = setup_client();
446 let addresses = format!("{},{}", SOL_ADDRESS, USDC_ADDRESS);
447 let result = client.get_multi_token_price(addresses).await;
448 println!("Multi token price result: {:?}", result);
449 assert!(result.is_ok());
450 }
451
452 #[tokio::test]
453 async fn test_get_token_ohlcv() {
454 let client = setup_client();
455 let result = client
456 .get_token_ohlcv(
457 SOL_ADDRESS.to_string(),
458 "1D".to_string(),
459 1677652288,
460 1677738688,
461 )
462 .await;
463 println!("OHLCV result: {:?}", result);
464 assert!(result.is_ok());
465 }
466
467 #[tokio::test]
468 async fn test_get_pair_ohlcv() {
469 let client = setup_client();
470 let result = client
471 .get_pair_ohlcv(
472 "8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NLGnaTUapubyvu".to_string(), "1D".to_string(),
474 1677652288,
475 1677738688,
476 )
477 .await;
478 println!("Pair OHLCV result: {:?}", result);
479 assert!(result.is_ok());
480 }
481
482 #[tokio::test]
483 async fn test_get_token_trades() {
484 let client = setup_client();
485 let result = client
486 .get_token_trades(SOL_ADDRESS.to_string(), Some(10), Some(0))
487 .await;
488 println!("Token trades result: {:?}", result);
489 assert!(result.is_ok());
490 }
491
492 #[tokio::test]
493 async fn test_get_pair_trades() {
494 let client = setup_client();
495 let result = client
496 .get_pair_trades(
497 "8HoQnePLqPj4M7PUDzfw8e3Ymdwgc7NLGnaTUapubyvu".to_string(),
498 Some(10),
499 Some(0),
500 )
501 .await;
502 println!("Pair trades result: {:?}", result);
503 assert!(result.is_ok());
504 }
505
506 #[tokio::test]
507 async fn test_get_token_overview() {
508 let client = setup_client();
509 let result = client.get_token_overview(SOL_ADDRESS.to_string()).await;
510 println!("Token overview result: {:?}", result);
511 assert!(result.is_ok());
512 }
513
514 #[tokio::test]
515 async fn test_get_token_list() {
516 let client = setup_client();
517 let result = client.get_token_list(Some(10), Some(0)).await;
518 println!("Token list result: {:?}", result);
519 assert!(result.is_ok());
520 }
521
522 #[tokio::test]
523 async fn test_get_token_security() {
524 let client = setup_client();
525 let result = client.get_token_security(SOL_ADDRESS.to_string()).await;
526 println!("Token security result: {:?}", result);
527 assert!(result.is_ok());
528 }
529
530 #[tokio::test]
531 async fn test_get_token_market_list() {
532 let client = setup_client();
533 let result = client.get_token_market_list(SOL_ADDRESS.to_string()).await;
534 println!("Market list result: {:?}", result);
535 assert!(result.is_ok());
536 }
537
538 #[tokio::test]
539 async fn test_get_token_new_listing() {
540 let client = setup_client();
541 let result = client.get_token_new_listing(Some(10), Some(0)).await;
542 println!("New listing result: {:?}", result);
543 assert!(result.is_ok());
544 }
545
546 #[tokio::test]
547 async fn test_get_token_top_traders() {
548 let client = setup_client();
549 let result = client
550 .get_token_top_traders(SOL_ADDRESS.to_string(), Some(10))
551 .await;
552 println!("Top traders result: {:?}", result);
553 assert!(result.is_ok());
554 }
555
556 #[tokio::test]
557 async fn test_get_token_trending() {
558 let client = setup_client();
559 let result = client.get_token_trending(Some(10)).await;
560 println!("Trending result: {:?}", result);
561 assert!(result.is_ok());
562 }
563
564 #[tokio::test]
565 async fn test_get_gainers_losers() {
566 let client = setup_client();
567 let result = client.get_gainers_losers().await;
568 println!("Gainers/Losers result: {:?}", result);
569 assert!(result.is_ok());
570 }
571
572 #[tokio::test]
573 async fn test_get_trader_txs_by_time() {
574 let client = setup_client();
575 let result = client
576 .get_trader_txs_by_time(SOL_ADDRESS.to_string(), 1677652288, 1677738688, Some(10))
577 .await;
578 println!("Trader txs result: {:?}", result);
579 assert!(result.is_ok());
580 }
581
582 #[tokio::test]
583 async fn test_list_supported_chains() {
584 let client = setup_client();
585 let result = client.list_supported_chains().await;
586 println!("Supported chains result: {:?}", result);
587 assert!(result.is_ok());
588 }
589
590 #[tokio::test]
591 async fn test_get_wallet_portfolio() {
592 let client = setup_client();
593 let result = client
594 .get_wallet_portfolio(TEST_WALLET.to_string(), TEST_CHAIN_ID.to_string())
595 .await;
596 println!("Wallet portfolio result: {:?}", result);
597 assert!(result.is_ok());
598 }
599
600 #[tokio::test]
601 async fn test_get_wallet_portfolio_multichain() {
602 let client = setup_client();
603 let result = client
604 .get_wallet_portfolio_multichain(TEST_WALLET.to_string())
605 .await;
606 println!("Multichain portfolio result: {:?}", result);
607 assert!(result.is_ok());
608 }
609
610 #[tokio::test]
625 async fn test_get_wallet_transaction_history() {
626 let client = setup_client();
627 let result = client
628 .get_wallet_transaction_history(
629 TEST_WALLET.to_string(),
630 TEST_CHAIN_ID.to_string(),
631 Some(10),
632 Some(0),
633 )
634 .await;
635 println!("Transaction history result: {:?}", result);
636 assert!(result.is_ok());
637 }
638
639 #[tokio::test]
640 async fn test_get_wallet_transaction_history_multichain() {
641 let client = setup_client();
642 let result = client
643 .get_wallet_transaction_history_multichain(TEST_WALLET.to_string(), Some(10), Some(0))
644 .await;
645 println!("Multichain transaction history result: {:?}", result);
646 assert!(result.is_ok());
647 }
648
649 #[tokio::test]
650 async fn test_simulate_transaction() {
651 let client = setup_client();
652 let result = client
653 .simulate_transaction(
654 TEST_CHAIN_ID.to_string(),
655 "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDBXsgXgYAAAAAAAA".to_string(),
656 )
657 .await;
658 println!("Transaction simulation result: {:?}", result);
659 assert!(result.is_ok());
660 }
661
662 #[tokio::test]
663 async fn test_error_handling() {
664 let client = BirdeyeClient::new("invalid-api-key".to_string());
665 let result = client.get_token_price(SOL_ADDRESS.to_string()).await;
666 assert!(result.is_err());
667 }
668
669 #[test]
670 fn test_format_resolution() {
671 assert_eq!(BirdeyeClient::format_resolution("1".to_string()), "1m");
672 assert_eq!(BirdeyeClient::format_resolution("15".to_string()), "15m");
673 assert_eq!(BirdeyeClient::format_resolution("1D".to_string()), "1D");
674 assert_eq!(BirdeyeClient::format_resolution("1W".to_string()), "1W");
675 }
676}