From 6bdf1d0ec2c47277a269dcb6cb3d148c2191480b Mon Sep 17 00:00:00 2001 From: c8e4 Date: Fri, 7 Nov 2025 22:48:03 +0100 Subject: [PATCH 1/7] add get_balance --- src/clients/node.rs | 19 ++++++++++++++++++- src/types/ergo.rs | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/clients/node.rs b/src/clients/node.rs index 3291e40..dc60310 100644 --- a/src/clients/node.rs +++ b/src/clients/node.rs @@ -1,7 +1,10 @@ use serde::{self, Deserialize}; use tracing::{debug, error, info}; -use crate::types::{HashDigest, ergo::UnconfirmedTransaction}; +use crate::types::{ + HashDigest, + ergo::{Balance, Base58String, UnconfirmedTransaction}, +}; #[derive(Debug, thiserror::Error)] pub enum NodeError { @@ -108,6 +111,20 @@ impl NodeClient { Ok(resp) } + #[tracing::instrument(skip(self))] + pub async fn get_balance(&self, address: &Base58String) -> Result { + let url = self.build_url("blockchain/balance"); + let resp = self + .http_client + .post(&url) + .json(address) + .send() + .await? + .json() + .await?; + Ok(resp) + } + fn build_url(&self, path: &str) -> String { format!("{}/{}", self.base_url, path) } diff --git a/src/types/ergo.rs b/src/types/ergo.rs index 4359a13..49a169d 100644 --- a/src/types/ergo.rs +++ b/src/types/ergo.rs @@ -82,3 +82,28 @@ pub struct NonMandatoryRegisters { #[serde(rename = "R9")] pub r9: Option, } + +#[derive(Debug, Deserialize, Serialize)] +pub struct Balance { + pub confirmed: BalancePart, + pub unconfirmed: BalancePart, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct BalancePart { + #[serde(rename = "nanoErgs")] + pub nano_ergs: u64, + pub tokens: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct BalanceToken { + #[serde(flatten)] + pub token: Token, + pub decimals: u32, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct Base58String(pub String); From 19940fae82704df9f047092352189f2a62b38dbc Mon Sep 17 00:00:00 2001 From: c8e4 Date: Fri, 7 Nov 2025 22:48:37 +0100 Subject: [PATCH 2/7] add get_balance test --- README.md | 10 +++++++++- tests/test_node.rs | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/test_node.rs diff --git a/README.md b/README.md index 3ba7200..7da0f3b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # hergmes -Ergo node messaging layer. \ No newline at end of file +Ergo node messaging layer. + +### Running node integration tests + +Make sure `ERGO_NODE_URL` is set in your environment, then run: +```sh +cargo test --test test_node +``` + diff --git a/tests/test_node.rs b/tests/test_node.rs new file mode 100644 index 0000000..34c8653 --- /dev/null +++ b/tests/test_node.rs @@ -0,0 +1,22 @@ +use std::time::Duration; + +use dotenvy::dotenv; +use hergmes::{clients::node::NodeClient, env::ERGO_NODE_URL, types::ergo::Base58String}; + +#[tokio::test] +async fn test_node_balance() { + let _ = dotenv(); + + let http_client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap(); + + let node = NodeClient::new(http_client, &ERGO_NODE_URL); + + let address = Base58String("9hMDjzgnrwET8dweNnK3wKHJf7Vi3zWcKsFEEcdETdSie34BQ16".to_string()); + + let balance = node.get_balance(&address).await.unwrap(); + + assert!(balance.confirmed.nano_ergs >= 10); +} From ee6e84fa5e62fed6ccd299840721445f1992e92d Mon Sep 17 00:00:00 2001 From: c8e4 Date: Sat, 29 Nov 2025 17:52:54 +0100 Subject: [PATCH 3/7] get_unspent_boxes_by_ergo_tree --- src/clients/node.rs | 51 +++++++++++++++++++++++++++++++++++++++------ src/types/ergo.rs | 23 ++++++++++++++++++++ tests/test_node.rs | 28 +++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/clients/node.rs b/src/clients/node.rs index dc60310..2a8665d 100644 --- a/src/clients/node.rs +++ b/src/clients/node.rs @@ -1,9 +1,9 @@ -use serde::{self, Deserialize}; -use tracing::{debug, error, info}; +use serde::{self, Deserialize, Serialize}; +use tracing::{debug, info}; use crate::types::{ HashDigest, - ergo::{Balance, Base58String, UnconfirmedTransaction}, + ergo::{Balance, Base58String, NodeBox, UnconfirmedTransaction}, }; #[derive(Debug, thiserror::Error)] @@ -34,6 +34,16 @@ pub struct NodeClient { base_url: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct UnspentByErgoTreeQuery<'a> { + offset: u32, + limit: u32, + sort_direction: &'a str, + include_unconfirmed: bool, + exclude_mempool_spent: bool, +} + impl NodeClient { pub fn new(http_client: reqwest::Client, base_url: &str) -> Self { Self { @@ -54,7 +64,6 @@ impl NodeClient { let url = self.build_url("transactions/unconfirmed"); let resp = self.http_client.get(&url).send().await?.json().await?; debug!(response = ?resp, "Mempool transactions fetched."); - Ok(resp) } @@ -63,7 +72,6 @@ impl NodeClient { let url = self.build_url("info"); let response: InfoResponse = self.http_client.get(&url).send().await?.json().await?; debug!(?response, "Node info fetched."); - Ok(response) } @@ -83,7 +91,6 @@ impl NodeClient { } debug!(?index_status, "Node is fully indexed."); - Ok(()) } @@ -125,6 +132,38 @@ impl NodeClient { Ok(resp) } + #[tracing::instrument(skip(self))] + pub async fn get_unspent_boxes_by_ergo_tree( + &self, + ergo_tree_hex: &str, + offset: u32, + limit: u32, + sort_direction: &str, + include_unconfirmed: bool, + exclude_mempool_spent: bool, + ) -> Result, NodeError> { + let url = self.build_url("blockchain/box/unspent/byErgoTree"); + let query = UnspentByErgoTreeQuery { + offset, + limit, + sort_direction, + include_unconfirmed, + exclude_mempool_spent, + }; + + let resp = self + .http_client + .post(&url) + .query(&query) + .json(&ergo_tree_hex) + .send() + .await? + .json() + .await?; + + Ok(resp) + } + fn build_url(&self, path: &str) -> String { format!("{}/{}", self.base_url, path) } diff --git a/src/types/ergo.rs b/src/types/ergo.rs index 49a169d..8ac1041 100644 --- a/src/types/ergo.rs +++ b/src/types/ergo.rs @@ -83,6 +83,29 @@ pub struct NonMandatoryRegisters { pub r9: Option, } +#[derive(Debug, Deserialize, Serialize)] +pub struct BoxesResponse { + pub items: Vec, + pub total: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NodeBox { + #[serde(flatten)] + pub utxo: UTxO, + pub address: Base58String, + #[serde(rename = "spentTransactionId")] + pub spent_transaction_id: Option, + #[serde(rename = "spendingHeight")] + pub spending_height: Option, + #[serde(rename = "inclusionHeight")] + pub inclusion_height: u32, + #[serde(rename = "spendingProof")] + pub spending_proof: Option, + #[serde(rename = "globalIndex")] + pub global_index: u64, +} + #[derive(Debug, Deserialize, Serialize)] pub struct Balance { pub confirmed: BalancePart, diff --git a/tests/test_node.rs b/tests/test_node.rs index 34c8653..af8106d 100644 --- a/tests/test_node.rs +++ b/tests/test_node.rs @@ -20,3 +20,31 @@ async fn test_node_balance() { assert!(balance.confirmed.nano_ergs >= 10); } + +#[tokio::test] +async fn test_unspent_boxes_by_ergo_tree_one_box() { + let _ = dotenv(); + + let http_client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap(); + + let node = NodeClient::new(http_client, &ERGO_NODE_URL); + + let ergo_tree = "0008cd02232fb68248be44236ad6c43a3e9b602647163fd83ae10325a6713959fb19dacf"; + + let resp = node + .get_unspent_boxes_by_ergo_tree(ergo_tree, 0, 1, "desc", false, false) + .await + .unwrap(); + + assert_eq!(resp.len(), 1); + + let b = &resp[0]; + assert_eq!(b.utxo.ergo_tree.0, hex::decode(ergo_tree).unwrap()); + assert!(b.utxo.value > 0); + assert!(b.utxo.creation_height > 0); + assert!(b.inclusion_height > 0); + assert!(b.global_index > 0); +} From 414645e98b5db454443dabbb599370cb3baa360f Mon Sep 17 00:00:00 2001 From: c8e4 Date: Sat, 29 Nov 2025 18:15:52 +0100 Subject: [PATCH 4/7] get_unspent_boxes_by_token_id --- src/clients/node.rs | 34 ++++++++++++++++++++++++++++++++++ tests/test_node.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/clients/node.rs b/src/clients/node.rs index 2a8665d..72f2165 100644 --- a/src/clients/node.rs +++ b/src/clients/node.rs @@ -164,6 +164,40 @@ impl NodeClient { Ok(resp) } + #[tracing::instrument(skip(self))] + pub async fn get_unspent_boxes_by_token_id( + &self, + token_id_hex: &str, + offset: u32, + limit: u32, + sort_direction: &str, + include_unconfirmed: bool, + exclude_mempool_spent: bool, + ) -> Result, NodeError> { + let url = self.build_url(&format!( + "blockchain/box/unspent/byTokenId/{}", + token_id_hex + )); + let query = UnspentByErgoTreeQuery { + offset, + limit, + sort_direction, + include_unconfirmed, + exclude_mempool_spent, + }; + + let resp = self + .http_client + .get(&url) + .query(&query) + .send() + .await? + .json() + .await?; + + Ok(resp) + } + fn build_url(&self, path: &str) -> String { format!("{}/{}", self.base_url, path) } diff --git a/tests/test_node.rs b/tests/test_node.rs index af8106d..3f01df0 100644 --- a/tests/test_node.rs +++ b/tests/test_node.rs @@ -48,3 +48,32 @@ async fn test_unspent_boxes_by_ergo_tree_one_box() { assert!(b.inclusion_height > 0); assert!(b.global_index > 0); } + +#[tokio::test] +async fn test_unspent_boxes_by_token_id_one_box() { + let _ = dotenv(); + + let http_client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .unwrap(); + + let node = NodeClient::new(http_client, &ERGO_NODE_URL); + + let token_id = "cbd75cfe1a4f37f9a22eaee516300e36ea82017073036f07a09c1d2e10277cda"; + let token_bytes: [u8; 32] = hex::decode(token_id).unwrap().try_into().unwrap(); + + let resp = node + .get_unspent_boxes_by_token_id(token_id, 0, 1, "desc", false, false) + .await + .unwrap(); + + assert_eq!(resp.len(), 1); + + let b = &resp[0]; + assert!(b.utxo.tokens.iter().any(|t| t.id.0 == token_bytes)); + assert!(b.utxo.value > 0); + assert!(b.utxo.creation_height > 0); + assert!(b.inclusion_height > 0); + assert!(b.global_index > 0); +} From ba4d15ec39a7331388e655ae4327b9688d19f67c Mon Sep 17 00:00:00 2001 From: c8e4 Date: Sat, 29 Nov 2025 18:32:59 +0100 Subject: [PATCH 5/7] chore: whitespace --- src/clients/node.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/clients/node.rs b/src/clients/node.rs index 72f2165..1fbab8f 100644 --- a/src/clients/node.rs +++ b/src/clients/node.rs @@ -56,6 +56,7 @@ impl NodeClient { pub async fn get_indexed_height(&self) -> Result { let url = self.build_url("blockchain/indexedHeight"); let resp = self.http_client.get(&url).send().await?.json().await?; + Ok(resp) } @@ -64,6 +65,7 @@ impl NodeClient { let url = self.build_url("transactions/unconfirmed"); let resp = self.http_client.get(&url).send().await?.json().await?; debug!(response = ?resp, "Mempool transactions fetched."); + Ok(resp) } @@ -72,12 +74,14 @@ impl NodeClient { let url = self.build_url("info"); let response: InfoResponse = self.http_client.get(&url).send().await?.json().await?; debug!(?response, "Node info fetched."); + Ok(response) } #[tracing::instrument(skip(self))] pub async fn get_last_mempool_update_timestamp(&self) -> Result { let info = self.get_info().await?; + Ok(info.last_mempool_update) } @@ -89,8 +93,8 @@ impl NodeClient { if index_status.indexed_height != index_status.full_height { return Err(NodeError::NotIndexed(index_status)); } - debug!(?index_status, "Node is fully indexed."); + Ok(()) } @@ -98,6 +102,7 @@ impl NodeClient { pub async fn get_unconfirmed_transaction_ids(&self) -> Result, NodeError> { let url = self.build_url("transactions/unconfirmed/transactionIds"); let resp = self.http_client.get(&url).send().await?.json().await?; + Ok(resp) } @@ -115,6 +120,7 @@ impl NodeClient { .await? .json() .await?; + Ok(resp) } @@ -129,6 +135,7 @@ impl NodeClient { .await? .json() .await?; + Ok(resp) } From 26bfbbc038434979c9e1aa266861ae4e83a910fc Mon Sep 17 00:00:00 2001 From: c8e4 Date: Sat, 29 Nov 2025 20:33:21 +0100 Subject: [PATCH 6/7] delete tests/ --- tests/test_node.rs | 79 ---------------------------------------------- 1 file changed, 79 deletions(-) delete mode 100644 tests/test_node.rs diff --git a/tests/test_node.rs b/tests/test_node.rs deleted file mode 100644 index 3f01df0..0000000 --- a/tests/test_node.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::time::Duration; - -use dotenvy::dotenv; -use hergmes::{clients::node::NodeClient, env::ERGO_NODE_URL, types::ergo::Base58String}; - -#[tokio::test] -async fn test_node_balance() { - let _ = dotenv(); - - let http_client = reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .unwrap(); - - let node = NodeClient::new(http_client, &ERGO_NODE_URL); - - let address = Base58String("9hMDjzgnrwET8dweNnK3wKHJf7Vi3zWcKsFEEcdETdSie34BQ16".to_string()); - - let balance = node.get_balance(&address).await.unwrap(); - - assert!(balance.confirmed.nano_ergs >= 10); -} - -#[tokio::test] -async fn test_unspent_boxes_by_ergo_tree_one_box() { - let _ = dotenv(); - - let http_client = reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .unwrap(); - - let node = NodeClient::new(http_client, &ERGO_NODE_URL); - - let ergo_tree = "0008cd02232fb68248be44236ad6c43a3e9b602647163fd83ae10325a6713959fb19dacf"; - - let resp = node - .get_unspent_boxes_by_ergo_tree(ergo_tree, 0, 1, "desc", false, false) - .await - .unwrap(); - - assert_eq!(resp.len(), 1); - - let b = &resp[0]; - assert_eq!(b.utxo.ergo_tree.0, hex::decode(ergo_tree).unwrap()); - assert!(b.utxo.value > 0); - assert!(b.utxo.creation_height > 0); - assert!(b.inclusion_height > 0); - assert!(b.global_index > 0); -} - -#[tokio::test] -async fn test_unspent_boxes_by_token_id_one_box() { - let _ = dotenv(); - - let http_client = reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .unwrap(); - - let node = NodeClient::new(http_client, &ERGO_NODE_URL); - - let token_id = "cbd75cfe1a4f37f9a22eaee516300e36ea82017073036f07a09c1d2e10277cda"; - let token_bytes: [u8; 32] = hex::decode(token_id).unwrap().try_into().unwrap(); - - let resp = node - .get_unspent_boxes_by_token_id(token_id, 0, 1, "desc", false, false) - .await - .unwrap(); - - assert_eq!(resp.len(), 1); - - let b = &resp[0]; - assert!(b.utxo.tokens.iter().any(|t| t.id.0 == token_bytes)); - assert!(b.utxo.value > 0); - assert!(b.utxo.creation_height > 0); - assert!(b.inclusion_height > 0); - assert!(b.global_index > 0); -} From e1ab6a050fe12172d99ad4acd0513c4a1fb8e77b Mon Sep 17 00:00:00 2001 From: c8e4 Date: Sat, 29 Nov 2025 20:35:32 +0100 Subject: [PATCH 7/7] revert README changes --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 7da0f3b..0daac06 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,3 @@ # hergmes Ergo node messaging layer. - -### Running node integration tests - -Make sure `ERGO_NODE_URL` is set in your environment, then run: -```sh -cargo test --test test_node -``` -