11//! The `NodeInterface` struct is defined which allows for interacting with an Ergo Node via Rust.
22
33use crate :: { BlockHeight , NanoErg , P2PKAddressString , P2SAddressString } ;
4+ use ergo_lib:: chain:: ergo_state_context:: { ErgoStateContext , Headers } ;
5+ use ergo_lib:: ergo_chain_types:: { Header , PreHeader } ;
46use ergo_lib:: ergotree_ir:: chain:: ergo_box:: ErgoBox ;
7+ use ergo_lib:: ergotree_ir:: chain:: token:: TokenId ;
58use reqwest:: Url ;
69use serde_json:: from_str;
7- use serde_with:: serde_as;
8- use serde_with:: NoneAsEmptyString ;
10+ use std:: convert:: TryInto ;
911use thiserror:: Error ;
1012
1113pub type Result < T > = std:: result:: Result < T , NodeError > ;
@@ -85,156 +87,82 @@ impl NodeInterface {
8587 } )
8688 }
8789
88- /// Get all addresses from the node wallet
89- pub fn wallet_addresses ( & self ) -> Result < Vec < P2PKAddressString > > {
90- let endpoint = "/wallet/addresses" ;
91- let res = self . send_get_req ( endpoint) ?;
92-
93- let mut addresses: Vec < String > = vec ! [ ] ;
94- for segment in res
95- . text ( )
96- . expect ( "Failed to get addresses from wallet." )
97- . split ( '\"' )
98- {
99- let seg = segment. trim ( ) ;
100- if is_mainnet_address ( seg) || is_testnet_address ( seg) {
101- addresses. push ( seg. to_string ( ) ) ;
102- }
103- }
104- if addresses. is_empty ( ) {
105- return Err ( NodeError :: NoAddressesInWallet ) ;
106- }
107- Ok ( addresses)
108- }
109-
110- /// A CLI interactive interface for prompting a user to select an address
111- pub fn select_wallet_address ( & self ) -> Result < P2PKAddressString > {
112- let address_list = self . wallet_addresses ( ) ?;
113- if address_list. len ( ) == 1 {
114- return Ok ( address_list[ 0 ] . clone ( ) ) ;
115- }
116-
117- let mut n = 0 ;
118- for address in & address_list {
119- n += 1 ;
120- println ! ( "{n}. {address}" ) ;
121- }
122- println ! ( "Which address would you like to select?" ) ;
123- let mut input = String :: new ( ) ;
124- if std:: io:: stdin ( ) . read_line ( & mut input) . is_ok ( ) {
125- if let Ok ( input_n) = input. trim ( ) . parse :: < usize > ( ) {
126- if input_n > address_list. len ( ) || input_n < 1 {
127- println ! ( "Please select an address within the range." ) ;
128- return self . select_wallet_address ( ) ;
129- }
130- return Ok ( address_list[ input_n - 1 ] . clone ( ) ) ;
131- }
132- }
133- self . select_wallet_address ( )
134- }
135-
136- /// Acquires unspent boxes from the node wallet
137- pub fn unspent_boxes ( & self ) -> Result < Vec < ErgoBox > > {
138- let endpoint = "/wallet/boxes/unspent?minConfirmations=0&minInclusionHeight=0" ;
139- let res = self . send_get_req ( endpoint) ;
90+ /// Acquires unspent boxes from the blockchain by specific address
91+ 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 ( ) ) ;
140102 let res_json = self . parse_response_to_json ( res) ?;
141103
142104 let mut box_list = vec ! [ ] ;
143105
144106 for i in 0 .. {
145- let box_json = & res_json[ i] [ "box" ] ;
107+ let box_json = & res_json[ i] ;
146108 if box_json. is_null ( ) {
147109 break ;
148110 } else if let Ok ( ergo_box) = from_str ( & box_json. to_string ( ) ) {
149- box_list. push ( ergo_box) ;
111+ // This condition is added due to a bug in the node indexer that returns some spent boxes as unspent.
112+ if box_json[ "spentTransactionId" ] . is_null ( ) {
113+ box_list. push ( ergo_box) ;
114+ }
150115 }
151116 }
152117 Ok ( box_list)
153118 }
154119
155- /// Returns unspent boxes from the node wallet ordered from highest to
156- /// lowest nanoErgs value.
157- pub fn unspent_boxes_sorted ( & self ) -> Result < Vec < ErgoBox > > {
158- let mut boxes = self . unspent_boxes ( ) ?;
159- boxes. sort_by ( |a, b| b. value . as_u64 ( ) . partial_cmp ( a. value . as_u64 ( ) ) . unwrap ( ) ) ;
160-
161- Ok ( boxes)
162- }
163-
164- /// Returns a sorted list of unspent boxes which cover at least the
165- /// provided value `total` of nanoErgs.
166- /// Note: This box selection strategy simply uses the largest
167- /// value holding boxes from the user's wallet first.
168- pub fn unspent_boxes_with_min_total ( & self , total : NanoErg ) -> Result < Vec < ErgoBox > > {
169- self . consume_boxes_until_total ( total, & self . unspent_boxes_sorted ( ) ?)
170- }
120+ /// Acquires unspent boxes from the blockchain by specific token_id
121+ 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) ?;
171134
172- /// Returns a list of unspent boxes which cover at least the
173- /// provided value `total` of nanoErgs.
174- /// Note: This box selection strategy simply uses the oldest unspent
175- /// boxes from the user's full node wallet first.
176- pub fn unspent_boxes_with_min_total_by_age ( & self , total : NanoErg ) -> Result < Vec < ErgoBox > > {
177- self . consume_boxes_until_total ( total, & self . unspent_boxes ( ) ?)
178- }
135+ let mut box_list = vec ! [ ] ;
179136
180- /// Given a `Vec<ErgoBox>`, consume each ErgoBox into a new list until
181- /// the `total` is reached. If there are an insufficient number of
182- /// nanoErgs in the provided `boxes` then it returns an error.
183- fn consume_boxes_until_total ( & self , total : NanoErg , boxes : & [ ErgoBox ] ) -> Result < Vec < ErgoBox > > {
184- let mut count = 0 ;
185- let mut filtered_boxes = vec ! [ ] ;
186- for b in boxes {
187- if count >= total {
137+ for i in 0 .. {
138+ let box_json = & res_json[ i] ;
139+ if box_json. is_null ( ) {
188140 break ;
189- } else {
190- count += b. value . as_u64 ( ) ;
191- filtered_boxes. push ( b. clone ( ) ) ;
141+ } else if let Ok ( ergo_box) = from_str ( & box_json. to_string ( ) ) {
142+ // This condition is added due to a bug in the node indexer that returns some spent boxes as unspent.
143+ if box_json[ "spentTransactionId" ] . is_null ( ) {
144+ box_list. push ( ergo_box) ;
145+ }
192146 }
193147 }
194- if count < total {
195- return Err ( NodeError :: InsufficientErgsBalance ( ) ) ;
196- }
197- Ok ( filtered_boxes)
148+ Ok ( box_list)
198149 }
199150
200- /// Acquires the unspent box with the highest value of Ergs inside
201- /// from the wallet
202- pub fn highest_value_unspent_box ( & self ) -> Result < ErgoBox > {
203- let boxes = self . unspent_boxes ( ) ?;
204-
205- // Find the highest value amount held in a single box in the wallet
206- let highest_value = boxes. iter ( ) . fold ( 0 , |acc, b| {
207- if * b. value . as_u64 ( ) > acc {
208- * b. value . as_u64 ( )
209- } else {
210- acc
211- }
212- } ) ;
213-
214- for b in boxes {
215- if * b. value . as_u64 ( ) == highest_value {
216- return Ok ( b) ;
217- }
218- }
219- Err ( NodeError :: NoBoxesFound )
220- }
151+ /// Get the current nanoErgs balance held in the `address`
152+ 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) ?;
221156
222- /// Acquires the unspent box with the highest value of Ergs inside
223- /// from the wallet and serializes it
224- pub fn serialized_highest_value_unspent_box ( & self ) -> Result < String > {
225- let ergs_box_id: String = self . highest_value_unspent_box ( ) ?. box_id ( ) . into ( ) ;
226- self . serialized_box_from_id ( & ergs_box_id)
227- }
157+ let balance = res_json[ "confirmed" ] [ "nanoErgs" ] . clone ( ) ;
228158
229- /// Acquires unspent boxes which cover `total` amount of nanoErgs
230- /// from the wallet and serializes the boxes
231- pub fn serialized_unspent_boxes_with_min_total ( & self , total : NanoErg ) -> Result < Vec < String > > {
232- let boxes = self . unspent_boxes_with_min_total ( total) ?;
233- let mut serialized_boxes = vec ! [ ] ;
234- for b in boxes {
235- serialized_boxes. push ( self . serialized_box_from_id ( & b. box_id ( ) . into ( ) ) ?) ;
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 ( ) ) )
236165 }
237- Ok ( serialized_boxes)
238166 }
239167
240168 /// Given a P2S Ergo address, extract the hex-encoded serialized ErgoTree (script)
@@ -328,23 +256,6 @@ impl NodeInterface {
328256 }
329257 }
330258
331- /// Get the current nanoErgs balance held in the Ergo Node wallet
332- pub fn wallet_nano_ergs_balance ( & self ) -> Result < NanoErg > {
333- let endpoint = "/wallet/balances" ;
334- let res = self . send_get_req ( endpoint) ;
335- let res_json = self . parse_response_to_json ( res) ?;
336-
337- let balance = res_json[ "balance" ] . clone ( ) ;
338-
339- if balance. is_null ( ) {
340- Err ( NodeError :: NodeSyncing )
341- } else {
342- balance
343- . as_u64 ( )
344- . ok_or_else ( || NodeError :: FailedParsingNodeResponse ( res_json. to_string ( ) ) )
345- }
346- }
347-
348259 /// Get the current block height of the blockchain
349260 pub fn current_block_height ( & self ) -> Result < BlockHeight > {
350261 let endpoint = "/info" ;
@@ -363,81 +274,67 @@ impl NodeInterface {
363274 }
364275 }
365276
366- /// Get wallet status /wallet/status
367- pub fn wallet_status ( & self ) -> Result < WalletStatus > {
368- let endpoint = "/wallet/status" ;
369- let res = self . send_get_req ( endpoint) ;
370- let res_json = self . parse_response_to_json ( res) ?;
277+ /// Get the current state context of the blockchain
278+ 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) ;
371285
372- if let Ok ( wallet_status) = from_str ( & res_json. to_string ( ) ) {
373- Ok ( wallet_status)
374- } else {
375- Err ( NodeError :: FailedParsingWalletStatus ( res_json. pretty ( 2 ) ) )
376- }
286+ Ok ( state_context)
377287 }
378288
379- /// Unlock wallet
380- pub fn wallet_unlock ( & self , password : & str ) -> Result < bool > {
381- let endpoint = "/wallet/unlock" ;
382- let body = object ! {
383- pass: password,
384- } ;
289+ /// Get the last `number` of block headers from the blockchain
290+ 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) ?;
385294
386- let res = self . send_post_req ( endpoint , body . to_string ( ) ) ? ;
295+ let mut headers : Vec < Header > = vec ! [ ] ;
387296
388- if res. status ( ) . is_success ( ) {
389- Ok ( true )
390- } else {
391- let json = self . parse_response_to_json ( Ok ( res) ) ?;
392- Err ( NodeError :: BadRequest ( json[ "error" ] . to_string ( ) ) )
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+ }
393304 }
305+ Ok ( headers)
394306 }
395- }
396307
397- #[ serde_as]
398- #[ derive( serde:: Deserialize , serde:: Serialize ) ]
399- pub struct WalletStatus {
400- #[ serde( rename = "isInitialized" ) ]
401- pub initialized : bool ,
402- #[ serde( rename = "isUnlocked" ) ]
403- pub unlocked : bool ,
404- #[ serde( rename = "changeAddress" ) ]
405- #[ serde_as( as = "NoneAsEmptyString" ) ]
406- pub change_address : Option < P2PKAddressString > ,
407- #[ serde( rename = "walletHeight" ) ]
408- pub height : BlockHeight ,
409- #[ serde( rename = "error" ) ]
410- pub error : Option < String > ,
411- }
308+ /// Checks if the blockchain indexer is active by querying the node.
309+ 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) ?;
412313
413- #[ cfg( test) ]
414- mod tests {
415- use super :: * ;
416-
417- #[ test]
418- fn test_parsing_wallet_status_unlocked ( ) {
419- let node_response_json_str = r#"{
420- "isInitialized": true,
421- "isUnlocked": true,
422- "changeAddress": "3Wwc4HWrTcYkRycPNhEUSwNNBdqSBuiHy2zFvjMHukccxE77BaX3",
423- "walletHeight": 251965,
424- "error": ""
425- }"# ;
426- let t: WalletStatus = serde_json:: from_str ( node_response_json_str) . unwrap ( ) ;
427- assert_eq ! ( t. height, 251965 ) ;
428- }
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+ }
429321
430- # [ test ]
431- fn test_parsing_wallet_status_locked ( ) {
432- let node_response_json_str = r#"{
433- "isInitialized": true,
434- "isUnlocked": false,
435- "changeAddress": "",
436- "walletHeight": 251965,
437- "error": ""
438- }"# ;
439- let t : WalletStatus = serde_json :: from_str ( node_response_json_str ) . unwrap ( ) ;
440- assert_eq ! ( t . change_address , None ) ;
441- assert_eq ! ( t . height , 251965 ) ;
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+ } )
442334 }
443335}
336+
337+ pub struct IndexerStatus {
338+ pub is_active : bool ,
339+ pub is_sync : bool ,
340+ }
0 commit comments