ergo_node_interface/
wallet.rs1use crate::node_interface::{
2 is_mainnet_address, is_testnet_address, NodeError, NodeInterface, Result,
3};
4use crate::{BlockHeight, NanoErg, P2PKAddressString};
5use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox;
6use serde_json::from_str;
7use serde_with::serde_as;
8use serde_with::NoneAsEmptyString;
9
10impl NodeInterface {
11 pub fn wallet_addresses(&self) -> Result<Vec<P2PKAddressString>> {
13 let endpoint = "/wallet/addresses";
14 let res = self.send_get_req(endpoint)?;
15
16 let mut addresses: Vec<String> = vec![];
17 for segment in res
18 .text()
19 .expect("Failed to get addresses from wallet.")
20 .split('\"')
21 {
22 let seg = segment.trim();
23 if is_mainnet_address(seg) || is_testnet_address(seg) {
24 addresses.push(seg.to_string());
25 }
26 }
27 if addresses.is_empty() {
28 return Err(NodeError::NoAddressesInWallet);
29 }
30 Ok(addresses)
31 }
32
33 pub fn unspent_boxes(&self) -> Result<Vec<ErgoBox>> {
35 let endpoint = "/wallet/boxes/unspent?minConfirmations=0&minInclusionHeight=0";
36 let res = self.send_get_req(endpoint);
37 let res_json = self.parse_response_to_json(res)?;
38
39 let mut box_list = vec![];
40
41 for i in 0.. {
42 let box_json = &res_json[i]["box"];
43 if box_json.is_null() {
44 break;
45 } else if let Ok(ergo_box) = from_str(&box_json.to_string()) {
46 box_list.push(ergo_box);
47 }
48 }
49 Ok(box_list)
50 }
51
52 pub fn unspent_boxes_sorted(&self) -> Result<Vec<ErgoBox>> {
55 let mut boxes = self.unspent_boxes()?;
56 boxes.sort_by(|a, b| b.value.as_u64().partial_cmp(a.value.as_u64()).unwrap());
57
58 Ok(boxes)
59 }
60
61 pub fn unspent_boxes_with_min_total(&self, total: NanoErg) -> Result<Vec<ErgoBox>> {
66 self.consume_boxes_until_total(total, &self.unspent_boxes_sorted()?)
67 }
68
69 pub fn unspent_boxes_with_min_total_by_age(&self, total: NanoErg) -> Result<Vec<ErgoBox>> {
74 self.consume_boxes_until_total(total, &self.unspent_boxes()?)
75 }
76
77 fn consume_boxes_until_total(&self, total: NanoErg, boxes: &[ErgoBox]) -> Result<Vec<ErgoBox>> {
81 let mut count = 0;
82 let mut filtered_boxes = vec![];
83 for b in boxes {
84 if count >= total {
85 break;
86 } else {
87 count += b.value.as_u64();
88 filtered_boxes.push(b.clone());
89 }
90 }
91 if count < total {
92 return Err(NodeError::InsufficientErgsBalance());
93 }
94 Ok(filtered_boxes)
95 }
96
97 pub fn highest_value_unspent_box(&self) -> Result<ErgoBox> {
100 let boxes = self.unspent_boxes()?;
101
102 let highest_value = boxes.iter().fold(0, |acc, b| {
104 if *b.value.as_u64() > acc {
105 *b.value.as_u64()
106 } else {
107 acc
108 }
109 });
110
111 for b in boxes {
112 if *b.value.as_u64() == highest_value {
113 return Ok(b);
114 }
115 }
116 Err(NodeError::NoBoxesFound)
117 }
118
119 pub fn serialized_highest_value_unspent_box(&self) -> Result<String> {
122 let ergs_box_id: String = self.highest_value_unspent_box()?.box_id().into();
123 self.serialized_box_from_id(&ergs_box_id)
124 }
125
126 pub fn serialized_unspent_boxes_with_min_total(&self, total: NanoErg) -> Result<Vec<String>> {
129 let boxes = self.unspent_boxes_with_min_total(total)?;
130 let mut serialized_boxes = vec![];
131 for b in boxes {
132 serialized_boxes.push(self.serialized_box_from_id(&b.box_id().into())?);
133 }
134 Ok(serialized_boxes)
135 }
136
137 pub fn wallet_nano_ergs_balance(&self) -> Result<NanoErg> {
139 let endpoint = "/wallet/balances";
140 let res = self.send_get_req(endpoint);
141 let res_json = self.parse_response_to_json(res)?;
142
143 let balance = res_json["balance"].clone();
144
145 if balance.is_null() {
146 Err(NodeError::NodeSyncing)
147 } else {
148 balance
149 .as_u64()
150 .ok_or_else(|| NodeError::FailedParsingNodeResponse(res_json.to_string()))
151 }
152 }
153
154 pub fn wallet_status(&self) -> Result<WalletStatus> {
156 let endpoint = "/wallet/status";
157 let res = self.send_get_req(endpoint);
158 let res_json = self.parse_response_to_json(res)?;
159
160 if let Ok(wallet_status) = from_str(&res_json.to_string()) {
161 Ok(wallet_status)
162 } else {
163 Err(NodeError::FailedParsingWalletStatus(res_json.pretty(2)))
164 }
165 }
166
167 pub fn wallet_unlock(&self, password: &str) -> Result<bool> {
169 let endpoint = "/wallet/unlock";
170 let body = object! {
171 pass: password,
172 };
173
174 let res = self.send_post_req(endpoint, body.to_string())?;
175
176 if res.status().is_success() {
177 Ok(true)
178 } else {
179 let json = self.parse_response_to_json(Ok(res))?;
180 Err(NodeError::BadRequest(json["error"].to_string()))
181 }
182 }
183}
184
185#[serde_as]
186#[derive(serde::Deserialize, serde::Serialize)]
187pub struct WalletStatus {
188 #[serde(rename = "isInitialized")]
189 pub initialized: bool,
190 #[serde(rename = "isUnlocked")]
191 pub unlocked: bool,
192 #[serde(rename = "changeAddress")]
193 #[serde_as(as = "NoneAsEmptyString")]
194 pub change_address: Option<P2PKAddressString>,
195 #[serde(rename = "walletHeight")]
196 pub height: BlockHeight,
197 #[serde(rename = "error")]
198 pub error: Option<String>,
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_parsing_wallet_status_unlocked() {
207 let node_response_json_str = r#"{
208 "isInitialized": true,
209 "isUnlocked": true,
210 "changeAddress": "3Wwc4HWrTcYkRycPNhEUSwNNBdqSBuiHy2zFvjMHukccxE77BaX3",
211 "walletHeight": 251965,
212 "error": ""
213 }"#;
214 let t: WalletStatus = serde_json::from_str(node_response_json_str).unwrap();
215 assert_eq!(t.height, 251965);
216 }
217
218 #[test]
219 fn test_parsing_wallet_status_locked() {
220 let node_response_json_str = r#"{
221 "isInitialized": true,
222 "isUnlocked": false,
223 "changeAddress": "",
224 "walletHeight": 251965,
225 "error": ""
226 }"#;
227 let t: WalletStatus = serde_json::from_str(node_response_json_str).unwrap();
228 assert_eq!(t.change_address, None);
229 assert_eq!(t.height, 251965);
230 }
231}