ergo_node_interface/
wallet.rs

1use 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    /// Get all addresses from the node wallet
12    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    /// Acquires unspent boxes from the node wallet
34    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    /// Returns unspent boxes from the node wallet ordered from highest to
53    /// lowest nanoErgs value.
54    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    /// Returns a sorted list of unspent boxes which cover at least the
62    /// provided value `total` of nanoErgs.
63    /// Note: This box selection strategy simply uses the largest
64    /// value holding boxes from the user's wallet first.
65    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    /// Returns a list of unspent boxes which cover at least the
70    /// provided value `total` of nanoErgs.
71    /// Note: This box selection strategy simply uses the oldest unspent
72    /// boxes from the user's full node wallet first.
73    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    /// Given a `Vec<ErgoBox>`, consume each ErgoBox into a new list until
78    /// the `total` is reached. If there are an insufficient number of
79    /// nanoErgs in the provided `boxes` then it returns an error.
80    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    /// Acquires the unspent box with the highest value of Ergs inside
98    /// from the wallet
99    pub fn highest_value_unspent_box(&self) -> Result<ErgoBox> {
100        let boxes = self.unspent_boxes()?;
101
102        // Find the highest value amount held in a single box in the wallet
103        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    /// Acquires the unspent box with the highest value of Ergs inside
120    /// from the wallet and serializes it
121    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    /// Acquires unspent boxes which cover `total` amount of nanoErgs
127    /// from the wallet and serializes the boxes
128    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    /// Get the current nanoErgs balance held in the Ergo Node wallet
138    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    /// Get wallet status /wallet/status
155    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    /// Unlock wallet
168    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}