1use anyhow::{Context, Result};
29use async_trait::async_trait;
30use serde_json::Value;
31use snarkvm::ledger::query::QueryTrait;
32use snarkvm::prelude::{Field, Network, Program, StatePath, TestnetV0};
33use std::io::Write;
34use std::str::FromStr;
35
36const UA: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
38
39const JSON_RPC_URL: &str = "https://testnetbeta.aleorpc.com";
41
42#[derive(Clone, Debug)]
44pub struct AleoHttpClient {
45 pub base_url: String,
47 rpc_url: String,
49 inner: reqwest::Client,
50}
51
52impl AleoHttpClient {
53 pub fn new(base_url: &str) -> Result<Self> {
55 Self::new_with_rpc(base_url, JSON_RPC_URL)
56 }
57
58 pub fn new_with_rpc(base_url: &str, rpc_url: &str) -> Result<Self> {
60 let mut builder = reqwest::Client::builder()
61 .http1_only()
62 .danger_accept_invalid_certs(true)
63 .timeout(std::time::Duration::from_secs(120));
64
65 if let Ok(proxy_url) = std::env::var("HTTP_PROXY")
69 .or_else(|_| std::env::var("http_proxy"))
70 .or_else(|_| std::env::var("HTTPS_PROXY"))
71 .or_else(|_| std::env::var("https_proxy"))
72 {
73 if let Ok(proxy) =
74 reqwest::Proxy::http(&proxy_url).or_else(|_| reqwest::Proxy::all(&proxy_url))
75 {
76 builder = builder.proxy(proxy);
77 }
78 }
79
80 let inner = builder.build().context("Failed to build HTTP client")?;
81 Ok(Self {
82 base_url: base_url.trim_end_matches('/').to_string(),
83 rpc_url: rpc_url.trim_end_matches('/').to_string(),
84 inner,
85 })
86 }
87
88 fn headers() -> reqwest::header::HeaderMap {
89 let mut h = reqwest::header::HeaderMap::new();
90 h.insert("User-Agent", UA.parse().unwrap());
91 h.insert(
92 "Accept",
93 "application/json, text/plain, */*".parse().unwrap(),
94 );
95 h
96 }
97
98 async fn json_rpc(&self, method: &str, params: Vec<Value>) -> Result<Value> {
100 let body = serde_json::json!({
101 "jsonrpc": "2.0",
102 "id": 1,
103 "method": method,
104 "params": params,
105 });
106 let mut headers = Self::headers();
107 headers.insert("Content-Type", "application/json".parse().unwrap());
108
109 let resp = self.inner.post(&self.rpc_url).headers(headers).json(&body).send().await?;
110 let text = resp.text().await?;
111 let v: Value = serde_json::from_str(&text).context("Failed to parse JSON-RPC response")?;
112
113 if let Some(err) = v.get("error") {
114 anyhow::bail!("JSON-RPC error ({method}): {err}");
115 }
116 v.get("result").cloned().context("JSON-RPC response missing result")
117 }
118
119 pub async fn fetch_program(&self, program_id: &str) -> Result<Program<TestnetV0>> {
121 let url = format!("{}/program/{program_id}", self.base_url);
122 tracing::info!("GET {url}");
123 let text = self.inner.get(&url).headers(Self::headers()).send().await?.text().await?;
124
125 let clean = text.trim_matches('"').replace("\\n", "\n");
126 Program::<TestnetV0>::from_str(&clean).context("Failed to parse program")
127 }
128
129 pub async fn fetch_state_root(&self) -> Result<(<TestnetV0 as Network>::StateRoot, u32)> {
131 let root_url = format!("{}/stateRoot/latest", self.base_url);
132 let text = self.inner.get(&root_url).headers(Self::headers()).send().await?.text().await?;
133 let root_str = text.trim_matches('"');
134 let state_root = <TestnetV0 as Network>::StateRoot::from_str(root_str)
135 .context("Failed to parse state root")?;
136
137 let height = self.fetch_block_height().await?;
138 Ok((state_root, height))
139 }
140
141 pub async fn broadcast_transaction(&self, tx_json: String) -> Result<String> {
143 let url = format!(
144 "{}/transaction/broadcast?check_transaction=true",
145 self.base_url
146 );
147 let mut headers = Self::headers();
148 headers.insert("Content-Type", "application/json".parse().unwrap());
149 headers.insert("Origin", "https://explorer.provable.com".parse().unwrap());
150 headers.insert("Referer", "https://explorer.provable.com/".parse().unwrap());
151
152 let resp = self.inner.post(&url).headers(headers).body(tx_json).send().await?;
153 let status = resp.status();
154 let body = resp.text().await.unwrap_or_default();
155
156 if !status.is_success() {
157 anyhow::bail!("Broadcast rejected ({status}): {body}");
158 }
159 Ok(body)
160 }
161
162 pub async fn fetch_transaction(&self, tx_id: &str) -> Result<String> {
165 let url = format!("{}/transaction/{tx_id}", self.base_url);
166 let res = self.inner.get(&url).headers(Self::headers()).send().await?;
167 let body = res.text().await?;
168 Ok(body)
169 }
170
171 pub async fn wait_for_confirmation(&self, tx_id: &str) -> Result<()> {
173 let check_url = format!("{}/transaction/{tx_id}", self.base_url);
174 tracing::info!("Waiting for confirmation... (polling every 5s)");
175
176 for _ in 1..=30 {
177 tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
178
179 match self.inner.get(&check_url).headers(Self::headers()).send().await {
180 Ok(res) if res.status().is_success() => {
181 tracing::info!("Confirmed on chain!");
182 tracing::info!("🔗 https://testnet.explorer.provable.com/transaction/{tx_id}");
183 return Ok(());
184 }
185 _ => {
186 print!(".");
187 std::io::stdout().flush().ok();
188 }
189 }
190 }
191 anyhow::bail!("Timed out waiting for confirmation of {tx_id}")
192 }
193
194 pub async fn fetch_mapping_value_rest(
200 &self,
201 program_id: &str,
202 mapping_name: &str,
203 key: &str,
204 ) -> Result<Option<u64>> {
205 let url = format!(
206 "{}/program/{program_id}/mapping/{mapping_name}/{key}",
207 self.base_url
208 );
209 let resp = self.inner.get(&url).headers(Self::headers()).send().await?;
210 if resp.status().is_client_error() {
211 return Ok(None); }
213 let text = resp.text().await?;
214 let cleaned = text.trim().trim_matches('"').replace("u64", "");
215 match cleaned.parse::<u64>() {
216 Ok(v) => Ok(Some(v)),
217 Err(e) => {
218 tracing::warn!("Failed to parse REST mapping value '{text}': {e}");
219 Ok(None)
220 }
221 }
222 }
223
224 pub async fn fetch_mapping_value(
228 &self,
229 program_id: &str,
230 mapping_name: &str,
231 key: &str,
232 ) -> Result<Option<String>> {
233 let result = self
234 .json_rpc(
235 "getMappingValue",
236 vec![
237 Value::String(program_id.to_string()),
238 Value::String(mapping_name.to_string()),
239 Value::String(key.to_string()),
240 ],
241 )
242 .await;
243
244 match result {
245 Ok(Value::String(s)) => Ok(Some(s)),
246 Ok(v) => Ok(Some(v.to_string())),
247 Err(e) => {
248 tracing::warn!("Mapping query note (key may not exist): {e}");
249 Ok(None)
250 }
251 }
252 }
253
254 pub async fn fetch_block_height(&self) -> Result<u32> {
256 let url = format!("{}/block/height/latest", self.base_url);
257 let text = self.inner.get(&url).headers(Self::headers()).send().await?.text().await?;
258 Ok(text.trim().parse()?)
259 }
260
261 pub async fn fetch_state_root_only(&self) -> Result<<TestnetV0 as Network>::StateRoot> {
263 let url = format!("{}/stateRoot/latest", self.base_url);
264 let text = self.inner.get(&url).headers(Self::headers()).send().await?.text().await?;
265 <TestnetV0 as Network>::StateRoot::from_str(text.trim_matches('"'))
266 .context("Failed to parse state root")
267 }
268
269 pub async fn fetch_records(&self, view_key: &str) -> Result<String> {
271 let height = self.fetch_block_height().await?;
272 let start = height.saturating_sub(1000);
273
274 let result = self
275 .json_rpc(
276 "records/isOwner",
277 vec![
278 Value::String(view_key.to_string()),
279 Value::Number(serde_json::Number::from(start)),
280 Value::Number(serde_json::Number::from(height)),
281 ],
282 )
283 .await?;
284
285 Ok(serde_json::to_string_pretty(&result)?)
286 }
287
288 pub async fn fetch_all_records(
292 &self,
293 start: u32,
294 end: u32,
295 page: u32,
296 per_page: u32,
297 ) -> Result<Value> {
298 let params = serde_json::json!({
299 "start": start,
300 "end": end,
301 "page": page,
302 "recordsPerRequest": per_page,
303 });
304 let body = serde_json::json!({
305 "jsonrpc": "2.0",
306 "id": 1,
307 "method": "records/all",
308 "params": params,
309 });
310 let mut headers = Self::headers();
311 headers.insert("Content-Type", "application/json".parse().unwrap());
312
313 let resp = self.inner.post(&self.rpc_url).headers(headers).json(&body).send().await?;
314 let text = resp.text().await?;
315 let v: Value =
316 serde_json::from_str(&text).context("Failed to parse records/all response")?;
317 if let Some(err) = v.get("error") {
318 anyhow::bail!("records/all error: {err}");
319 }
320 v.get("result").cloned().context("records/all response missing result")
321 }
322
323 pub async fn find_private_credits_records(&self, view_key: &str) -> Result<Vec<(String, u64)>> {
327 use snarkvm::console::program::Record;
328 use snarkvm::prelude::Ciphertext;
329 use std::str::FromStr;
330
331 let vk = snarkvm::prelude::ViewKey::<TestnetV0>::from_str(view_key)?;
332 let owner_addr = vk.to_address();
333
334 let height = self.fetch_block_height().await?;
335 let start = height.saturating_sub(100_000); let mut results = Vec::new();
337
338 tracing::info!("Scanning blocks {start}..{height} for private records...");
339 let records = self.fetch_all_records(start, height, 0, 500).await?;
340
341 if let Some(arr) = records.as_array() {
342 for record_entry in arr {
343 let program_id = record_entry["program_id"].as_str().unwrap_or("");
344 if program_id != "credits.aleo" {
345 continue;
346 }
347 let ciphertext_str = record_entry["record_ciphertext"].as_str().unwrap_or("");
348 if ciphertext_str.is_empty() {
349 continue;
350 }
351 if let Ok(record) =
353 Record::<TestnetV0, Ciphertext<TestnetV0>>::from_str(ciphertext_str)
354 {
355 if let Ok(decrypted) = record.decrypt(&vk) {
356 if *decrypted.owner() == snarkvm::prelude::Owner::Public(owner_addr)
358 || *decrypted.owner()
359 == snarkvm::prelude::Owner::Private(
360 snarkvm::prelude::Plaintext::from(
361 snarkvm::prelude::Literal::Address(owner_addr),
362 ),
363 )
364 {
365 for (id, entry) in decrypted.data().iter() {
367 if id.to_string() == "microcredits" {
368 let amount_str =
369 entry.to_string().replace("u64", "").trim().to_string();
370 if let Ok(amount) = amount_str.parse::<u64>() {
371 results.push((ciphertext_str.to_string(), amount));
372 }
373 }
374 }
375 }
376 }
377 }
378 }
379 }
380
381 results.sort_by_key(|a| std::cmp::Reverse(a.1));
383 Ok(results)
384 }
385}
386
387#[derive(Clone, Debug)]
389pub struct FixedStateRootQuery<N: Network> {
390 pub state_root: N::StateRoot,
391 pub block_height: u32,
392}
393
394#[async_trait(?Send)]
395impl<N: Network> QueryTrait<N> for FixedStateRootQuery<N> {
396 fn current_state_root(&self) -> Result<N::StateRoot> {
397 Ok(self.state_root)
398 }
399 fn current_block_height(&self) -> Result<u32> {
400 Ok(self.block_height)
401 }
402 fn get_state_path_for_commitment(&self, _commitment: &Field<N>) -> Result<StatePath<N>> {
403 StatePath::from_str("").or_else(|_| anyhow::bail!("State path not available"))
404 }
405 fn get_state_paths_for_commitments(
406 &self,
407 _commitments: &[Field<N>],
408 ) -> Result<Vec<StatePath<N>>> {
409 Ok(Vec::new())
410 }
411 async fn current_state_root_async(&self) -> Result<N::StateRoot> {
412 Ok(self.state_root)
413 }
414 async fn current_block_height_async(&self) -> Result<u32> {
415 Ok(self.block_height)
416 }
417 async fn get_state_path_for_commitment_async(
418 &self,
419 _commitment: &Field<N>,
420 ) -> Result<StatePath<N>> {
421 StatePath::from_str("").or_else(|_| anyhow::bail!("State path not available"))
422 }
423 async fn get_state_paths_for_commitments_async(
424 &self,
425 _commitments: &[Field<N>],
426 ) -> Result<Vec<StatePath<N>>> {
427 Ok(Vec::new())
428 }
429}
430
431#[derive(Debug)]
438pub struct ProvableQuery {
439 pub state_root: <TestnetV0 as Network>::StateRoot,
440 pub block_height: u32,
441 pub api_base_url: String,
443 last_state_root: std::sync::Mutex<Option<<TestnetV0 as Network>::StateRoot>>,
447}
448
449impl Clone for ProvableQuery {
450 fn clone(&self) -> Self {
451 Self {
452 state_root: self.state_root,
453 block_height: self.block_height,
454 api_base_url: self.api_base_url.clone(),
455 last_state_root: std::sync::Mutex::new(None),
458 }
459 }
460}
461
462impl ProvableQuery {
463 pub fn new(
464 state_root: <TestnetV0 as Network>::StateRoot,
465 block_height: u32,
466 api_base_url: &str,
467 ) -> Self {
468 Self {
469 state_root,
470 block_height,
471 api_base_url: api_base_url.trim_end_matches('/').to_string(),
472 last_state_root: std::sync::Mutex::new(None),
473 }
474 }
475}
476
477#[async_trait(?Send)]
478impl QueryTrait<TestnetV0> for ProvableQuery {
479 fn current_state_root(&self) -> Result<<TestnetV0 as Network>::StateRoot> {
480 let guard = self.last_state_root.lock().unwrap();
484 match *guard {
485 Some(root) => Ok(root),
486 None => Ok(self.state_root),
487 }
488 }
489 fn current_block_height(&self) -> Result<u32> {
490 Ok(self.block_height)
491 }
492 fn get_state_path_for_commitment(&self, commitment: &Field<TestnetV0>) -> Result<StatePath<TestnetV0>> {
493 let url = format!("{}/statePath/{commitment}", self.api_base_url);
494 let response = ureq::get(&url)
495 .set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36")
496 .set("Accept", "application/json")
497 .call()
498 .map_err(|e| anyhow::anyhow!("Provable statePath GET {url} failed: {e}"))?;
499 let body = response.into_string()
500 .map_err(|e| anyhow::anyhow!("Failed to read statePath response from {url}: {e}"))?;
501 let trimmed = body.trim().trim_matches('"');
503 let path = StatePath::<TestnetV0>::from_str(trimmed)
504 .map_err(|e| anyhow::anyhow!("Failed to parse state path from '{trimmed}': {e}"))?;
505 let path_root = path.global_state_root();
508 let mut guard = self.last_state_root.lock().unwrap();
509 *guard = Some(path_root);
510 Ok(path)
511 }
512 fn get_state_paths_for_commitments(
513 &self,
514 commitments: &[Field<TestnetV0>],
515 ) -> Result<Vec<StatePath<TestnetV0>>> {
516 if commitments.is_empty() {
517 return Ok(Vec::new());
518 }
519 let cm_strings: Vec<String> = commitments.iter().map(|c| c.to_string()).collect();
520 let url = format!("{}/statePaths?commitments={}", self.api_base_url, cm_strings.join(","));
521 let response = ureq::get(&url)
522 .set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36")
523 .set("Accept", "application/json")
524 .call()
525 .map_err(|e| anyhow::anyhow!("Provable statePaths GET {url} failed: {e}"))?;
526 let body = response.into_string()
527 .map_err(|e| anyhow::anyhow!("Failed to read statePaths response from {url}: {e}"))?;
528 serde_json::from_str(&body)
529 .map_err(|e| anyhow::anyhow!("Failed to parse state paths from '{body}': {e}"))
530 }
531 async fn current_state_root_async(&self) -> Result<<TestnetV0 as Network>::StateRoot> {
532 Ok(self.state_root.clone())
533 }
534 async fn current_block_height_async(&self) -> Result<u32> {
535 Ok(self.block_height)
536 }
537 async fn get_state_path_for_commitment_async(
538 &self,
539 commitment: &Field<TestnetV0>,
540 ) -> Result<StatePath<TestnetV0>> {
541 self.get_state_path_for_commitment(commitment)
542 }
543 async fn get_state_paths_for_commitments_async(
544 &self,
545 commitments: &[Field<TestnetV0>],
546 ) -> Result<Vec<StatePath<TestnetV0>>> {
547 self.get_state_paths_for_commitments(commitments)
548 }
549}