Skip to main content

goldrush_sdk/
nfts.rs

1use crate::{Error, GoldRushClient};
2use crate::models::nfts::{NftsResponse, NftMetadataResponse};
3use reqwest::Method;
4
5/// Options for customizing NFT queries.
6#[derive(Debug, Clone, Default)]
7pub struct NftOptions {
8    /// Page number for pagination (0-indexed).
9    pub page_number: Option<u32>,
10    
11    /// Number of items per page.
12    pub page_size: Option<u32>,
13    
14    /// Quote currency for pricing (e.g., "USD", "ETH").
15    pub quote_currency: Option<String>,
16    
17    /// Whether to include metadata.
18    pub with_metadata: Option<bool>,
19    
20    /// Whether to exclude spam NFTs.
21    pub no_spam: Option<bool>,
22}
23
24impl NftOptions {
25    /// Create new default options.
26    pub fn new() -> Self {
27        Self::default()
28    }
29    
30    /// Set page number for pagination.
31    pub fn page_number(mut self, page: u32) -> Self {
32        self.page_number = Some(page);
33        self
34    }
35    
36    /// Set page size.
37    pub fn page_size(mut self, size: u32) -> Self {
38        self.page_size = Some(size);
39        self
40    }
41    
42    /// Set the quote currency.
43    pub fn quote_currency<S: Into<String>>(mut self, currency: S) -> Self {
44        self.quote_currency = Some(currency.into());
45        self
46    }
47    
48    /// Include or exclude metadata in the response.
49    pub fn with_metadata(mut self, include_metadata: bool) -> Self {
50        self.with_metadata = Some(include_metadata);
51        self
52    }
53    
54    /// Exclude spam NFTs.
55    pub fn no_spam(mut self, exclude_spam: bool) -> Self {
56        self.no_spam = Some(exclude_spam);
57        self
58    }
59}
60
61impl GoldRushClient {
62    /// Get NFTs owned by an address.
63    ///
64    /// # Arguments
65    ///
66    /// * `chain_name` - The blockchain name (e.g., "eth-mainnet", "matic-mainnet")
67    /// * `address` - The wallet address to query
68    /// * `options` - Optional query parameters
69    ///
70    /// # Example
71    ///
72    /// ```rust,no_run
73    /// use goldrush_sdk::{GoldRushClient, NftOptions};
74    ///
75    /// # async fn example(client: GoldRushClient) -> Result<(), goldrush_sdk::Error> {
76    /// let options = NftOptions::new()
77    ///     .page_size(10)
78    ///     .with_metadata(true)
79    ///     .no_spam(true);
80    ///     
81    /// let nfts = client
82    ///     .get_nfts_for_address(
83    ///         "eth-mainnet",
84    ///         "0xfc43f5f9dd45258b3aff31bdbe6561d97e8b71de",
85    ///         Some(options)
86    ///     )
87    ///     .await?;
88    /// # Ok(())
89    /// # }
90    /// ```
91    pub async fn get_nfts_for_address(
92        &self,
93        chain_name: &str,
94        address: &str,
95        options: Option<NftOptions>,
96    ) -> Result<NftsResponse, Error> {
97        // TODO: Confirm exact endpoint path with maintainers
98        let path = format!("/v1/{}/address/{}/balances_nft/", chain_name, address);
99        
100        let mut builder = self.build_request(Method::GET, &path);
101        
102        // Add query parameters if options are provided
103        if let Some(opts) = options {
104            if let Some(page_num) = opts.page_number {
105                builder = builder.query(&[("page-number", page_num.to_string())]);
106            }
107            if let Some(page_sz) = opts.page_size {
108                builder = builder.query(&[("page-size", page_sz.to_string())]);
109            }
110            if let Some(currency) = opts.quote_currency {
111                builder = builder.query(&[("quote-currency", currency)]);
112            }
113            if let Some(with_meta) = opts.with_metadata {
114                builder = builder.query(&[("with-metadata", with_meta.to_string())]);
115            }
116            if let Some(no_spam) = opts.no_spam {
117                builder = builder.query(&[("no-spam", no_spam.to_string())]);
118            }
119        }
120        
121        self.send_with_retry(builder).await
122    }
123
124    /// Get metadata for a specific NFT.
125    ///
126    /// # Arguments
127    ///
128    /// * `chain_name` - The blockchain name
129    /// * `contract_address` - The NFT collection contract address
130    /// * `token_id` - The specific token ID
131    ///
132    /// # Example
133    ///
134    /// ```rust,no_run
135    /// use goldrush_sdk::GoldRushClient;
136    ///
137    /// # async fn example(client: GoldRushClient) -> Result<(), goldrush_sdk::Error> {
138    /// let metadata = client
139    ///     .get_nft_metadata(
140    ///         "eth-mainnet",
141    ///         "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
142    ///         "1"
143    ///     )
144    ///     .await?;
145    /// # Ok(())
146    /// # }
147    /// ```
148    pub async fn get_nft_metadata(
149        &self,
150        chain_name: &str,
151        contract_address: &str,
152        token_id: &str,
153    ) -> Result<NftMetadataResponse, Error> {
154        // TODO: Confirm exact endpoint path with maintainers
155        let path = format!(
156            "/v1/{}/tokens/{}/nft_metadata/{}/",
157            chain_name,
158            contract_address,
159            token_id
160        );
161        
162        let builder = self.build_request(Method::GET, &path);
163        self.send_with_retry(builder).await
164    }
165    
166    /// Get all NFTs from a specific collection.
167    ///
168    /// # Arguments
169    ///
170    /// * `chain_name` - The blockchain name
171    /// * `contract_address` - The NFT collection contract address
172    /// * `options` - Optional query parameters
173    ///
174    /// # Example
175    ///
176    /// ```rust,no_run
177    /// use goldrush_sdk::GoldRushClient;
178    ///
179    /// # async fn example(client: GoldRushClient) -> Result<(), goldrush_sdk::Error> {
180    /// let collection_nfts = client
181    ///     .get_nfts_for_collection(
182    ///         "eth-mainnet",
183    ///         "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
184    ///         None
185    ///     )
186    ///     .await?;
187    /// # Ok(())
188    /// # }
189    /// ```
190    pub async fn get_nfts_for_collection(
191        &self,
192        chain_name: &str,
193        contract_address: &str,
194        options: Option<NftOptions>,
195    ) -> Result<NftsResponse, Error> {
196        // TODO: Confirm exact endpoint path with maintainers
197        let path = format!("/v1/{}/tokens/{}/nft_token_ids/", chain_name, contract_address);
198        
199        let mut builder = self.build_request(Method::GET, &path);
200        
201        if let Some(opts) = options {
202            if let Some(page_num) = opts.page_number {
203                builder = builder.query(&[("page-number", page_num.to_string())]);
204            }
205            if let Some(page_sz) = opts.page_size {
206                builder = builder.query(&[("page-size", page_sz.to_string())]);
207            }
208            if let Some(with_meta) = opts.with_metadata {
209                builder = builder.query(&[("with-metadata", with_meta.to_string())]);
210            }
211        }
212        
213        self.send_with_retry(builder).await
214    }
215    
216    /// Get NFT owners for a specific collection.
217    ///
218    /// # Arguments
219    ///
220    /// * `chain_name` - The blockchain name
221    /// * `contract_address` - The NFT collection contract address
222    /// * `options` - Optional query parameters
223    ///
224    /// # Example
225    ///
226    /// ```rust,no_run
227    /// use goldrush_sdk::GoldRushClient;
228    ///
229    /// # async fn example(client: GoldRushClient) -> Result<(), goldrush_sdk::Error> {
230    /// let owners = client
231    ///     .get_nft_owners_for_collection(
232    ///         "eth-mainnet",
233    ///         "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
234    ///         None
235    ///     )
236    ///     .await?;
237    /// # Ok(())
238    /// # }
239    /// ```
240    pub async fn get_nft_owners_for_collection(
241        &self,
242        chain_name: &str,
243        contract_address: &str,
244        options: Option<NftOptions>,
245    ) -> Result<NftsResponse, Error> {
246        // TODO: Confirm exact endpoint path with maintainers
247        let path = format!("/v1/{}/tokens/{}/nft_token_owners/", chain_name, contract_address);
248        
249        let mut builder = self.build_request(Method::GET, &path);
250        
251        if let Some(opts) = options {
252            if let Some(page_num) = opts.page_number {
253                builder = builder.query(&[("page-number", page_num.to_string())]);
254            }
255            if let Some(page_sz) = opts.page_size {
256                builder = builder.query(&[("page-size", page_sz.to_string())]);
257            }
258        }
259        
260        self.send_with_retry(builder).await
261    }
262}
263
264/// Iterator for paginating through NFTs.
265pub struct NftsPageIter<'a> {
266    client: &'a GoldRushClient,
267    chain_name: String,
268    address: String,
269    options: NftOptions,
270    finished: bool,
271}
272
273impl<'a> NftsPageIter<'a> {
274    /// Create a new NFTs page iterator.
275    pub fn new<C: Into<String>, A: Into<String>>(
276        client: &'a GoldRushClient,
277        chain_name: C,
278        address: A,
279        options: NftOptions,
280    ) -> Self {
281        Self {
282            client,
283            chain_name: chain_name.into(),
284            address: address.into(),
285            options,
286            finished: false,
287        }
288    }
289
290    /// Get the next page of NFTs.
291    pub async fn next(
292        &mut self,
293    ) -> Result<Option<Vec<crate::models::nfts::NftItem>>, Error> {
294        if self.finished {
295            return Ok(None);
296        }
297
298        let resp = self
299            .client
300            .get_nfts_for_address(&self.chain_name, &self.address, Some(self.options.clone()))
301            .await?;
302
303        if let Some(data) = resp.data {
304            let items = data.items;
305            if items.is_empty() || !resp.pagination.as_ref().and_then(|p| p.has_more).unwrap_or(false) {
306                self.finished = true;
307            } else if let Some(pagination) = resp.pagination {
308                if let Some(next_page) = pagination.page_number.map(|n| n + 1) {
309                    self.options.page_number = Some(next_page);
310                } else {
311                    self.finished = true;
312                }
313            }
314            Ok(Some(items))
315        } else {
316            self.finished = true;
317            Ok(None)
318        }
319    }
320    
321    /// Check if there are more pages available.
322    pub fn has_more(&self) -> bool {
323        !self.finished
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use serde_json::json;
331
332    #[test]
333    fn test_nft_options_builder() {
334        let options = NftOptions::new()
335            .page_size(20)
336            .with_metadata(true)
337            .no_spam(true)
338            .quote_currency("USD");
339            
340        assert_eq!(options.page_size, Some(20));
341        assert_eq!(options.with_metadata, Some(true));
342        assert_eq!(options.no_spam, Some(true));
343        assert_eq!(options.quote_currency, Some("USD".to_string()));
344    }
345
346    #[test]
347    fn test_deserialize_nfts_response() {
348        let json_data = json!({
349            "data": {
350                "address": "0x123",
351                "chain_id": 1,
352                "items": [{
353                    "contract_address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
354                    "token_id": "1",
355                    "token_balance": "1",
356                    "contract_name": "Bored Ape Yacht Club",
357                    "contract_ticker_symbol": "BAYC",
358                    "supports_erc": ["erc721"]
359                }]
360            },
361            "pagination": {
362                "has_more": false,
363                "page_number": 0,
364                "page_size": 100,
365                "total_count": 1
366            }
367        });
368
369        let response: NftsResponse = serde_json::from_value(json_data).unwrap();
370        assert!(response.data.is_some());
371        
372        let data = response.data.unwrap();
373        assert_eq!(data.items.len(), 1);
374        assert_eq!(data.items[0].contract_ticker_symbol, Some("BAYC".to_string()));
375        assert_eq!(data.items[0].token_id, "1");
376    }
377
378    #[test] 
379    fn test_deserialize_nft_metadata_response() {
380        let json_data = json!({
381            "data": [{
382                "contract_address": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
383                "token_id": "1",
384                "token_uri": "https://ipfs.io/ipfs/...",
385                "metadata": {
386                    "name": "Bored Ape #1",
387                    "description": "A bored ape",
388                    "image": "https://ipfs.io/ipfs/..."
389                }
390            }]
391        });
392
393        let response: NftMetadataResponse = serde_json::from_value(json_data).unwrap();
394        assert!(response.data.is_some());
395        
396        let data = response.data.unwrap();
397        assert_eq!(data.len(), 1);
398        assert_eq!(data[0].contract_address, "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d");
399        assert_eq!(data[0].token_id, "1");
400    }
401}