use crate::{BlockHeight, NanoErg, P2PKAddressString, P2SAddressString};
use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox;
use reqwest::Url;
use serde_json::from_str;
use serde_with::serde_as;
use serde_with::NoneAsEmptyString;
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),
}
#[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 wallet_addresses(&self) -> Result<Vec<P2PKAddressString>> {
let endpoint = "/wallet/addresses";
let res = self.send_get_req(endpoint)?;
let mut addresses: Vec<String> = vec![];
for segment in res
.text()
.expect("Failed to get addresses from wallet.")
.split('\"')
{
let seg = segment.trim();
if is_mainnet_address(seg) || is_testnet_address(seg) {
addresses.push(seg.to_string());
}
}
if addresses.is_empty() {
return Err(NodeError::NoAddressesInWallet);
}
Ok(addresses)
}
pub fn select_wallet_address(&self) -> Result<P2PKAddressString> {
let address_list = self.wallet_addresses()?;
if address_list.len() == 1 {
return Ok(address_list[0].clone());
}
let mut n = 0;
for address in &address_list {
n += 1;
println!("{n}. {address}");
}
println!("Which address would you like to select?");
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
if let Ok(input_n) = input.trim().parse::<usize>() {
if input_n > address_list.len() || input_n < 1 {
println!("Please select an address within the range.");
return self.select_wallet_address();
}
return Ok(address_list[input_n - 1].clone());
}
}
self.select_wallet_address()
}
pub fn unspent_boxes(&self) -> Result<Vec<ErgoBox>> {
let endpoint = "/wallet/boxes/unspent?minConfirmations=0&minInclusionHeight=0";
let res = self.send_get_req(endpoint);
let res_json = self.parse_response_to_json(res)?;
let mut box_list = vec![];
for i in 0.. {
let box_json = &res_json[i]["box"];
if box_json.is_null() {
break;
} else if let Ok(ergo_box) = from_str(&box_json.to_string()) {
box_list.push(ergo_box);
}
}
Ok(box_list)
}
pub fn unspent_boxes_sorted(&self) -> Result<Vec<ErgoBox>> {
let mut boxes = self.unspent_boxes()?;
boxes.sort_by(|a, b| b.value.as_u64().partial_cmp(a.value.as_u64()).unwrap());
Ok(boxes)
}
pub fn unspent_boxes_with_min_total(&self, total: NanoErg) -> Result<Vec<ErgoBox>> {
self.consume_boxes_until_total(total, &self.unspent_boxes_sorted()?)
}
pub fn unspent_boxes_with_min_total_by_age(&self, total: NanoErg) -> Result<Vec<ErgoBox>> {
self.consume_boxes_until_total(total, &self.unspent_boxes()?)
}
fn consume_boxes_until_total(&self, total: NanoErg, boxes: &[ErgoBox]) -> Result<Vec<ErgoBox>> {
let mut count = 0;
let mut filtered_boxes = vec![];
for b in boxes {
if count >= total {
break;
} else {
count += b.value.as_u64();
filtered_boxes.push(b.clone());
}
}
if count < total {
return Err(NodeError::InsufficientErgsBalance());
}
Ok(filtered_boxes)
}
pub fn highest_value_unspent_box(&self) -> Result<ErgoBox> {
let boxes = self.unspent_boxes()?;
let highest_value = boxes.iter().fold(0, |acc, b| {
if *b.value.as_u64() > acc {
*b.value.as_u64()
} else {
acc
}
});
for b in boxes {
if *b.value.as_u64() == highest_value {
return Ok(b);
}
}
Err(NodeError::NoBoxesFound)
}
pub fn serialized_highest_value_unspent_box(&self) -> Result<String> {
let ergs_box_id: String = self.highest_value_unspent_box()?.box_id().into();
self.serialized_box_from_id(&ergs_box_id)
}
pub fn serialized_unspent_boxes_with_min_total(&self, total: NanoErg) -> Result<Vec<String>> {
let boxes = self.unspent_boxes_with_min_total(total)?;
let mut serialized_boxes = vec![];
for b in boxes {
serialized_boxes.push(self.serialized_box_from_id(&b.box_id().into())?);
}
Ok(serialized_boxes)
}
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 wallet_nano_ergs_balance(&self) -> Result<NanoErg> {
let endpoint = "/wallet/balances";
let res = self.send_get_req(endpoint);
let res_json = self.parse_response_to_json(res)?;
let balance = res_json["balance"].clone();
if balance.is_null() {
Err(NodeError::NodeSyncing)
} else {
balance
.as_u64()
.ok_or_else(|| NodeError::FailedParsingNodeResponse(res_json.to_string()))
}
}
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 wallet_status(&self) -> Result<WalletStatus> {
let endpoint = "/wallet/status";
let res = self.send_get_req(endpoint);
let res_json = self.parse_response_to_json(res)?;
if let Ok(wallet_status) = from_str(&res_json.to_string()) {
Ok(wallet_status)
} else {
Err(NodeError::FailedParsingWalletStatus(res_json.pretty(2)))
}
}
pub fn wallet_unlock(&self, password: &str) -> Result<bool> {
let endpoint = "/wallet/unlock";
let body = object! {
pass: password,
};
let res = self.send_post_req(endpoint, body.to_string())?;
if res.status().is_success() {
Ok(true)
} else {
let json = self.parse_response_to_json(Ok(res))?;
Err(NodeError::BadRequest(json["error"].to_string()))
}
}
}
#[serde_as]
#[derive(serde::Deserialize, serde::Serialize)]
pub struct WalletStatus {
#[serde(rename = "isInitialized")]
pub initialized: bool,
#[serde(rename = "isUnlocked")]
pub unlocked: bool,
#[serde(rename = "changeAddress")]
#[serde_as(as = "NoneAsEmptyString")]
pub change_address: Option<P2PKAddressString>,
#[serde(rename = "walletHeight")]
pub height: BlockHeight,
#[serde(rename = "error")]
pub error: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parsing_wallet_status_unlocked() {
let node_response_json_str = r#"{
"isInitialized": true,
"isUnlocked": true,
"changeAddress": "3Wwc4HWrTcYkRycPNhEUSwNNBdqSBuiHy2zFvjMHukccxE77BaX3",
"walletHeight": 251965,
"error": ""
}"#;
let t: WalletStatus = serde_json::from_str(node_response_json_str).unwrap();
assert_eq!(t.height, 251965);
}
#[test]
fn test_parsing_wallet_status_locked() {
let node_response_json_str = r#"{
"isInitialized": true,
"isUnlocked": false,
"changeAddress": "",
"walletHeight": 251965,
"error": ""
}"#;
let t: WalletStatus = serde_json::from_str(node_response_json_str).unwrap();
assert_eq!(t.change_address, None);
assert_eq!(t.height, 251965);
}
}