Skip to content

Commit 8926e95

Browse files
authored
Merge pull request #23 from zargarzadehm/add-blockchain-indexer
Add blockchain indexer
2 parents 8f7deb4 + dfd4e7f commit 8926e95

File tree

3 files changed

+367
-212
lines changed

3 files changed

+367
-212
lines changed

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod requests;
88
pub mod scanning;
99
pub mod transactions;
1010
mod types;
11+
pub mod wallet;
1112

1213
pub use local_config::*;
1314
pub use node_interface::NodeInterface;

src/node_interface.rs

Lines changed: 109 additions & 212 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
//! The `NodeInterface` struct is defined which allows for interacting with an Ergo Node via Rust.
22
33
use crate::{BlockHeight, NanoErg, P2PKAddressString, P2SAddressString};
4+
use ergo_lib::chain::ergo_state_context::{ErgoStateContext, Headers};
5+
use ergo_lib::ergo_chain_types::{Header, PreHeader};
46
use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox;
7+
use ergo_lib::ergotree_ir::chain::token::TokenId;
58
use reqwest::Url;
69
use serde_json::from_str;
7-
use serde_with::serde_as;
8-
use serde_with::NoneAsEmptyString;
10+
use std::convert::TryInto;
911
use thiserror::Error;
1012

1113
pub type Result<T> = std::result::Result<T, NodeError>;
@@ -85,156 +87,82 @@ impl NodeInterface {
8587
})
8688
}
8789

88-
/// Get all addresses from the node wallet
89-
pub fn wallet_addresses(&self) -> Result<Vec<P2PKAddressString>> {
90-
let endpoint = "/wallet/addresses";
91-
let res = self.send_get_req(endpoint)?;
92-
93-
let mut addresses: Vec<String> = vec![];
94-
for segment in res
95-
.text()
96-
.expect("Failed to get addresses from wallet.")
97-
.split('\"')
98-
{
99-
let seg = segment.trim();
100-
if is_mainnet_address(seg) || is_testnet_address(seg) {
101-
addresses.push(seg.to_string());
102-
}
103-
}
104-
if addresses.is_empty() {
105-
return Err(NodeError::NoAddressesInWallet);
106-
}
107-
Ok(addresses)
108-
}
109-
110-
/// A CLI interactive interface for prompting a user to select an address
111-
pub fn select_wallet_address(&self) -> Result<P2PKAddressString> {
112-
let address_list = self.wallet_addresses()?;
113-
if address_list.len() == 1 {
114-
return Ok(address_list[0].clone());
115-
}
116-
117-
let mut n = 0;
118-
for address in &address_list {
119-
n += 1;
120-
println!("{n}. {address}");
121-
}
122-
println!("Which address would you like to select?");
123-
let mut input = String::new();
124-
if std::io::stdin().read_line(&mut input).is_ok() {
125-
if let Ok(input_n) = input.trim().parse::<usize>() {
126-
if input_n > address_list.len() || input_n < 1 {
127-
println!("Please select an address within the range.");
128-
return self.select_wallet_address();
129-
}
130-
return Ok(address_list[input_n - 1].clone());
131-
}
132-
}
133-
self.select_wallet_address()
134-
}
135-
136-
/// Acquires unspent boxes from the node wallet
137-
pub fn unspent_boxes(&self) -> Result<Vec<ErgoBox>> {
138-
let endpoint = "/wallet/boxes/unspent?minConfirmations=0&minInclusionHeight=0";
139-
let res = self.send_get_req(endpoint);
90+
/// Acquires unspent boxes from the blockchain by specific address
91+
pub fn unspent_boxes_by_address(
92+
&self,
93+
address: &P2PKAddressString,
94+
offset: u64,
95+
limit: u64,
96+
) -> Result<Vec<ErgoBox>> {
97+
let endpoint = format!(
98+
"/blockchain/box/unspent/byAddress?offset={}&limit={}",
99+
offset, limit
100+
);
101+
let res = self.send_post_req(endpoint.as_str(), address.clone());
140102
let res_json = self.parse_response_to_json(res)?;
141103

142104
let mut box_list = vec![];
143105

144106
for i in 0.. {
145-
let box_json = &res_json[i]["box"];
107+
let box_json = &res_json[i];
146108
if box_json.is_null() {
147109
break;
148110
} else if let Ok(ergo_box) = from_str(&box_json.to_string()) {
149-
box_list.push(ergo_box);
111+
// This condition is added due to a bug in the node indexer that returns some spent boxes as unspent.
112+
if box_json["spentTransactionId"].is_null() {
113+
box_list.push(ergo_box);
114+
}
150115
}
151116
}
152117
Ok(box_list)
153118
}
154119

155-
/// Returns unspent boxes from the node wallet ordered from highest to
156-
/// lowest nanoErgs value.
157-
pub fn unspent_boxes_sorted(&self) -> Result<Vec<ErgoBox>> {
158-
let mut boxes = self.unspent_boxes()?;
159-
boxes.sort_by(|a, b| b.value.as_u64().partial_cmp(a.value.as_u64()).unwrap());
160-
161-
Ok(boxes)
162-
}
163-
164-
/// Returns a sorted list of unspent boxes which cover at least the
165-
/// provided value `total` of nanoErgs.
166-
/// Note: This box selection strategy simply uses the largest
167-
/// value holding boxes from the user's wallet first.
168-
pub fn unspent_boxes_with_min_total(&self, total: NanoErg) -> Result<Vec<ErgoBox>> {
169-
self.consume_boxes_until_total(total, &self.unspent_boxes_sorted()?)
170-
}
120+
/// Acquires unspent boxes from the blockchain by specific token_id
121+
pub fn unspent_boxes_by_token_id(
122+
&self,
123+
token_id: &TokenId,
124+
offset: u64,
125+
limit: u64,
126+
) -> Result<Vec<ErgoBox>> {
127+
let id: String = (*token_id).into();
128+
let endpoint = format!(
129+
"/blockchain/box/unspent/byTokenId/{}?offset={}&limit={}",
130+
id, offset, limit
131+
);
132+
let res = self.send_get_req(endpoint.as_str());
133+
let res_json = self.parse_response_to_json(res)?;
171134

172-
/// Returns a list of unspent boxes which cover at least the
173-
/// provided value `total` of nanoErgs.
174-
/// Note: This box selection strategy simply uses the oldest unspent
175-
/// boxes from the user's full node wallet first.
176-
pub fn unspent_boxes_with_min_total_by_age(&self, total: NanoErg) -> Result<Vec<ErgoBox>> {
177-
self.consume_boxes_until_total(total, &self.unspent_boxes()?)
178-
}
135+
let mut box_list = vec![];
179136

180-
/// Given a `Vec<ErgoBox>`, consume each ErgoBox into a new list until
181-
/// the `total` is reached. If there are an insufficient number of
182-
/// nanoErgs in the provided `boxes` then it returns an error.
183-
fn consume_boxes_until_total(&self, total: NanoErg, boxes: &[ErgoBox]) -> Result<Vec<ErgoBox>> {
184-
let mut count = 0;
185-
let mut filtered_boxes = vec![];
186-
for b in boxes {
187-
if count >= total {
137+
for i in 0.. {
138+
let box_json = &res_json[i];
139+
if box_json.is_null() {
188140
break;
189-
} else {
190-
count += b.value.as_u64();
191-
filtered_boxes.push(b.clone());
141+
} else if let Ok(ergo_box) = from_str(&box_json.to_string()) {
142+
// This condition is added due to a bug in the node indexer that returns some spent boxes as unspent.
143+
if box_json["spentTransactionId"].is_null() {
144+
box_list.push(ergo_box);
145+
}
192146
}
193147
}
194-
if count < total {
195-
return Err(NodeError::InsufficientErgsBalance());
196-
}
197-
Ok(filtered_boxes)
148+
Ok(box_list)
198149
}
199150

200-
/// Acquires the unspent box with the highest value of Ergs inside
201-
/// from the wallet
202-
pub fn highest_value_unspent_box(&self) -> Result<ErgoBox> {
203-
let boxes = self.unspent_boxes()?;
204-
205-
// Find the highest value amount held in a single box in the wallet
206-
let highest_value = boxes.iter().fold(0, |acc, b| {
207-
if *b.value.as_u64() > acc {
208-
*b.value.as_u64()
209-
} else {
210-
acc
211-
}
212-
});
213-
214-
for b in boxes {
215-
if *b.value.as_u64() == highest_value {
216-
return Ok(b);
217-
}
218-
}
219-
Err(NodeError::NoBoxesFound)
220-
}
151+
/// Get the current nanoErgs balance held in the `address`
152+
pub fn nano_ergs_balance(&self, address: &P2PKAddressString) -> Result<NanoErg> {
153+
let endpoint = "/blockchain/balance";
154+
let res = self.send_post_req(endpoint, address.clone());
155+
let res_json = self.parse_response_to_json(res)?;
221156

222-
/// Acquires the unspent box with the highest value of Ergs inside
223-
/// from the wallet and serializes it
224-
pub fn serialized_highest_value_unspent_box(&self) -> Result<String> {
225-
let ergs_box_id: String = self.highest_value_unspent_box()?.box_id().into();
226-
self.serialized_box_from_id(&ergs_box_id)
227-
}
157+
let balance = res_json["confirmed"]["nanoErgs"].clone();
228158

229-
/// Acquires unspent boxes which cover `total` amount of nanoErgs
230-
/// from the wallet and serializes the boxes
231-
pub fn serialized_unspent_boxes_with_min_total(&self, total: NanoErg) -> Result<Vec<String>> {
232-
let boxes = self.unspent_boxes_with_min_total(total)?;
233-
let mut serialized_boxes = vec![];
234-
for b in boxes {
235-
serialized_boxes.push(self.serialized_box_from_id(&b.box_id().into())?);
159+
if balance.is_null() {
160+
Err(NodeError::NodeSyncing)
161+
} else {
162+
balance
163+
.as_u64()
164+
.ok_or_else(|| NodeError::FailedParsingNodeResponse(res_json.to_string()))
236165
}
237-
Ok(serialized_boxes)
238166
}
239167

240168
/// Given a P2S Ergo address, extract the hex-encoded serialized ErgoTree (script)
@@ -328,23 +256,6 @@ impl NodeInterface {
328256
}
329257
}
330258

331-
/// Get the current nanoErgs balance held in the Ergo Node wallet
332-
pub fn wallet_nano_ergs_balance(&self) -> Result<NanoErg> {
333-
let endpoint = "/wallet/balances";
334-
let res = self.send_get_req(endpoint);
335-
let res_json = self.parse_response_to_json(res)?;
336-
337-
let balance = res_json["balance"].clone();
338-
339-
if balance.is_null() {
340-
Err(NodeError::NodeSyncing)
341-
} else {
342-
balance
343-
.as_u64()
344-
.ok_or_else(|| NodeError::FailedParsingNodeResponse(res_json.to_string()))
345-
}
346-
}
347-
348259
/// Get the current block height of the blockchain
349260
pub fn current_block_height(&self) -> Result<BlockHeight> {
350261
let endpoint = "/info";
@@ -363,81 +274,67 @@ impl NodeInterface {
363274
}
364275
}
365276

366-
/// Get wallet status /wallet/status
367-
pub fn wallet_status(&self) -> Result<WalletStatus> {
368-
let endpoint = "/wallet/status";
369-
let res = self.send_get_req(endpoint);
370-
let res_json = self.parse_response_to_json(res)?;
277+
/// Get the current state context of the blockchain
278+
pub fn get_state_context(&self) -> Result<ErgoStateContext> {
279+
let mut vec_headers = self.get_last_block_headers(10)?;
280+
vec_headers.reverse();
281+
let ten_headers: [Header; 10] = vec_headers.try_into().unwrap();
282+
let headers = Headers::from(ten_headers);
283+
let pre_header = PreHeader::from(headers.first().unwrap().clone());
284+
let state_context = ErgoStateContext::new(pre_header, headers);
371285

372-
if let Ok(wallet_status) = from_str(&res_json.to_string()) {
373-
Ok(wallet_status)
374-
} else {
375-
Err(NodeError::FailedParsingWalletStatus(res_json.pretty(2)))
376-
}
286+
Ok(state_context)
377287
}
378288

379-
/// Unlock wallet
380-
pub fn wallet_unlock(&self, password: &str) -> Result<bool> {
381-
let endpoint = "/wallet/unlock";
382-
let body = object! {
383-
pass: password,
384-
};
289+
/// Get the last `number` of block headers from the blockchain
290+
pub fn get_last_block_headers(&self, number: u32) -> Result<Vec<Header>> {
291+
let endpoint = format!("/blocks/lastHeaders/{}", number);
292+
let res = self.send_get_req(endpoint.as_str());
293+
let res_json = self.parse_response_to_json(res)?;
385294

386-
let res = self.send_post_req(endpoint, body.to_string())?;
295+
let mut headers: Vec<Header> = vec![];
387296

388-
if res.status().is_success() {
389-
Ok(true)
390-
} else {
391-
let json = self.parse_response_to_json(Ok(res))?;
392-
Err(NodeError::BadRequest(json["error"].to_string()))
297+
for i in 0.. {
298+
let header_json = &res_json[i];
299+
if header_json.is_null() {
300+
break;
301+
} else if let Ok(header) = from_str(&header_json.to_string()) {
302+
headers.push(header);
303+
}
393304
}
305+
Ok(headers)
394306
}
395-
}
396307

397-
#[serde_as]
398-
#[derive(serde::Deserialize, serde::Serialize)]
399-
pub struct WalletStatus {
400-
#[serde(rename = "isInitialized")]
401-
pub initialized: bool,
402-
#[serde(rename = "isUnlocked")]
403-
pub unlocked: bool,
404-
#[serde(rename = "changeAddress")]
405-
#[serde_as(as = "NoneAsEmptyString")]
406-
pub change_address: Option<P2PKAddressString>,
407-
#[serde(rename = "walletHeight")]
408-
pub height: BlockHeight,
409-
#[serde(rename = "error")]
410-
pub error: Option<String>,
411-
}
308+
/// Checks if the blockchain indexer is active by querying the node.
309+
pub fn indexer_status(&self) -> Result<IndexerStatus> {
310+
let endpoint = "/blockchain/indexedHeight";
311+
let res = self.send_get_req(endpoint);
312+
let res_json = self.parse_response_to_json(res)?;
412313

413-
#[cfg(test)]
414-
mod tests {
415-
use super::*;
416-
417-
#[test]
418-
fn test_parsing_wallet_status_unlocked() {
419-
let node_response_json_str = r#"{
420-
"isInitialized": true,
421-
"isUnlocked": true,
422-
"changeAddress": "3Wwc4HWrTcYkRycPNhEUSwNNBdqSBuiHy2zFvjMHukccxE77BaX3",
423-
"walletHeight": 251965,
424-
"error": ""
425-
}"#;
426-
let t: WalletStatus = serde_json::from_str(node_response_json_str).unwrap();
427-
assert_eq!(t.height, 251965);
428-
}
314+
let error = res_json["error"].clone();
315+
if !error.is_null() {
316+
return Ok(IndexerStatus {
317+
is_active: false,
318+
is_sync: false,
319+
});
320+
}
429321

430-
#[test]
431-
fn test_parsing_wallet_status_locked() {
432-
let node_response_json_str = r#"{
433-
"isInitialized": true,
434-
"isUnlocked": false,
435-
"changeAddress": "",
436-
"walletHeight": 251965,
437-
"error": ""
438-
}"#;
439-
let t: WalletStatus = serde_json::from_str(node_response_json_str).unwrap();
440-
assert_eq!(t.change_address, None);
441-
assert_eq!(t.height, 251965);
322+
let full_height = res_json["fullHeight"]
323+
.as_u64()
324+
.ok_or(NodeError::FailedParsingNodeResponse(res_json.to_string()))?;
325+
let indexed_height = res_json["indexedHeight"]
326+
.as_u64()
327+
.ok_or(NodeError::FailedParsingNodeResponse(res_json.to_string()))?;
328+
329+
let is_sync = full_height.abs_diff(indexed_height) < 10;
330+
Ok(IndexerStatus {
331+
is_active: true,
332+
is_sync,
333+
})
442334
}
443335
}
336+
337+
pub struct IndexerStatus {
338+
pub is_active: bool,
339+
pub is_sync: bool,
340+
}

0 commit comments

Comments
 (0)