1use crate::client::RequestParts;
4use crate::ids::{AccountID, TransactionID};
5use crate::models::{Transaction, TransactionFilter};
6use crate::validation::Validate;
7use crate::{Client, Error, Result, Timestamp};
8use reqwest::Method;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
15#[serde(rename_all = "camelCase")]
16pub struct ListTransactionsQuery {
17 #[serde(rename = "from", default, skip_serializing_if = "Option::is_none")]
21 pub from: Option<Timestamp>,
22 #[serde(rename = "to", default, skip_serializing_if = "Option::is_none")]
25 pub to: Option<Timestamp>,
26 #[serde(rename = "pageSize", default, skip_serializing_if = "Option::is_none")]
28 pub page_size: Option<i64>,
29 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
31 pub r#type: Option<Vec<TransactionFilter>>,
32}
33
34impl Validate for ListTransactionsQuery {
35 fn validate(&self) -> Result<()> {
36 if self
37 .page_size
38 .is_some_and(|size| !(1..=1000).contains(&size))
39 {
40 return Err(Error::InvalidInput("pageSize must be 1..=1000".into()));
41 }
42 if let (Some(from), Some(to)) = (self.from, self.to)
43 && (to < from
44 || to.into_inner().signed_duration_since(from.into_inner())
45 > chrono::Duration::days(365))
46 {
47 return Err(Error::InvalidInput(
48 "transaction range must be ordered and at most 365 days".into(),
49 ));
50 }
51 Ok(())
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct ListTransactionsResponse {
59 #[serde(rename = "from", default, skip_serializing_if = "Option::is_none")]
61 pub from: Option<Timestamp>,
62 #[serde(rename = "to", default, skip_serializing_if = "Option::is_none")]
64 pub to: Option<Timestamp>,
65 #[serde(rename = "pageSize", default, skip_serializing_if = "Option::is_none")]
67 pub page_size: Option<i64>,
68 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
70 pub r#type: Option<Vec<TransactionFilter>>,
71 #[serde(rename = "count", default, skip_serializing_if = "Option::is_none")]
73 pub count: Option<i64>,
74 #[serde(rename = "pages", default, skip_serializing_if = "Option::is_none")]
76 pub pages: Option<Vec<String>>,
77 #[serde(
79 rename = "lastTransactionID",
80 default,
81 skip_serializing_if = "Option::is_none"
82 )]
83 pub last_transaction_id: Option<TransactionID>,
84}
85
86impl Client {
87 pub async fn list_transactions(
95 &self,
96 account_id: &AccountID,
97 query: Option<&ListTransactionsQuery>,
98 ) -> std::result::Result<
99 crate::ApiResponse<ListTransactionsResponse>,
100 crate::OperationError<crate::GenericRejection>,
101 > {
102 let account_id_encoded = crate::client::segment(account_id.as_str());
103 let path = format!("/v3/accounts/{account_id_encoded}/transactions");
104 let request = RequestParts::new(Method::GET, &path);
105 let request = match query {
106 Some(query) => request.query(query)?,
107 None => request,
108 };
109 self.execute(request).await
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct TransactionDetailsResponse {
117 #[serde(
119 rename = "transaction",
120 default,
121 skip_serializing_if = "Option::is_none"
122 )]
123 pub transaction: Option<Transaction>,
124 #[serde(
126 rename = "lastTransactionID",
127 default,
128 skip_serializing_if = "Option::is_none"
129 )]
130 pub last_transaction_id: Option<TransactionID>,
131}
132
133impl Client {
134 pub async fn transaction_details(
142 &self,
143 account_id: &AccountID,
144 transaction_id: &TransactionID,
145 ) -> std::result::Result<
146 crate::ApiResponse<TransactionDetailsResponse>,
147 crate::OperationError<crate::GenericRejection>,
148 > {
149 let account_id_encoded = crate::client::segment(account_id.as_str());
150 let transaction_id_encoded = crate::client::segment(transaction_id.as_str());
151 let path =
152 format!("/v3/accounts/{account_id_encoded}/transactions/{transaction_id_encoded}");
153 self.execute(RequestParts::new(Method::GET, &path)).await
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase")]
160pub struct TransactionsByIdRangeQuery {
161 #[serde(rename = "from")]
163 pub from: TransactionID,
164 #[serde(rename = "to")]
166 pub to: TransactionID,
167 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
169 pub r#type: Option<Vec<TransactionFilter>>,
170}
171
172impl Validate for TransactionsByIdRangeQuery {
173 fn validate(&self) -> Result<()> {
174 let from = self.from.as_str().parse::<u64>();
175 let to = self.to.as_str().parse::<u64>();
176 if !matches!((from, to), (Ok(from), Ok(to)) if from <= to) {
177 return Err(Error::InvalidInput("invalid transaction ID range".into()));
178 }
179 Ok(())
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct TransactionsByIdRangeResponse {
187 #[serde(
189 rename = "transactions",
190 default,
191 skip_serializing_if = "Option::is_none"
192 )]
193 pub transactions: Option<Vec<Transaction>>,
194 #[serde(
196 rename = "lastTransactionID",
197 default,
198 skip_serializing_if = "Option::is_none"
199 )]
200 pub last_transaction_id: Option<TransactionID>,
201}
202
203impl Client {
204 pub async fn transactions_by_id_range(
212 &self,
213 account_id: &AccountID,
214 query: &TransactionsByIdRangeQuery,
215 ) -> std::result::Result<
216 crate::ApiResponse<TransactionsByIdRangeResponse>,
217 crate::OperationError<crate::GenericRejection>,
218 > {
219 let account_id_encoded = crate::client::segment(account_id.as_str());
220 let path = format!("/v3/accounts/{account_id_encoded}/transactions/idrange");
221 self.execute(RequestParts::new(Method::GET, &path).query(query)?)
222 .await
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct TransactionsSinceIdQuery {
230 #[serde(rename = "id")]
233 pub id: TransactionID,
234 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
236 pub r#type: Option<Vec<TransactionFilter>>,
237}
238
239impl Validate for TransactionsSinceIdQuery {
240 fn validate(&self) -> Result<()> {
241 Ok(())
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct TransactionsSinceIdResponse {
249 #[serde(
251 rename = "transactions",
252 default,
253 skip_serializing_if = "Option::is_none"
254 )]
255 pub transactions: Option<Vec<Transaction>>,
256 #[serde(
258 rename = "lastTransactionID",
259 default,
260 skip_serializing_if = "Option::is_none"
261 )]
262 pub last_transaction_id: Option<TransactionID>,
263}
264
265impl Client {
266 pub async fn transactions_since_id(
274 &self,
275 account_id: &AccountID,
276 query: &TransactionsSinceIdQuery,
277 ) -> std::result::Result<
278 crate::ApiResponse<TransactionsSinceIdResponse>,
279 crate::OperationError<crate::GenericRejection>,
280 > {
281 let account_id_encoded = crate::client::segment(account_id.as_str());
282 let path = format!("/v3/accounts/{account_id_encoded}/transactions/sinceid");
283 self.execute(RequestParts::new(Method::GET, &path).query(query)?)
284 .await
285 }
286}