use crate::{BlockHeight, NanoErg, P2PKAddressString, P2SAddressString};
use ergo_lib::chain::ergo_state_context::{ErgoStateContext, Headers};
use ergo_lib::ergo_chain_types::{Header, PreHeader};
use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox;
use ergo_lib::ergotree_ir::chain::token::TokenId;
use reqwest::Url;
use serde_json::from_str;
use std::convert::TryInto;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, NodeError>;
#[derive(Error, Debug)]
pub enum NodeError {
#[error("The configured node is unreachable. Please ensure your config is correctly filled out and the node is running.")]
NodeUnreachable,
#[error("Failed reading response from node: {0}")]
FailedParsingNodeResponse(String),
#[error("Failed parsing JSON box from node: {0}")]
FailedParsingBox(String),
#[error("No Boxes Were Found.")]
NoBoxesFound,
#[error("An insufficient number of Ergs were found.")]
InsufficientErgsBalance(),
#[error("Failed registering UTXO-set scan with the node: {0}")]
FailedRegisteringScan(String),
#[error("The node rejected the request you provided.\nNode Response: {0}")]
BadRequest(String),
#[error("The node wallet has no addresses.")]
NoAddressesInWallet,
#[error("The node is still syncing.")]
NodeSyncing,
#[error("Error while processing Node Interface Config Yaml: {0}")]
YamlError(String),
#[error("{0}")]
Other(String),
#[error("Failed parsing wallet status from node: {0}")]
FailedParsingWalletStatus(String),
#[error("Failed to parse URL: {0}")]
InvalidUrl(String),
#[error("Failed to parse scan ID: {0}")]
InvalidScanId(String),
}
#[derive(Debug, Clone)]
pub struct NodeInterface {
pub api_key: String,
pub url: Url,
}
pub fn is_mainnet_address(address: &str) -> bool {
address.starts_with('9')
}
pub fn is_testnet_address(address: &str) -> bool {
address.starts_with('3')
}
impl NodeInterface {
pub fn new(api_key: &str, ip: &str, port: &str) -> Result<Self> {
let url = Url::parse(("http://".to_string() + ip + ":" + port + "/").as_str())
.map_err(|e| NodeError::InvalidUrl(e.to_string()))?;
Ok(NodeInterface {
api_key: api_key.to_string(),
url,
})
}
pub fn from_url(api_key: &str, url: Url) -> Self {
NodeInterface {
api_key: api_key.to_string(),
url,
}
}
pub fn from_url_str(api_key: &str, url: &str) -> Result<Self> {
let url = Url::parse(url).map_err(|e| NodeError::InvalidUrl(e.to_string()))?;
Ok(NodeInterface {
api_key: api_key.to_string(),
url,
})
}
pub fn unspent_boxes_by_address(
&self,
address: &P2PKAddressString,
offset: u64,
limit: u64,
) -> Result<Vec<ErgoBox>> {
let endpoint = format!(
"/blockchain/box/unspent/byAddress?offset={}&limit={}",
offset, limit
);
let res = self.send_post_req(endpoint.as_str(), address.clone());
let res_json = self.parse_response_to_json(res)?;
let mut box_list = vec![];
for i in 0.. {
let box_json = &res_json[i];
if box_json.is_null() {
break;
} else if let Ok(ergo_box) = from_str(&box_json.to_string()) {
if box_json["spentTransactionId"].is_null() {
box_list.push(ergo_box);
}
}
}
Ok(box_list)
}
pub fn unspent_boxes_by_token_id(
&self,
token_id: &TokenId,
offset: u64,
limit: u64,
) -> Result<Vec<ErgoBox>> {
let id: String = (*token_id).into();
let endpoint = format!(
"/blockchain/box/unspent/byTokenId/{}?offset={}&limit={}",
id, offset, limit
);
let res = self.send_get_req(endpoint.as_str());
let res_json = self.parse_response_to_json(res)?;
let mut box_list = vec![];
for i in 0.. {
let box_json = &res_json[i];
if box_json.is_null() {
break;
} else if let Ok(ergo_box) = from_str(&box_json.to_string()) {
if box_json["spentTransactionId"].is_null() {
box_list.push(ergo_box);
}
}
}
Ok(box_list)
}
pub fn nano_ergs_balance(&self, address: &P2PKAddressString) -> Result<NanoErg> {
let endpoint = "/blockchain/balance";
let res = self.send_post_req(endpoint, address.clone());
let res_json = self.parse_response_to_json(res)?;
let balance = res_json["confirmed"]["nanoErgs"].clone();
if balance.is_null() {
Err(NodeError::NodeSyncing)
} else {
balance
.as_u64()
.ok_or_else(|| NodeError::FailedParsingNodeResponse(res_json.to_string()))
}
}
pub fn p2s_to_tree(&self, address: &P2SAddressString) -> Result<String> {
let endpoint = "/script/addressToTree/".to_string() + address;
let res = self.send_get_req(&endpoint);
let res_json = self.parse_response_to_json(res)?;
Ok(res_json["tree"].to_string())
}
pub fn p2s_to_bytes(&self, address: &P2SAddressString) -> Result<String> {
let endpoint = "/script/addressToBytes/".to_string() + address;
let res = self.send_get_req(&endpoint);
let res_json = self.parse_response_to_json(res)?;
Ok(res_json["bytes"].to_string())
}
pub fn p2pk_to_raw(&self, address: &P2PKAddressString) -> Result<String> {
let endpoint = "/utils/addressToRaw/".to_string() + address;
let res = self.send_get_req(&endpoint);
let res_json = self.parse_response_to_json(res)?;
Ok(res_json["raw"].to_string())
}
pub fn p2pk_to_raw_for_register(&self, address: &P2PKAddressString) -> Result<String> {
let add = self.p2pk_to_raw(address)?;
Ok("07".to_string() + &add)
}
pub fn raw_to_p2pk(&self, raw: &str) -> Result<P2PKAddressString> {
let endpoint = "/utils/rawToAddress/".to_string() + raw;
let res = self.send_get_req(&endpoint);
let res_json = self.parse_response_to_json(res)?;
Ok(res_json["address"].to_string())
}
pub fn raw_from_register_to_p2pk(&self, typed_raw: &str) -> Result<P2PKAddressString> {
self.raw_to_p2pk(&typed_raw[2..])
}
pub fn serialize_boxes(&self, b: &[ErgoBox]) -> Result<Vec<String>> {
Ok(b.iter()
.map(|b| {
self.serialized_box_from_id(&b.box_id().into())
.unwrap_or_else(|_| "".to_string())
})
.collect())
}
pub fn serialize_box(&self, b: &ErgoBox) -> Result<String> {
self.serialized_box_from_id(&b.box_id().into())
}
pub fn serialized_box_from_id(&self, box_id: &String) -> Result<String> {
let endpoint = "/utxo/byIdBinary/".to_string() + box_id;
let res = self.send_get_req(&endpoint);
let res_json = self.parse_response_to_json(res)?;
Ok(res_json["bytes"].to_string())
}
pub fn box_from_id(&self, box_id: &String) -> Result<ErgoBox> {
let endpoint = "/utxo/byId/".to_string() + box_id;
let res = self.send_get_req(&endpoint);
let res_json = self.parse_response_to_json(res)?;
if let Ok(ergo_box) = from_str(&res_json.to_string()) {
Ok(ergo_box)
} else {
Err(NodeError::FailedParsingBox(res_json.pretty(2)))
}
}
pub fn current_block_height(&self) -> Result<BlockHeight> {
let endpoint = "/info";
let res = self.send_get_req(endpoint);
let res_json = self.parse_response_to_json(res)?;
let height_json = res_json["fullHeight"].clone();
if height_json.is_null() {
Err(NodeError::NodeSyncing)
} else {
height_json
.to_string()
.parse()
.map_err(|_| NodeError::FailedParsingNodeResponse(res_json.to_string()))
}
}
pub fn get_state_context(&self) -> Result<ErgoStateContext> {
let mut vec_headers = self.get_last_block_headers(10)?;
vec_headers.reverse();
let ten_headers: [Header; 10] = vec_headers.try_into().unwrap();
let headers = Headers::from(ten_headers);
let pre_header = PreHeader::from(headers.first().unwrap().clone());
let state_context = ErgoStateContext::new(pre_header, headers);
Ok(state_context)
}
pub fn get_last_block_headers(&self, number: u32) -> Result<Vec<Header>> {
let endpoint = format!("/blocks/lastHeaders/{}", number);
let res = self.send_get_req(endpoint.as_str());
let res_json = self.parse_response_to_json(res)?;
let mut headers: Vec<Header> = vec![];
for i in 0.. {
let header_json = &res_json[i];
if header_json.is_null() {
break;
} else if let Ok(header) = from_str(&header_json.to_string()) {
headers.push(header);
}
}
Ok(headers)
}
pub fn indexer_status(&self) -> Result<IndexerStatus> {
let endpoint = "/blockchain/indexedHeight";
let res = self.send_get_req(endpoint);
let res_json = self.parse_response_to_json(res)?;
let error = res_json["error"].clone();
if !error.is_null() {
return Ok(IndexerStatus {
is_active: false,
is_sync: false,
});
}
let full_height = res_json["fullHeight"]
.as_u64()
.ok_or(NodeError::FailedParsingNodeResponse(res_json.to_string()))?;
let indexed_height = res_json["indexedHeight"]
.as_u64()
.ok_or(NodeError::FailedParsingNodeResponse(res_json.to_string()))?;
let is_sync = full_height.abs_diff(indexed_height) < 10;
Ok(IndexerStatus {
is_active: true,
is_sync,
})
}
}
pub struct IndexerStatus {
pub is_active: bool,
pub is_sync: bool,
}