ergo_node_interface/
node_interface.rs1use crate::{BlockHeight, NanoErg, P2PKAddressString, P2SAddressString};
4use ergo_lib::chain::ergo_state_context::{ErgoStateContext, Headers};
5use ergo_lib::ergo_chain_types::{Header, PreHeader};
6use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox;
7use ergo_lib::ergotree_ir::chain::token::TokenId;
8use reqwest::Url;
9use serde_json::from_str;
10use std::convert::TryInto;
11use thiserror::Error;
12
13pub type Result<T> = std::result::Result<T, NodeError>;
14
15#[derive(Error, Debug)]
16pub enum NodeError {
17 #[error("The configured node is unreachable. Please ensure your config is correctly filled out and the node is running.")]
18 NodeUnreachable,
19 #[error("Failed reading response from node: {0}")]
20 FailedParsingNodeResponse(String),
21 #[error("Failed parsing JSON box from node: {0}")]
22 FailedParsingBox(String),
23 #[error("No Boxes Were Found.")]
24 NoBoxesFound,
25 #[error("An insufficient number of Ergs were found.")]
26 InsufficientErgsBalance(),
27 #[error("Failed registering UTXO-set scan with the node: {0}")]
28 FailedRegisteringScan(String),
29 #[error("The node rejected the request you provided.\nNode Response: {0}")]
30 BadRequest(String),
31 #[error("The node wallet has no addresses.")]
32 NoAddressesInWallet,
33 #[error("The node is still syncing.")]
34 NodeSyncing,
35 #[error("Error while processing Node Interface Config Yaml: {0}")]
36 YamlError(String),
37 #[error("{0}")]
38 Other(String),
39 #[error("Failed parsing wallet status from node: {0}")]
40 FailedParsingWalletStatus(String),
41 #[error("Failed to parse URL: {0}")]
42 InvalidUrl(String),
43 #[error("Failed to parse scan ID: {0}")]
44 InvalidScanId(String),
45}
46
47#[derive(Debug, Clone)]
50pub struct NodeInterface {
51 pub api_key: String,
52 pub url: Url,
53}
54
55pub fn is_mainnet_address(address: &str) -> bool {
56 address.starts_with('9')
57}
58
59pub fn is_testnet_address(address: &str) -> bool {
60 address.starts_with('3')
61}
62
63impl NodeInterface {
64 pub fn new(api_key: &str, ip: &str, port: &str) -> Result<Self> {
67 let url = Url::parse(("http://".to_string() + ip + ":" + port + "/").as_str())
68 .map_err(|e| NodeError::InvalidUrl(e.to_string()))?;
69 Ok(NodeInterface {
70 api_key: api_key.to_string(),
71 url,
72 })
73 }
74
75 pub fn from_url(api_key: &str, url: Url) -> Self {
76 NodeInterface {
77 api_key: api_key.to_string(),
78 url,
79 }
80 }
81
82 pub fn from_url_str(api_key: &str, url: &str) -> Result<Self> {
83 let url = Url::parse(url).map_err(|e| NodeError::InvalidUrl(e.to_string()))?;
84 Ok(NodeInterface {
85 api_key: api_key.to_string(),
86 url,
87 })
88 }
89
90 pub fn unspent_boxes_by_address(
92 &self,
93 address: &P2PKAddressString,
94 offset: u64,
95 limit: u64,
96 ) -> Result<Vec<ErgoBox>> {
97 let endpoint = format!(
98 "/blockchain/box/unspent/byAddress?offset={}&limit={}",
99 offset, limit
100 );
101 let res = self.send_post_req(endpoint.as_str(), address.clone());
102 let res_json = self.parse_response_to_json(res)?;
103
104 let mut box_list = vec![];
105
106 for i in 0.. {
107 let box_json = &res_json[i];
108 if box_json.is_null() {
109 break;
110 } else if let Ok(ergo_box) = from_str(&box_json.to_string()) {
111 if box_json["spentTransactionId"].is_null() {
113 box_list.push(ergo_box);
114 }
115 }
116 }
117 Ok(box_list)
118 }
119
120 pub fn unspent_boxes_by_token_id(
122 &self,
123 token_id: &TokenId,
124 offset: u64,
125 limit: u64,
126 ) -> Result<Vec<ErgoBox>> {
127 let id: String = (*token_id).into();
128 let endpoint = format!(
129 "/blockchain/box/unspent/byTokenId/{}?offset={}&limit={}",
130 id, offset, limit
131 );
132 let res = self.send_get_req(endpoint.as_str());
133 let res_json = self.parse_response_to_json(res)?;
134
135 let mut box_list = vec![];
136
137 for i in 0.. {
138 let box_json = &res_json[i];
139 if box_json.is_null() {
140 break;
141 } else if let Ok(ergo_box) = from_str(&box_json.to_string()) {
142 if box_json["spentTransactionId"].is_null() {
144 box_list.push(ergo_box);
145 }
146 }
147 }
148 Ok(box_list)
149 }
150
151 pub fn nano_ergs_balance(&self, address: &P2PKAddressString) -> Result<NanoErg> {
153 let endpoint = "/blockchain/balance";
154 let res = self.send_post_req(endpoint, address.clone());
155 let res_json = self.parse_response_to_json(res)?;
156
157 let balance = res_json["confirmed"]["nanoErgs"].clone();
158
159 if balance.is_null() {
160 Err(NodeError::NodeSyncing)
161 } else {
162 balance
163 .as_u64()
164 .ok_or_else(|| NodeError::FailedParsingNodeResponse(res_json.to_string()))
165 }
166 }
167
168 pub fn p2s_to_tree(&self, address: &P2SAddressString) -> Result<String> {
170 let endpoint = "/script/addressToTree/".to_string() + address;
171 let res = self.send_get_req(&endpoint);
172 let res_json = self.parse_response_to_json(res)?;
173
174 Ok(res_json["tree"].to_string())
175 }
176
177 pub fn p2s_to_bytes(&self, address: &P2SAddressString) -> Result<String> {
179 let endpoint = "/script/addressToBytes/".to_string() + address;
180 let res = self.send_get_req(&endpoint);
181 let res_json = self.parse_response_to_json(res)?;
182
183 Ok(res_json["bytes"].to_string())
184 }
185
186 pub fn p2pk_to_raw(&self, address: &P2PKAddressString) -> Result<String> {
188 let endpoint = "/utils/addressToRaw/".to_string() + address;
189 let res = self.send_get_req(&endpoint);
190 let res_json = self.parse_response_to_json(res)?;
191
192 Ok(res_json["raw"].to_string())
193 }
194
195 pub fn p2pk_to_raw_for_register(&self, address: &P2PKAddressString) -> Result<String> {
199 let add = self.p2pk_to_raw(address)?;
200 Ok("07".to_string() + &add)
201 }
202
203 pub fn raw_to_p2pk(&self, raw: &str) -> Result<P2PKAddressString> {
205 let endpoint = "/utils/rawToAddress/".to_string() + raw;
206 let res = self.send_get_req(&endpoint);
207 let res_json = self.parse_response_to_json(res)?;
208
209 Ok(res_json["address"].to_string())
210 }
211
212 pub fn raw_from_register_to_p2pk(&self, typed_raw: &str) -> Result<P2PKAddressString> {
215 self.raw_to_p2pk(&typed_raw[2..])
216 }
217
218 pub fn serialize_boxes(&self, b: &[ErgoBox]) -> Result<Vec<String>> {
221 Ok(b.iter()
222 .map(|b| {
223 self.serialized_box_from_id(&b.box_id().into())
224 .unwrap_or_else(|_| "".to_string())
225 })
226 .collect())
227 }
228
229 pub fn serialize_box(&self, b: &ErgoBox) -> Result<String> {
232 self.serialized_box_from_id(&b.box_id().into())
233 }
234
235 pub fn serialized_box_from_id(&self, box_id: &String) -> Result<String> {
238 let endpoint = "/utxo/byIdBinary/".to_string() + box_id;
239 let res = self.send_get_req(&endpoint);
240 let res_json = self.parse_response_to_json(res)?;
241
242 Ok(res_json["bytes"].to_string())
243 }
244
245 pub fn box_from_id(&self, box_id: &String) -> Result<ErgoBox> {
248 let endpoint = "/utxo/byId/".to_string() + box_id;
249 let res = self.send_get_req(&endpoint);
250 let res_json = self.parse_response_to_json(res)?;
251
252 if let Ok(ergo_box) = from_str(&res_json.to_string()) {
253 Ok(ergo_box)
254 } else {
255 Err(NodeError::FailedParsingBox(res_json.pretty(2)))
256 }
257 }
258
259 pub fn current_block_height(&self) -> Result<BlockHeight> {
261 let endpoint = "/info";
262 let res = self.send_get_req(endpoint);
263 let res_json = self.parse_response_to_json(res)?;
264
265 let height_json = res_json["fullHeight"].clone();
266
267 if height_json.is_null() {
268 Err(NodeError::NodeSyncing)
269 } else {
270 height_json
271 .to_string()
272 .parse()
273 .map_err(|_| NodeError::FailedParsingNodeResponse(res_json.to_string()))
274 }
275 }
276
277 pub fn get_state_context(&self) -> Result<ErgoStateContext> {
279 let mut vec_headers = self.get_last_block_headers(10)?;
280 vec_headers.reverse();
281 let ten_headers: [Header; 10] = vec_headers.try_into().unwrap();
282 let headers = Headers::from(ten_headers);
283 let pre_header = PreHeader::from(headers.first().unwrap().clone());
284 let state_context = ErgoStateContext::new(pre_header, headers);
285
286 Ok(state_context)
287 }
288
289 pub fn get_last_block_headers(&self, number: u32) -> Result<Vec<Header>> {
291 let endpoint = format!("/blocks/lastHeaders/{}", number);
292 let res = self.send_get_req(endpoint.as_str());
293 let res_json = self.parse_response_to_json(res)?;
294
295 let mut headers: Vec<Header> = vec![];
296
297 for i in 0.. {
298 let header_json = &res_json[i];
299 if header_json.is_null() {
300 break;
301 } else if let Ok(header) = from_str(&header_json.to_string()) {
302 headers.push(header);
303 }
304 }
305 Ok(headers)
306 }
307
308 pub fn indexer_status(&self) -> Result<IndexerStatus> {
310 let endpoint = "/blockchain/indexedHeight";
311 let res = self.send_get_req(endpoint);
312 let res_json = self.parse_response_to_json(res)?;
313
314 let error = res_json["error"].clone();
315 if !error.is_null() {
316 return Ok(IndexerStatus {
317 is_active: false,
318 is_sync: false,
319 });
320 }
321
322 let full_height = res_json["fullHeight"]
323 .as_u64()
324 .ok_or(NodeError::FailedParsingNodeResponse(res_json.to_string()))?;
325 let indexed_height = res_json["indexedHeight"]
326 .as_u64()
327 .ok_or(NodeError::FailedParsingNodeResponse(res_json.to_string()))?;
328
329 let is_sync = full_height.abs_diff(indexed_height) < 10;
330 Ok(IndexerStatus {
331 is_active: true,
332 is_sync,
333 })
334 }
335}
336
337pub struct IndexerStatus {
338 pub is_active: bool,
339 pub is_sync: bool,
340}