diff --git a/src/config.rs b/src/config.rs index bb9a841..026d75a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::error::{HeliusError, Result}; +use crate::error::Result; use crate::types::{Cluster, HeliusEndpoints}; #[derive(Clone)] @@ -10,21 +10,7 @@ pub struct Config { impl Config { pub fn new(api_key: &str, cluster: Cluster) -> Result { - if api_key.is_empty() { - return Err(HeliusError::InvalidInput("API key must not be empty".to_string())); - } - - let endpoints: HeliusEndpoints = match cluster { - Cluster::Devnet => HeliusEndpoints { - api: "https://api-devnet.helius-rpc.com".to_string(), - rpc: "https://devnet.helius-rpc.com".to_string(), - }, - Cluster::MainnetBeta => HeliusEndpoints { - api: "https://api-mainnet.helius-rpc.com".to_string(), - rpc: "https://mainnet.helius-rpc.com".to_string(), - }, - }; - + let endpoints: HeliusEndpoints = HeliusEndpoints::for_cluster(&cluster); Ok(Config { api_key: api_key.to_string(), cluster, diff --git a/src/error.rs b/src/error.rs index f085ba8..6ae74a1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ -use reqwest::{Error as ReqwestError, Response, StatusCode}; -use serde_json::Value; +use reqwest::{Error as ReqwestError, StatusCode}; +use serde_json::Error as SerdeJsonError; use thiserror::Error; #[derive(Debug, Error)] @@ -22,8 +22,11 @@ pub enum HeliusError { #[error("Too many requests made to {path}")] RateLimitExceeded { path: String }, + #[error("Request error: {0}")] + ReqwestError(ReqwestError), + #[error("Serialization / Deserialization error: {0}")] - SerdeJson(ReqwestError), + SerdeJson(SerdeJsonError), #[error("Unauthorized access to {path}: {text}")] Unauthorized { path: String, text: String }, @@ -33,13 +36,7 @@ pub enum HeliusError { } impl HeliusError { - pub async fn from_response_status(status: StatusCode, path: String, response: Response) -> Self { - let body: String = response.text().await.unwrap_or_default(); - let v: Value = serde_json::from_str(&body).unwrap_or_default(); - - // Extract only the message part of the JSON - let text: String = v["message"].as_str().unwrap_or("").to_string(); - + pub fn from_response_status(status: StatusCode, path: String, text: String) -> Self { match status { StatusCode::BAD_REQUEST => HeliusError::BadRequest { path, text }, StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => HeliusError::Unauthorized { path, text }, @@ -51,5 +48,11 @@ impl HeliusError { } } +impl From for HeliusError { + fn from(err: SerdeJsonError) -> HeliusError { + HeliusError::SerdeJson(err) + } +} + // Handy type alias pub type Result = std::result::Result; diff --git a/src/request_handler.rs b/src/request_handler.rs index 88b08e3..dc5b413 100644 --- a/src/request_handler.rs +++ b/src/request_handler.rs @@ -15,8 +15,7 @@ impl RequestHandler { } async fn send_request(&self, request_builder: RequestBuilder) -> Result { - let response: Response = request_builder.send().await.map_err(|e| HeliusError::Network(e))?; - Ok(response) + request_builder.send().await.map_err(HeliusError::Network) } pub async fn send(&self, method: Method, url: Url, body: Option<&R>) -> Result @@ -24,23 +23,63 @@ impl RequestHandler { R: Serialize + ?Sized + Send + Sync + Debug, T: for<'de> Deserialize<'de> + Default, { - let mut request_builder: RequestBuilder = self.http_client.request(method, url.clone()); + let mut request_builder: RequestBuilder = self.http_client.request(method, url); if let Some(body) = body { request_builder = request_builder.json(body); } let response: Response = self.send_request(request_builder).await?; - let path: String = url.path().to_string(); - self.handle_response(path, response).await + self.handle_response(response).await } - async fn handle_response Deserialize<'de>>(&self, path: String, response: Response) -> Result { - let status: StatusCode = response.status(); + // async fn handle_response Deserialize<'de>>(&self, response: Response) -> Result { + // let status: StatusCode = response.status(); + // let path: String = response.url().path().to_string(); + // let body_text = response.text().await.unwrap_or_default(); + + // println!("Response status: {}, Body: {}", status, body_text); - match status { - StatusCode::OK | StatusCode::CREATED => response.json::().await.map_err(HeliusError::SerdeJson), - _ => Err(HeliusError::from_response_status(status, path, response).await), + // if status.is_success() { + // serde_json::from_str::(&body_text).map_err(|e| HeliusError::from(e)) + // } else { + // Err(HeliusError::from_response_status(status, path, body_text)) + // } + // } + // async fn handle_response Deserialize<'de>>(&self, response: Response) -> Result { + // let status: StatusCode = response.status(); + // let path: String = response.url().path().to_string(); + // let body_text: String = response.text().await.unwrap_or_default(); + + // println!("Response status: {}, Body: {}", status, body_text); + + // if status.is_success() { + // serde_json::from_str::(&body_text).map_err(|e| { + // eprintln!("Failed to deserialize response: {}", e); + // HeliusError::from(e) + // }) + // } else { + // Err(HeliusError::from_response_status(status, path, body_text)) + // } + // } + async fn handle_response Deserialize<'de>>(&self, response: Response) -> Result { + let status: StatusCode = response.status(); + let path: String = response.url().path().to_string(); + let body_text: String = response.text().await.unwrap_or_default(); + + println!("Response status: {}, Body: {}", status, body_text); + + if status.is_success() { + match serde_json::from_str::(&body_text) { + Ok(data) => Ok(data), + Err(e) => { + println!("Deserialization error: {}", e); + println!("Raw JSON: {}", body_text); + Err(HeliusError::from(e)) + } + } + } else { + Err(HeliusError::from_response_status(status, path.clone(), body_text)) } } } diff --git a/src/rpc_client.rs b/src/rpc_client.rs index 98a9da8..d4048be 100644 --- a/src/rpc_client.rs +++ b/src/rpc_client.rs @@ -3,8 +3,10 @@ use std::sync::Arc; use crate::config::Config; use crate::error::Result; use crate::request_handler::RequestHandler; +use crate::types::{AssetsByOwnerRequest, GetAssetResponseList}; -use reqwest::Client; +use reqwest::{Client, Method, Url}; +use serde_json::{json, Value}; pub struct RpcClient { pub handler: RequestHandler, @@ -16,4 +18,18 @@ impl RpcClient { let handler: RequestHandler = RequestHandler::new(client)?; Ok(RpcClient { handler, config }) } + + pub async fn get_assets_by_owner(&self, request: AssetsByOwnerRequest) -> Result { + let url: String = format!("{}?api-key={}", self.config.endpoints.rpc, self.config.api_key); + let url: Url = Url::parse(&url).expect("Failed to parse URL"); + + let request_body: Value = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "getAssetsByOwner", + "params": request + }); + + self.handler.send(Method::POST, url, Some(&request_body)).await + } } diff --git a/src/types/mod.rs b/src/types/mod.rs index c0178d8..cab419f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,5 +1,5 @@ pub mod enums; pub mod types; -pub use self::types::{Cluster, HeliusEndpoints}; -pub use enums::{AssetSortBy, AssetSortDirection}; +pub use self::enums::{AssetSortBy, AssetSortDirection}; +pub use self::types::{AssetsByOwnerRequest, Cluster, GetAssetResponseList, HeliusEndpoints}; diff --git a/src/types/types.rs b/src/types/types.rs index 6688d07..b986c46 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -1,4 +1,6 @@ -use super::enums::{AssetSortBy, AssetSortDirection, Interface, OwnershipModel, RoyaltyModel, Scope, UseMethods, Context }; +use super::enums::{ + AssetSortBy, AssetSortDirection, Context, Interface, OwnershipModel, RoyaltyModel, Scope, UseMethods, +}; use serde::{Deserialize, Serialize}; /// Defines the available clusters supported by Helius @@ -15,17 +17,53 @@ pub struct HeliusEndpoints { pub rpc: String, } -#[derive(Serialize, Deserialize, Debug)] +impl HeliusEndpoints { + pub fn for_cluster(cluster: &Cluster) -> Self { + match cluster { + Cluster::Devnet => HeliusEndpoints { + api: "https://api-devnet.helius-rpc.com/".to_string(), + rpc: "https://devnet.helius-rpc.com/".to_string(), + }, + Cluster::MainnetBeta => HeliusEndpoints { + api: "https://api-mainnet.helius-rpc.com/".to_string(), + rpc: "https://mainnet.helius-rpc.com/".to_string(), + }, + } + } +} + +#[derive(Serialize, Deserialize)] pub struct AssetsByOwnerRequest { + #[serde(rename = "ownerAddress")] pub owner_address: String, - pub page: i32, + #[serde(rename = "page")] + pub page: Option, + #[serde(rename = "limit")] pub limit: Option, + #[serde(rename = "before")] pub before: Option, + #[serde(rename = "after")] pub after: Option, + #[serde(rename = "displayOptions")] pub display_options: Option, + #[serde(rename = "sortBy")] pub sort_by: Option, } +impl Default for AssetsByOwnerRequest { + fn default() -> Self { + AssetsByOwnerRequest { + owner_address: Default::default(), + page: None, + limit: None, + before: None, + after: None, + display_options: None, + sort_by: None, + } + } +} + #[derive(Serialize, Deserialize, Debug)] pub struct DisplayOptions { pub show_unverified_collections: Option, @@ -39,13 +77,15 @@ pub struct AssetSortingRequest { pub sort_direction: AssetSortDirection, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] pub struct GetAssetResponseList { - pub grand_total: Option, - pub total: i32, - pub limit: i32, - pub page: i32, - pub items: Vec, + #[serde(rename = "grandTotal")] + pub grand_total: Option, + pub total: Option, + pub limit: Option, + pub page: Option, + #[serde(rename = "items")] + pub items: Option>, } #[derive(Serialize, Deserialize, Debug)]