From 3a9b4643c598ae83c53584fa72c77b4b926502bb Mon Sep 17 00:00:00 2001 From: Isenewo Oluwaseyi Date: Wed, 4 Mar 2026 20:02:51 +0100 Subject: [PATCH] feat: Setup Horizon API Client with reqwest --- Cargo.lock | 227 +++++++++++++++++++++++++++++++++++++++ src/horizon/client.rs | 160 +++++++++++++++++++++++++++ src/horizon/mod.rs | 3 + src/main.rs | 11 +- src/setup/token_setup.rs | 2 + 5 files changed, 398 insertions(+), 5 deletions(-) create mode 100644 src/horizon/client.rs create mode 100644 src/horizon/mod.rs diff --git a/Cargo.lock b/Cargo.lock index da9b626..fc5b1b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,18 +14,39 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -54,13 +75,21 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "contract-fox" version = "0.1.0" dependencies = [ + "ed25519-dalek", "reqwest", "serde", "serde_json", + "stellar-strkey", "thiserror", "tokio", ] @@ -91,6 +120,83 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crate-git-revision" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c521bf1f43d31ed2f73441775ed31935d77901cb3451e44b38a1c1612fcbaf98" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -102,6 +208,30 @@ dependencies = [ "syn", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -133,6 +263,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -224,6 +360,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -696,6 +842,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -745,6 +901,15 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -801,6 +966,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -952,12 +1126,32 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "slab" version = "0.4.12" @@ -980,12 +1174,33 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stellar-strkey" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" +dependencies = [ + "base32", + "crate-git-revision", + "thiserror", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1216,6 +1431,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1258,6 +1479,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" 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;