Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# hergmes

Ergo node messaging layer.
Ergo node messaging layer.
117 changes: 106 additions & 11 deletions src/clients/node.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use serde::{self, Deserialize, Serialize};
use tracing::{debug, error, info};
use tracing::{debug, info};

use crate::types::{
HashDigest,
ergo::{SpendingProof, TransactionInput, UTxO, UnconfirmedTransaction},
ergo::{
Balance, Base58String, NodeBox, SpendingProof, TransactionInput, UTxO,
UnconfirmedTransaction,
},
};

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -34,6 +37,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,
}

#[derive(Debug, Deserialize, Serialize)]
struct MempoolTransactionResponse {
pub id: HashDigest,
Expand All @@ -50,11 +63,11 @@ pub struct MempoolTransactionInput {
}

impl From<MempoolTransactionResponse> for UnconfirmedTransaction {
fn from(mempool_input: MempoolTransactionResponse) -> Self {
fn from(m: MempoolTransactionResponse) -> Self {
UnconfirmedTransaction {
id: mempool_input.id,
outputs: mempool_input.outputs,
inputs: mempool_input
id: m.id,
outputs: m.outputs,
inputs: m
.inputs
.into_iter()
.map(|input| TransactionInput {
Expand All @@ -78,6 +91,7 @@ impl NodeClient {
pub async fn get_indexed_height(&self) -> Result<IndexedHeightResponse, NodeError> {
let url = self.build_url("blockchain/indexedHeight");
let resp = self.http_client.get(&url).send().await?.json().await?;

Ok(resp)
}

Expand All @@ -93,12 +107,10 @@ impl NodeClient {
.json()
.await?;

// Filter out invalid transactions (those with missing UTxOs in inputs)
// https://github.com/ergoplatform/ergo/issues/2248#issuecomment-3463844934
let valid = resp
.into_iter()
.filter(|utx| utx.inputs.iter().all(|i| i.utxo.is_some()))
.map(|utx| utx.into())
.filter(|tx| tx.inputs.iter().all(|i| i.utxo.is_some()))
.map(|tx| tx.into())
.collect::<Vec<UnconfirmedTransaction>>();

Ok(valid)
Expand All @@ -116,6 +128,7 @@ impl NodeClient {
#[tracing::instrument(skip(self))]
pub async fn get_last_mempool_update_timestamp(&self) -> Result<u64, NodeError> {
let info = self.get_info().await?;

Ok(info.last_mempool_update)
}

Expand All @@ -127,7 +140,6 @@ 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(())
Expand All @@ -137,6 +149,7 @@ impl NodeClient {
pub async fn get_unconfirmed_transaction_ids(&self) -> Result<Vec<HashDigest>, NodeError> {
let url = self.build_url("transactions/unconfirmed/transactionIds");
let resp = self.http_client.get(&url).send().await?.json().await?;

Ok(resp)
}

Expand All @@ -154,6 +167,88 @@ impl NodeClient {
.await?
.json()
.await?;

Ok(resp)
}

#[tracing::instrument(skip(self))]
pub async fn get_balance(&self, address: &Base58String) -> Result<Balance, NodeError> {
let url = self.build_url("blockchain/balance");
let resp = self
.http_client
.post(&url)
.json(address)
.send()
.await?
.json()
.await?;

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<Vec<NodeBox>, 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)
}

#[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<Vec<NodeBox>, 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)
}

Expand Down
48 changes: 48 additions & 0 deletions src/types/ergo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,51 @@ pub struct NonMandatoryRegisters {
#[serde(rename = "R9")]
pub r9: Option<HexBytes>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct BoxesResponse<T> {
pub items: Vec<T>,
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<HashDigest>,
#[serde(rename = "spendingHeight")]
pub spending_height: Option<u32>,
#[serde(rename = "inclusionHeight")]
pub inclusion_height: u32,
#[serde(rename = "spendingProof")]
pub spending_proof: Option<SpendingProof>,
#[serde(rename = "globalIndex")]
pub global_index: u64,
}

#[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<BalanceToken>,
}

#[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);