1use crate::{Error, GoldRushClient};
2use crate::models::nfts::{NftsResponse, NftMetadataResponse};
3use reqwest::Method;
4
5#[derive(Debug, Clone, Default)]
7pub struct NftOptions {
8 pub page_number: Option<u32>,
10
11 pub page_size: Option<u32>,
13
14 pub quote_currency: Option<String>,
16
17 pub with_metadata: Option<bool>,
19
20 pub no_spam: Option<bool>,
22}
23
24impl NftOptions {
25 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn page_number(mut self, page: u32) -> Self {
32 self.page_number = Some(page);
33 self
34 }
35
36 pub fn page_size(mut self, size: u32) -> Self {
38 self.page_size = Some(size);
39 self
40 }
41
42 pub fn quote_currency<S: Into<String>>(mut self, currency: S) -> Self {
44 self.quote_currency = Some(currency.into());
45 self
46 }
47
48 pub fn with_metadata(mut self, include_metadata: bool) -> Self {
50 self.with_metadata = Some(include_metadata);
51 self
52 }
53
54 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 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 let path = format!("/v1/{}/address/{}/balances_nft/", chain_name, address);
99
100 let mut builder = self.build_request(Method::GET, &path);
101
102 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 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 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 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 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 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 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
264pub 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 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 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 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}