use crate::node_interface::{
is_mainnet_address, is_testnet_address, NodeError, NodeInterface, Result,
};
use crate::{BlockHeight, NanoErg, P2PKAddressString};
use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox;
use serde_json::from_str;
use serde_with::serde_as;
use serde_with::NoneAsEmptyString;
impl NodeInterface {
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 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 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 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);
}
}