use crate::error::{Error, Result};
use crate::tx::track::{fetch_tx_result, parse_tx_response, TxResult};
use serde_json::Value;
pub async fn get_tx(rest_url: &str, hash: &str) -> Result<TxResult> {
let url = format!(
"{}/cosmos/tx/v1beta1/txs/{hash}",
rest_url.trim_end_matches('/')
);
let http = reqwest::Client::new();
match fetch_tx_result(&http, &url).await? {
Some(result) => Ok(result),
None => Err(Error::InvalidResponse(format!("tx {hash} not found"))),
}
}
pub async fn get_block(rest_url: &str, height: i64) -> Result<Value> {
let url = format!(
"{}/cosmos/base/tendermint/v1beta1/blocks/{height}",
rest_url.trim_end_matches('/')
);
get_json(&url).await
}
pub async fn get_latest_block(rest_url: &str) -> Result<Value> {
let url = format!(
"{}/cosmos/base/tendermint/v1beta1/blocks/latest",
rest_url.trim_end_matches('/')
);
get_json(&url).await
}
#[derive(Debug, Clone)]
pub struct TxSearchResult {
pub txs: Vec<TxResult>,
pub total: u64,
pub raw: Value,
}
pub async fn search_txs(
rest_url: &str,
events: &[&str],
page: u64,
limit: u64,
) -> Result<TxSearchResult> {
let page = if page == 0 { 1 } else { page };
let limit = if limit == 0 { 100 } else { limit };
let query = build_event_query(events);
if query.is_empty() {
return Err(Error::InvalidResponse(
"at least one event predicate is required".into(),
));
}
let http = reqwest::Client::new();
let resp = http
.get(format!(
"{}/cosmos/tx/v1beta1/txs",
rest_url.trim_end_matches('/')
))
.header("Accept", "application/json")
.query(&[
("query", query.as_str()),
("page", &page.to_string()),
("limit", &limit.to_string()),
])
.send()
.await?;
let status = resp.status();
let url = resp.url().to_string();
let body = resp.text().await?;
if !status.is_success() {
return Err(Error::Http {
status: status.as_u16(),
url,
body,
});
}
let v: Value =
serde_json::from_str(&body).map_err(|e| Error::InvalidResponse(e.to_string()))?;
Ok(parse_tx_search(v))
}
pub fn build_event_query(events: &[&str]) -> String {
let mut parts: Vec<String> = Vec::with_capacity(events.len());
for e in events {
let e = e.trim();
if e.is_empty() {
continue;
}
match e.split_once('=') {
Some((key, value)) => {
let value = value.trim();
let quoted = if value.starts_with('\'') || value.starts_with('"') {
value.to_string()
} else {
format!("'{value}'")
};
parts.push(format!("{}={}", key.trim(), quoted));
}
None => parts.push(e.to_string()),
}
}
parts.join(" AND ")
}
fn parse_tx_search(v: Value) -> TxSearchResult {
let mut txs = Vec::new();
if let Some(arr) = v["tx_responses"].as_array() {
for entry in arr {
let wrapped = serde_json::json!({ "tx_response": entry });
txs.push(parse_tx_response(&wrapped));
}
}
let total = match (&v["total"], &v["pagination"]["total"]) {
(Value::String(s), _) if !s.is_empty() => s.parse().unwrap_or(0),
(_, Value::String(s)) if !s.is_empty() => s.parse().unwrap_or(0),
_ => 0,
};
TxSearchResult { txs, total, raw: v }
}
async fn get_json(url: &str) -> Result<Value> {
let http = reqwest::Client::new();
let resp = http
.get(url)
.header("Accept", "application/json")
.send()
.await?;
let status = resp.status();
let body = resp.text().await?;
if !status.is_success() {
return Err(Error::Http {
status: status.as_u16(),
url: url.to_string(),
body,
});
}
serde_json::from_str(&body).map_err(|e| Error::InvalidResponse(e.to_string()))
}