diff --git a/Cargo.toml b/Cargo.toml index a5e3707..9828f9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,18 +10,20 @@ readme = "README.md" keywords = ["ergo", "blockchain", "node-interface", "dApp"] categories = ["cryptography::cryptocurrencies"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -json = "0.12.4" -openssl = { version = "0.10", features = ["vendored"] } -reqwest = { version = "0.11.4", features = ["blocking"] } -serde = "1.0" -serde_json = "1.0" +json = "0.12.4" +openssl = { version = "0.10", features = ["vendored"] } +reqwest = { version = "0.11.4", features = ["blocking"] } +serde = "1.0" +serde_json = "1.0" +serde_with = { version = "1.14", features = ["json"] } ergo-lib = { version = "0.23" } # ergo-lib = { git = "https://github.com/ergoplatform/sigma-rust", rev = "26ca5c4dddc96186f767e09ea1885b675c70e0f3" } -thiserror = "1.0.22" -blake2b_simd = "0.5.11" -base16 = "0.2.1" -yaml-rust = "0.4.4" -serde_with = { version = "1.14", features = ["json"] } +blake2b_simd = "0.5.11" +base16 = "0.2.1" +yaml-rust = "0.4.4" +thiserror = "1.0.22" +derive_more = "0.99" + +[dev-dependencies] +expect-test = "1.0.1" diff --git a/src/lib.rs b/src/lib.rs index 0f224a5..8608ce6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,10 +7,11 @@ pub mod node_interface; mod requests; pub mod scanning; pub mod transactions; +mod types; pub use local_config::*; pub use node_interface::NodeInterface; -pub use scanning::Scan; +pub use types::*; /// A Base58 encoded String of a Ergo P2PK address. pub type P2PKAddressString = String; @@ -26,5 +27,3 @@ pub type BlockHeight = u64; pub type BlockDuration = u64; /// A Base58 encoded String of a Token ID. pub type TokenID = String; -/// Integer which is provided by the Ergo node to reference a given scan. -pub type ScanID = String; diff --git a/src/node_interface.rs b/src/node_interface.rs index ecf2cb9..f3f00d8 100644 --- a/src/node_interface.rs +++ b/src/node_interface.rs @@ -38,6 +38,8 @@ pub enum NodeError { FailedParsingWalletStatus(String), #[error("Failed to parse URL: {0}")] InvalidUrl(String), + #[error("Failed to parse scan ID: {0}")] + InvalidScanId(String), } /// The `NodeInterface` struct which holds the relevant Ergo node data diff --git a/src/scanning.rs b/src/scanning.rs index 7f1c166..6de18dd 100644 --- a/src/scanning.rs +++ b/src/scanning.rs @@ -1,149 +1,48 @@ -/// A struct `Scan` is defined here which wraps the concept of UTXO-set -/// scanning in a Rust-based struct interface. +//! A struct `Scan` is defined here which wraps the concept of UTXO-set +//! scanning in a Rust-based struct interface. + use crate::node_interface::NodeInterface; pub use crate::node_interface::{NodeError, Result}; -use crate::{P2PKAddressString, ScanID}; +use crate::ScanId; use ergo_lib::ergotree_ir::chain::ergo_box::ErgoBox; -use json; -use json::JsonValue; -use serde_json::from_str; - -/// A `Scan` is a name + scan_id for a given scan with extra methods for acquiring boxes. -#[derive(Debug, Clone)] -pub struct Scan { - pub name: String, - pub id: ScanID, - pub node_interface: NodeInterface, -} - -impl Scan { - /// Manually create a new `Scan` struct. It is assumed that - /// a scan with the given `id` has already been registered - /// with the Ergo Node and the developer is simply creating - /// a struct for the given scan. - pub fn new(name: &str, scan_id: &str, node_interface: &NodeInterface) -> Scan { - Scan { - name: name.to_string(), - id: scan_id.to_string(), - node_interface: node_interface.clone(), - } - } - - /// Register a new scan in the Ergo Node and builds/returns - /// a `Scan` struct in a `Result`. - pub fn register( - name: &String, - tracking_rule: JsonValue, - node_interface: &NodeInterface, - ) -> Result { - let scan_json = object! { - scanName: name.to_string(), - trackingRule: tracking_rule, - }; - - let scan_id = node_interface.register_scan(&scan_json)?; - Ok(Scan::new(name, &scan_id, node_interface)) - } - - /// Returns all `ErgoBox`es found by the scan - pub fn get_boxes(&self) -> Result> { - let boxes = self.node_interface.scan_boxes(&self.id)?; - Ok(boxes) - } - - /// Returns the first `ErgoBox` found by the scan - pub fn get_box(&self) -> Result { - self.get_boxes()? - .into_iter() - .next() - .ok_or(NodeError::NoBoxesFound) - } - - /// Returns all `ErgoBox`es found by the scan - /// serialized and ready to be used as rawInputs - pub fn get_serialized_boxes(&self) -> Result> { - let boxes = self.node_interface.serialize_boxes(&self.get_boxes()?)?; - Ok(boxes) - } - - /// Returns the first `ErgoBox` found by the registered scan - /// serialized and ready to be used as a rawInput - pub fn get_serialized_box(&self) -> Result { - let ser_box = self.node_interface.serialize_box(&self.get_box()?)?; - Ok(ser_box) - } - - /// Saves UTXO-set scans (specifically id) to local scanIDs.json - pub fn save_scan_ids_locally(scans: Vec) -> Result { - let mut id_json = object! {}; - let mut json_list: Vec = vec![]; - for scan in scans { - if &scan.id == "null" { - return Err(NodeError::FailedRegisteringScan(scan.name)); - } - let sub_json = object! {name: scan.name, id: scan.id}; - json_list.push(sub_json); - } - id_json["scans"] = json_list.into(); - std::fs::write("scanIDs.json", json::stringify_pretty(id_json, 4)).map_err(|_| { - NodeError::Other("Failed to save scans to local scanIDs.json".to_string()) - })?; - Ok(true) - } - - /// Read UTXO-set scan ids from local scanIDs.json - pub fn read_local_scan_ids(node: &NodeInterface) -> Result> { - let file_string = &std::fs::read_to_string("scanIDs.json") - .map_err(|_| NodeError::Other("Unable to read scanIDs.json".to_string()))?; - let scan_json = json::parse(file_string) - .map_err(|_| NodeError::Other("Failed to parse scanIDs.json".to_string()))?; - - let scans: &Vec = &scan_json["scans"] - .members() - .map(|scan| Scan::new(&scan["name"].to_string(), &scan["id"].to_string(), node)) - .collect(); - - Ok(scans.clone()) - } - - /// Serialize a "P2PKAddressString" to be used within a scan tracking rule - pub fn serialize_p2pk_for_tracking( - node: &NodeInterface, - address: &P2PKAddressString, - ) -> Result { - let raw = node.p2pk_to_raw(address)?; - Ok("0e240008cd".to_string() + &raw) - } -} +use serde_json::{from_str, Value}; +use serde_json::{json, to_string_pretty}; /// Scanning-related endpoints impl NodeInterface { - // Initiates a rescan of the blockchain history, thereby finding boxes - // which may have been missed by a scan been added the block height - // that a box was created. - // Note: Rescanning can take a long time. - // pub fn rescan_blockchain_history(&self) -> Result { - // todo!(); - // } - /// Registers a scan with the node and either returns the `scan_id` /// or an error - pub fn register_scan(&self, scan_json: &JsonValue) -> Result { + pub fn register_scan(&self, scan_json: Value) -> Result { let endpoint = "/scan/register"; - let body = scan_json.clone().to_string(); + let body = scan_json.to_string(); + let res = self.send_post_req(endpoint, body); + let res_json = self.parse_response_to_json(res)?; + + if res_json["error"].is_null() { + let scan_id = res_json["scanId"].to_string().parse::()?; + Ok(scan_id) + } else { + Err(NodeError::BadRequest(res_json["error"].to_string())) + } + } + + pub fn deregister_scan(&self, scan_id: ScanId) -> Result { + let endpoint = "/scan/deregister"; + let body = generate_deregister_scan_json(scan_id); let res = self.send_post_req(endpoint, body); let res_json = self.parse_response_to_json(res)?; if res_json["error"].is_null() { - Ok(res_json["scanId"].to_string()) + let scan_id = res_json["scanId"].to_string().parse::()?; + Ok(scan_id) } else { Err(NodeError::BadRequest(res_json["error"].to_string())) } } /// Using the `scan_id` of a registered scan, acquires unspent boxes which have been found by said scan - pub fn scan_boxes(&self, scan_id: &ScanID) -> Result> { - let endpoint = "/scan/unspentBoxes/".to_string() + scan_id; + pub fn scan_boxes(&self, scan_id: ScanId) -> Result> { + let endpoint = format!("/scan/unspentBoxes/{scan_id}"); let res = self.send_get_req(&endpoint); let res_json = self.parse_response_to_json(res)?; @@ -167,23 +66,18 @@ impl NodeInterface { /// Using the `scan_id` of a registered scan, manually adds a box to said /// scan. - pub fn add_box_to_scan(&self, scan_id: &ScanID, box_id: &String) -> Result { + pub fn add_box_to_scan(&self, scan_id: ScanId, box_id: &String) -> Result { let ergo_box = serde_json::to_string(&self.box_from_id(box_id)?) .map_err(|_| NodeError::FailedParsingBox(box_id.clone()))?; - - let scan_id_int: u64 = scan_id - .parse() - .map_err(|_| NodeError::Other("Scan ID was not a valid integer number.".to_string()))?; - + let scan_id_int: u64 = scan_id.into(); let endpoint = "/scan/addBox"; - let body = object! { + + let body = json! ({ "scanIds": vec![scan_id_int], "box": ergo_box, - }; - + }); let res = self.send_post_req(endpoint, body.to_string()); let res_json = self.parse_response_to_json(res)?; - if res_json["error"].is_null() { Ok(res_json.to_string()) } else { @@ -191,3 +85,25 @@ impl NodeInterface { } } } + +fn generate_deregister_scan_json(scan_id: ScanId) -> String { + let body = json!({ + "scanId": scan_id, + }); + to_string_pretty(&body).unwrap() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_deregister_scan_json() { + let scan_id = ScanId::from(100); + expect_test::expect![[r#" + { + "scanId": 100 + }"#]] + .assert_eq(&generate_deregister_scan_json(scan_id)); + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..43036b2 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,20 @@ +use std::str::FromStr; + +use derive_more::{Display, From, Into}; +use serde::{Deserialize, Serialize}; + +use crate::node_interface::NodeError; + +#[derive(Debug, Copy, Clone, From, Into, Display, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct ScanId(u64); + +impl FromStr for ScanId { + type Err = NodeError; + + fn from_str(s: &str) -> Result { + let scan_id = s + .parse::() + .map_err(|_| NodeError::InvalidScanId(s.to_string()))?; + Ok(ScanId(scan_id)) + } +}