Skip to main content

goldrush_sdk/
transactions.rs

1use crate::{Error, GoldRushClient};
2use crate::models::transactions::{TransactionsResponse, TransactionResponse};
3use reqwest::Method;
4
5/// Options for customizing transaction queries.
6#[derive(Debug, Clone, Default)]
7pub struct TxOptions {
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    /// Include or exclude log events.
18    pub with_log_events: Option<bool>,
19    
20    /// Start block height for filtering.
21    pub starting_block: Option<u64>,
22    
23    /// End block height for filtering.
24    pub ending_block: Option<u64>,
25}
26
27impl TxOptions {
28    /// Create new default options.
29    pub fn new() -> Self {
30        Self::default()
31    }
32    
33    /// Set page number for pagination.
34    pub fn page_number(mut self, page: u32) -> Self {
35        self.page_number = Some(page);
36        self
37    }
38    
39    /// Set page size.
40    pub fn page_size(mut self, size: u32) -> Self {
41        self.page_size = Some(size);
42        self
43    }
44    
45    /// Set the quote currency.
46    pub fn quote_currency<S: Into<String>>(mut self, currency: S) -> Self {
47        self.quote_currency = Some(currency.into());
48        self
49    }
50    
51    /// Include or exclude log events in the response.
52    pub fn with_log_events(mut self, include_logs: bool) -> Self {
53        self.with_log_events = Some(include_logs);
54        self
55    }
56    
57    /// Set starting block for filtering.
58    pub fn starting_block(mut self, block: u64) -> Self {
59        self.starting_block = Some(block);
60        self
61    }
62    
63    /// Set ending block for filtering.
64    pub fn ending_block(mut self, block: u64) -> Self {
65        self.ending_block = Some(block);
66        self
67    }
68}
69
70impl GoldRushClient {
71    /// Get all transactions for an address.
72    ///
73    /// # Arguments
74    ///
75    /// * `chain_name` - The blockchain name (e.g., "eth-mainnet", "matic-mainnet")
76    /// * `address` - The wallet address to query
77    /// * `options` - Optional query parameters
78    ///
79    /// # Example
80    ///
81    /// ```rust,no_run
82    /// use goldrush_sdk::{GoldRushClient, TxOptions};
83    ///
84    /// # async fn example(client: GoldRushClient) -> Result<(), goldrush_sdk::Error> {
85    /// let options = TxOptions::new()
86    ///     .page_size(10)
87    ///     .quote_currency("USD");
88    ///     
89    /// let transactions = client
90    ///     .get_all_transactions_for_address(
91    ///         "eth-mainnet",
92    ///         "0xfc43f5f9dd45258b3aff31bdbe6561d97e8b71de",
93    ///         Some(options)
94    ///     )
95    ///     .await?;
96    /// # Ok(())
97    /// # }
98    /// ```
99    pub async fn get_all_transactions_for_address(
100        &self,
101        chain_name: &str,
102        address: &str,
103        options: Option<TxOptions>,
104    ) -> Result<TransactionsResponse, Error> {
105        // TODO: Confirm exact endpoint path with maintainers
106        let path = format!("/v1/{}/address/{}/transactions_v2/", chain_name, address);
107        
108        let mut builder = self.build_request(Method::GET, &path);
109        
110        // Add query parameters if options are provided
111        if let Some(opts) = options {
112            if let Some(page_num) = opts.page_number {
113                builder = builder.query(&[("page-number", page_num.to_string())]);
114            }
115            if let Some(page_sz) = opts.page_size {
116                builder = builder.query(&[("page-size", page_sz.to_string())]);
117            }
118            if let Some(currency) = opts.quote_currency {
119                builder = builder.query(&[("quote-currency", currency)]);
120            }
121            if let Some(with_logs) = opts.with_log_events {
122                builder = builder.query(&[("with-log-events", with_logs.to_string())]);
123            }
124            if let Some(start_block) = opts.starting_block {
125                builder = builder.query(&[("starting-block", start_block.to_string())]);
126            }
127            if let Some(end_block) = opts.ending_block {
128                builder = builder.query(&[("ending-block", end_block.to_string())]);
129            }
130        }
131        
132        self.send_with_retry(builder).await
133    }
134
135    /// Get a specific transaction by hash.
136    ///
137    /// # Arguments
138    ///
139    /// * `chain_name` - The blockchain name
140    /// * `tx_hash` - The transaction hash
141    ///
142    /// # Example
143    ///
144    /// ```rust,no_run
145    /// use goldrush_sdk::GoldRushClient;
146    ///
147    /// # async fn example(client: GoldRushClient) -> Result<(), goldrush_sdk::Error> {
148    /// let transaction = client
149    ///     .get_transaction(
150    ///         "eth-mainnet",
151    ///         "0x1234567890abcdef..."
152    ///     )
153    ///     .await?;
154    /// # Ok(())
155    /// # }
156    /// ```
157    pub async fn get_transaction(
158        &self,
159        chain_name: &str,
160        tx_hash: &str,
161    ) -> Result<TransactionResponse, Error> {
162        // TODO: Confirm exact endpoint path with maintainers
163        let path = format!("/v1/{}/transaction_v2/{}/", chain_name, tx_hash);
164        
165        let builder = self.build_request(Method::GET, &path);
166        self.send_with_retry(builder).await
167    }
168    
169    /// Get transactions between two addresses.
170    ///
171    /// # Arguments
172    ///
173    /// * `chain_name` - The blockchain name
174    /// * `from_address` - The sender address
175    /// * `to_address` - The recipient address
176    /// * `options` - Optional query parameters
177    ///
178    /// # Example
179    ///
180    /// ```rust,no_run
181    /// use goldrush_sdk::GoldRushClient;
182    ///
183    /// # async fn example(client: GoldRushClient) -> Result<(), goldrush_sdk::Error> {
184    /// let transactions = client
185    ///     .get_transactions_between_addresses(
186    ///         "eth-mainnet",
187    ///         "0xfrom...",
188    ///         "0xto...",
189    ///         None
190    ///     )
191    ///     .await?;
192    /// # Ok(())
193    /// # }
194    /// ```
195    pub async fn get_transactions_between_addresses(
196        &self,
197        chain_name: &str,
198        from_address: &str,
199        to_address: &str,
200        options: Option<TxOptions>,
201    ) -> Result<TransactionsResponse, Error> {
202        // TODO: Confirm exact endpoint path with maintainers
203        let path = format!(
204            "/v1/{}/bulk/transactions/{}/{}/", 
205            chain_name, 
206            from_address, 
207            to_address
208        );
209        
210        let mut builder = self.build_request(Method::GET, &path);
211        
212        if let Some(opts) = options {
213            if let Some(page_num) = opts.page_number {
214                builder = builder.query(&[("page-number", page_num.to_string())]);
215            }
216            if let Some(page_sz) = opts.page_size {
217                builder = builder.query(&[("page-size", page_sz.to_string())]);
218            }
219            if let Some(currency) = opts.quote_currency {
220                builder = builder.query(&[("quote-currency", currency)]);
221            }
222        }
223        
224        self.send_with_retry(builder).await
225    }
226}
227
228/// Iterator for paginating through transactions.
229pub struct TransactionsPageIter<'a> {
230    client: &'a GoldRushClient,
231    chain_name: String,
232    address: String,
233    options: TxOptions,
234    finished: bool,
235}
236
237impl<'a> TransactionsPageIter<'a> {
238    /// Create a new transaction page iterator.
239    pub fn new<C: Into<String>, A: Into<String>>(
240        client: &'a GoldRushClient,
241        chain_name: C,
242        address: A,
243        options: TxOptions,
244    ) -> Self {
245        Self {
246            client,
247            chain_name: chain_name.into(),
248            address: address.into(),
249            options,
250            finished: false,
251        }
252    }
253
254    /// Get the next page of transactions.
255    pub async fn next(
256        &mut self,
257    ) -> Result<Option<Vec<crate::models::transactions::TransactionItem>>, Error> {
258        if self.finished {
259            return Ok(None);
260        }
261
262        let resp = self
263            .client
264            .get_all_transactions_for_address(&self.chain_name, &self.address, Some(self.options.clone()))
265            .await?;
266
267        if let Some(data) = resp.data {
268            let items = data.items;
269            if items.is_empty() || !resp.pagination.as_ref().and_then(|p| p.has_more).unwrap_or(false) {
270                self.finished = true;
271            } else if let Some(pagination) = resp.pagination {
272                if let Some(next_page) = pagination.page_number.map(|n| n + 1) {
273                    self.options.page_number = Some(next_page);
274                } else {
275                    self.finished = true;
276                }
277            }
278            Ok(Some(items))
279        } else {
280            self.finished = true;
281            Ok(None)
282        }
283    }
284    
285    /// Check if there are more pages available.
286    pub fn has_more(&self) -> bool {
287        !self.finished
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use serde_json::json;
295
296    #[test]
297    fn test_tx_options_builder() {
298        let options = TxOptions::new()
299            .page_size(25)
300            .quote_currency("USD")
301            .with_log_events(true)
302            .starting_block(18000000);
303            
304        assert_eq!(options.page_size, Some(25));
305        assert_eq!(options.quote_currency, Some("USD".to_string()));
306        assert_eq!(options.with_log_events, Some(true));
307        assert_eq!(options.starting_block, Some(18000000));
308    }
309
310    #[test]
311    fn test_deserialize_transaction_response() {
312        let json_data = json!({
313            "data": {
314                "tx_hash": "0x123abc...",
315                "from_address": "0xfrom...",
316                "to_address": "0xto...",
317                "value": "1000000000000000000",
318                "successful": true,
319                "block_height": 18500000,
320                "gas_used": 21000,
321                "fees_paid": "420000000000000"
322            },
323            "error": null
324        });
325
326        let response: TransactionResponse = serde_json::from_value(json_data).unwrap();
327        assert!(response.data.is_some());
328        
329        let tx = response.data.unwrap();
330        assert_eq!(tx.tx_hash, "0x123abc...");
331        assert_eq!(tx.successful, Some(true));
332        assert_eq!(tx.block_height, Some(18500000));
333    }
334
335    #[test]
336    fn test_deserialize_transactions_response() {
337        let json_data = json!({
338            "data": {
339                "address": "0x123",
340                "chain_id": 1,
341                "items": [{
342                    "tx_hash": "0x123abc...",
343                    "from_address": "0xfrom...",
344                    "to_address": "0xto...",
345                    "value": "1000000000000000000",
346                    "successful": true
347                }]
348            },
349            "pagination": {
350                "has_more": true,
351                "page_number": 0,
352                "page_size": 100
353            }
354        });
355
356        let response: TransactionsResponse = serde_json::from_value(json_data).unwrap();
357        assert!(response.data.is_some());
358        
359        let data = response.data.unwrap();
360        assert_eq!(data.items.len(), 1);
361        assert_eq!(data.items[0].tx_hash, "0x123abc...");
362    }
363}