Skip to main content

quantus_cli/subsquid/
types.rs

1//! Types for Subsquid API responses.
2
3use serde::{Deserialize, Serialize};
4
5/// A transfer as returned by the Subsquid indexer.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct Transfer {
9	/// Unique identifier
10	pub id: String,
11
12	/// Block ID
13	pub block_id: String,
14
15	/// Block height
16	pub block_height: i64,
17
18	/// Timestamp of the transfer
19	pub timestamp: String,
20
21	/// Extrinsic hash (if available)
22	pub extrinsic_hash: Option<String>,
23
24	/// Sender address (SS58 format)
25	pub from_id: String,
26
27	/// Recipient address (SS58 format)
28	pub to_id: String,
29
30	/// Transfer amount (as string to handle large numbers)
31	pub amount: String,
32
33	/// Transaction fee
34	pub fee: String,
35
36	/// Blake3 hash of the sender's raw address
37	pub from_hash: String,
38
39	/// Blake3 hash of the recipient's raw address
40	pub to_hash: String,
41}
42
43/// Result from a prefix query.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct TransfersByPrefixResult {
47	/// Matching transfers
48	pub transfers: Vec<Transfer>,
49
50	/// Total count of matches
51	pub total_count: i64,
52}
53
54/// GraphQL response wrapper.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct GraphQLResponse<T> {
57	pub data: Option<T>,
58	pub errors: Option<Vec<GraphQLError>>,
59}
60
61/// GraphQL error.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct GraphQLError {
64	pub message: String,
65	pub locations: Option<Vec<GraphQLErrorLocation>>,
66	pub path: Option<Vec<serde_json::Value>>,
67}
68
69/// GraphQL error location.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct GraphQLErrorLocation {
72	pub line: i64,
73	pub column: i64,
74}
75
76/// Query parameters for transfer prefix queries.
77#[derive(Debug, Clone, Default)]
78pub struct TransferQueryParams {
79	/// Minimum block number (inclusive)
80	pub after_block: Option<u32>,
81
82	/// Maximum block number (inclusive)
83	pub before_block: Option<u32>,
84
85	/// Minimum transfer amount
86	pub min_amount: Option<u128>,
87
88	/// Maximum transfer amount
89	pub max_amount: Option<u128>,
90
91	/// Maximum number of results
92	pub limit: u32,
93
94	/// Offset for pagination
95	pub offset: u32,
96}
97
98impl TransferQueryParams {
99	pub fn new() -> Self {
100		Self { limit: 100, offset: 0, ..Default::default() }
101	}
102
103	pub fn with_limit(mut self, limit: u32) -> Self {
104		self.limit = limit;
105		self
106	}
107
108	#[allow(dead_code)]
109	pub fn with_offset(mut self, offset: u32) -> Self {
110		self.offset = offset;
111		self
112	}
113
114	pub fn with_after_block(mut self, block: u32) -> Self {
115		self.after_block = Some(block);
116		self
117	}
118
119	pub fn with_before_block(mut self, block: u32) -> Self {
120		self.before_block = Some(block);
121		self
122	}
123
124	pub fn with_min_amount(mut self, amount: u128) -> Self {
125		self.min_amount = Some(amount);
126		self
127	}
128
129	#[allow(dead_code)]
130	pub fn with_max_amount(mut self, amount: u128) -> Self {
131		self.max_amount = Some(amount);
132		self
133	}
134}
135
136#[cfg(test)]
137mod tests {
138	use super::*;
139
140	#[test]
141	fn test_transfer_query_params_default() {
142		let params = TransferQueryParams::default();
143		assert_eq!(params.limit, 0);
144		assert_eq!(params.offset, 0);
145		assert!(params.after_block.is_none());
146		assert!(params.before_block.is_none());
147		assert!(params.min_amount.is_none());
148		assert!(params.max_amount.is_none());
149	}
150
151	#[test]
152	fn test_transfer_query_params_new() {
153		let params = TransferQueryParams::new();
154		assert_eq!(params.limit, 100);
155		assert_eq!(params.offset, 0);
156	}
157
158	#[test]
159	fn test_transfer_query_params_builder() {
160		let params = TransferQueryParams::new()
161			.with_limit(50)
162			.with_offset(10)
163			.with_after_block(1000)
164			.with_before_block(2000)
165			.with_min_amount(1_000_000)
166			.with_max_amount(10_000_000);
167
168		assert_eq!(params.limit, 50);
169		assert_eq!(params.offset, 10);
170		assert_eq!(params.after_block, Some(1000));
171		assert_eq!(params.before_block, Some(2000));
172		assert_eq!(params.min_amount, Some(1_000_000));
173		assert_eq!(params.max_amount, Some(10_000_000));
174	}
175
176	#[test]
177	fn test_transfer_deserialization() {
178		let json = r#"{
179            "id": "transfer-123",
180            "blockId": "block-456",
181            "blockHeight": 12345,
182            "timestamp": "2024-01-15T12:30:00Z",
183            "extrinsicHash": "0xabcd1234",
184            "fromId": "qzAlice123",
185            "toId": "qzBob456",
186            "amount": "1000000000000",
187            "fee": "1000000",
188            "fromHash": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234",
189            "toHash": "5678efgh5678efgh5678efgh5678efgh5678efgh5678efgh5678efgh5678efgh"
190        }"#;
191
192		let transfer: Transfer = serde_json::from_str(json).expect("should deserialize");
193
194		assert_eq!(transfer.id, "transfer-123");
195		assert_eq!(transfer.block_id, "block-456");
196		assert_eq!(transfer.block_height, 12345);
197		assert_eq!(transfer.timestamp, "2024-01-15T12:30:00Z");
198		assert_eq!(transfer.extrinsic_hash, Some("0xabcd1234".to_string()));
199		assert_eq!(transfer.from_id, "qzAlice123");
200		assert_eq!(transfer.to_id, "qzBob456");
201		assert_eq!(transfer.amount, "1000000000000");
202		assert_eq!(transfer.fee, "1000000");
203	}
204
205	#[test]
206	fn test_transfer_deserialization_null_extrinsic_hash() {
207		let json = r#"{
208            "id": "transfer-123",
209            "blockId": "block-456",
210            "blockHeight": 12345,
211            "timestamp": "2024-01-15T12:30:00Z",
212            "extrinsicHash": null,
213            "fromId": "qzAlice123",
214            "toId": "qzBob456",
215            "amount": "1000000000000",
216            "fee": "1000000",
217            "fromHash": "abcd1234",
218            "toHash": "5678efgh"
219        }"#;
220
221		let transfer: Transfer = serde_json::from_str(json).expect("should deserialize");
222		assert!(transfer.extrinsic_hash.is_none());
223	}
224
225	#[test]
226	fn test_graphql_response_with_data() {
227		let json = r#"{
228            "data": {
229                "transfers": [],
230                "totalCount": 0
231            }
232        }"#;
233
234		let response: GraphQLResponse<TransfersByPrefixResult> =
235			serde_json::from_str(json).expect("should deserialize");
236
237		assert!(response.data.is_some());
238		assert!(response.errors.is_none());
239		assert_eq!(response.data.unwrap().total_count, 0);
240	}
241
242	#[test]
243	fn test_graphql_response_with_error() {
244		let json = r#"{
245            "data": null,
246            "errors": [
247                {
248                    "message": "Query returned too many results",
249                    "locations": [{"line": 1, "column": 1}],
250                    "path": ["transfersByHashPrefix"]
251                }
252            ]
253        }"#;
254
255		let response: GraphQLResponse<TransfersByPrefixResult> =
256			serde_json::from_str(json).expect("should deserialize");
257
258		assert!(response.data.is_none());
259		assert!(response.errors.is_some());
260
261		let errors = response.errors.unwrap();
262		assert_eq!(errors.len(), 1);
263		assert_eq!(errors[0].message, "Query returned too many results");
264	}
265}