diff --git a/Cargo.lock b/Cargo.lock index 1d6dfd8..f332f6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,9 +1020,9 @@ checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -2096,9 +2096,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", diff --git a/src/horizon/client.rs b/src/horizon/client.rs new file mode 100644 index 0000000..e236ca1 --- /dev/null +++ b/src/horizon/client.rs @@ -0,0 +1,160 @@ +use reqwest::Client; +use serde::Deserialize; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum HorizonError { + #[error("reqwest error: {0}")] + Reqwest(#[from] reqwest::Error), + + #[error("json error: {0}")] + Json(#[from] serde_json::Error), + + #[error("http error {0}: {1}")] + Http(u16, String), + + #[error("other: {0}")] + Other(String), +} + +#[derive(Clone)] +pub struct HorizonClient { + base_url: String, + client: Client, +} + +impl HorizonClient { + pub fn new(base_url: impl Into) -> Self { + let client = Client::builder().build().expect("reqwest client"); + Self { + base_url: base_url.into().trim_end_matches('/').to_string(), + client, + } + } + + /// Convenience constructor for public testnet Horizon + pub fn public_testnet() -> Self { + Self::new("https://horizon-testnet.stellar.org") + } + + async fn get_json Deserialize<'de>>(&self, path: &str) -> Result { + let url = format!("{}{}", self.base_url, path); + let resp = self.client.get(&url).send().await?; + let status = resp.status(); + let text = resp.text().await?; + if !status.is_success() { + return Err(HorizonError::Http(status.as_u16(), text)); + } + let parsed = serde_json::from_str(&text)?; + Ok(parsed) + } + + pub async fn get_account(&self, address: &str) -> Result { + let path = format!("/accounts/{}", address); + self.get_json(&path).await + } + + pub async fn get_transactions( + &self, + address: &str, + cursor: Option<&str>, + ) -> Result { + let mut path = format!("/accounts/{}/transactions?limit=10&order=desc", address); + if let Some(c) = cursor { + path.push_str("&cursor="); + path.push_str(c); + } + self.get_json(&path).await + } + + pub async fn get_payments( + &self, + address: &str, + cursor: Option<&str>, + ) -> Result { + let mut path = format!("/accounts/{}/payments?limit=10&order=desc", address); + if let Some(c) = cursor { + path.push_str("&cursor="); + path.push_str(c); + } + self.get_json(&path).await + } + + pub async fn get_transaction(&self, hash: &str) -> Result { + let path = format!("/transactions/{}", hash); + self.get_json(&path).await + } +} + +// ---- Response structs (minimal, expand as needed) ---- + +#[derive(Debug, Deserialize)] +pub struct Balance { + pub asset_type: String, + #[serde(default)] + pub asset_code: Option, + pub balance: String, +} + +#[derive(Debug, Deserialize)] +pub struct AccountResponse { + pub id: String, + pub account_id: String, + pub sequence: String, + pub balances: Vec, + #[serde(flatten)] + pub extra: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +pub struct TransactionSummary { + pub id: String, + pub paging_token: String, + pub hash: Option, + pub created_at: Option, + pub source_account: Option, + #[serde(flatten)] + pub extra: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +pub struct TransactionPage { + pub _embedded: EmbeddedTransactions, +} + +#[derive(Debug, Deserialize)] +pub struct EmbeddedTransactions { + pub records: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct PaymentSummary { + pub id: String, + pub paging_token: String, + pub source_account: Option, + pub type_: Option, + #[serde(rename = "type_i")] + pub type_i: Option, + #[serde(flatten)] + pub extra: serde_json::Value, +} + +#[derive(Debug, Deserialize)] +pub struct PaymentPage { + pub _embedded: EmbeddedPayments, +} + +#[derive(Debug, Deserialize)] +pub struct EmbeddedPayments { + pub records: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct TransactionDetail { + pub id: String, + pub hash: String, + pub ledger: Option, + pub created_at: Option, + #[serde(flatten)] + pub extra: serde_json::Value, +} diff --git a/src/horizon/mod.rs b/src/horizon/mod.rs new file mode 100644 index 0000000..54786c0 --- /dev/null +++ b/src/horizon/mod.rs @@ -0,0 +1,3 @@ +pub mod client; + +pub use client::{HorizonClient, HorizonError}; diff --git a/src/main.rs b/src/main.rs index f6c4fc2..56398b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ -pub mod friendbot; -mod setup; -pub mod utils; - -fn main() {} +pub mod friendbot; +pub mod horizon; +mod setup; +pub mod utils; + +fn main() {} diff --git a/src/setup/token_setup.rs b/src/setup/token_setup.rs index 8e000ff..9b436ad 100644 --- a/src/setup/token_setup.rs +++ b/src/setup/token_setup.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::fmt; use std::process::Command;