quantus_cli/subsquid/
client.rs1use crate::error::{QuantusError, Result};
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6
7use super::types::{GraphQLResponse, Transfer, TransferQueryParams, TransfersByPrefixResult};
8
9pub struct SubsquidClient {
11 url: String,
12 http_client: Client,
13}
14
15#[derive(Serialize)]
16struct GraphQLRequest {
17 query: String,
18 variables: serde_json::Value,
19}
20
21#[derive(Deserialize)]
22struct TransfersByHashPrefixData {
23 #[serde(rename = "transfersByHashPrefix")]
24 transfers_by_hash_prefix: TransfersByPrefixResult,
25}
26
27impl SubsquidClient {
28 pub fn new(url: String) -> Result<Self> {
34 let http_client = Client::builder()
35 .build()
36 .map_err(|e| QuantusError::Generic(format!("Failed to create HTTP client: {}", e)))?;
37
38 Ok(Self { url, http_client })
39 }
40
41 pub async fn query_transfers_by_prefix(
56 &self,
57 to_prefixes: Option<Vec<String>>,
58 from_prefixes: Option<Vec<String>>,
59 params: TransferQueryParams,
60 ) -> Result<Vec<Transfer>> {
61 let query = r#"
63 query TransfersByHashPrefix($input: TransfersByPrefixInput!) {
64 transfersByHashPrefix(input: $input) {
65 transfers {
66 id
67 blockId
68 blockHeight
69 timestamp
70 extrinsicHash
71 fromId
72 toId
73 amount
74 fee
75 fromHash
76 toHash
77 }
78 totalCount
79 }
80 }
81 "#;
82
83 let mut input = serde_json::json!({
85 "limit": params.limit,
86 "offset": params.offset,
87 });
88
89 if let Some(prefixes) = to_prefixes {
90 input["toHashPrefixes"] = serde_json::json!(prefixes);
91 }
92
93 if let Some(prefixes) = from_prefixes {
94 input["fromHashPrefixes"] = serde_json::json!(prefixes);
95 }
96
97 if let Some(block) = params.after_block {
98 input["afterBlock"] = serde_json::json!(block);
99 }
100
101 if let Some(block) = params.before_block {
102 input["beforeBlock"] = serde_json::json!(block);
103 }
104
105 if let Some(amount) = params.min_amount {
106 input["minAmount"] = serde_json::json!(amount.to_string());
107 }
108
109 if let Some(amount) = params.max_amount {
110 input["maxAmount"] = serde_json::json!(amount.to_string());
111 }
112
113 let request = GraphQLRequest {
114 query: query.to_string(),
115 variables: serde_json::json!({ "input": input }),
116 };
117
118 let response = self
120 .http_client
121 .post(&self.url)
122 .json(&request)
123 .send()
124 .await
125 .map_err(|e| QuantusError::Generic(format!("Failed to send request: {}", e)))?;
126
127 if !response.status().is_success() {
128 let status = response.status();
129 let body = response.text().await.unwrap_or_default();
130 return Err(QuantusError::Generic(format!(
131 "Subsquid request failed with status {}: {}",
132 status, body
133 )));
134 }
135
136 let graphql_response: GraphQLResponse<TransfersByHashPrefixData> = response
137 .json()
138 .await
139 .map_err(|e| QuantusError::Generic(format!("Failed to parse response: {}", e)))?;
140
141 if let Some(errors) = graphql_response.errors {
143 let error_messages: Vec<String> = errors.iter().map(|e| e.message.clone()).collect();
144 return Err(QuantusError::Generic(format!(
145 "GraphQL errors: {}",
146 error_messages.join("; ")
147 )));
148 }
149
150 let data = graphql_response
152 .data
153 .ok_or_else(|| QuantusError::Generic("No data in response".to_string()))?;
154
155 Ok(data.transfers_by_hash_prefix.transfers)
156 }
157
158 pub async fn query_transfers_for_addresses(
175 &self,
176 addresses: &[[u8; 32]],
177 prefix_len: usize,
178 params: TransferQueryParams,
179 ) -> Result<Vec<Transfer>> {
180 use super::hash::{compute_address_hash, get_hash_prefix};
181 use std::collections::HashSet;
182
183 if addresses.is_empty() {
184 return Ok(vec![]);
185 }
186
187 let address_hashes: HashSet<String> = addresses.iter().map(compute_address_hash).collect();
189
190 let prefixes: Vec<String> = address_hashes
191 .iter()
192 .map(|h| get_hash_prefix(h, prefix_len))
193 .collect::<HashSet<_>>()
194 .into_iter()
195 .collect();
196
197 let transfers = self
199 .query_transfers_by_prefix(Some(prefixes.clone()), Some(prefixes), params)
200 .await?;
201
202 let filtered: Vec<Transfer> = transfers
204 .into_iter()
205 .filter(|t| {
206 address_hashes.contains(&t.to_hash) || address_hashes.contains(&t.from_hash)
207 })
208 .collect();
209
210 Ok(filtered)
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_transfer_query_params_builder() {
220 let params = TransferQueryParams::new()
221 .with_limit(50)
222 .with_offset(10)
223 .with_after_block(1000)
224 .with_before_block(2000);
225
226 assert_eq!(params.limit, 50);
227 assert_eq!(params.offset, 10);
228 assert_eq!(params.after_block, Some(1000));
229 assert_eq!(params.before_block, Some(2000));
230 }
231}