From bf5a37e71bd5380526242e0b7576e2ad358a4e92 Mon Sep 17 00:00:00 2001 From: Dimitris Frangiadakis Date: Tue, 12 Mar 2024 00:56:34 +0100 Subject: [PATCH] Transactions with signature --- Cargo.lock | 160 +++++++++++++++++++++++++--- Cargo.toml | 16 ++- LICENSE | 21 ++++ README.md | 87 +++++++++++++++ SECURITY.md | 15 +++ src/icon_service.rs | 27 ++--- src/lib.rs | 12 +-- src/query.rs | 0 src/transaction.rs | 209 +++++++++++++++++++++++++++++++++++++ src/transaction_builder.rs | 194 ---------------------------------- src/utils/decoder.rs | 0 src/utils/encoder.rs | 0 src/utils/mod.rs | 5 +- src/utils/serializer.rs | 64 ++++++++++++ src/wallet.rs | 62 +++++++++++ tests/test.rs | 37 ++++--- 16 files changed, 661 insertions(+), 248 deletions(-) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md delete mode 100644 src/query.rs delete mode 100644 src/transaction_builder.rs delete mode 100644 src/utils/decoder.rs delete mode 100644 src/utils/encoder.rs create mode 100644 src/utils/serializer.rs diff --git a/Cargo.lock b/Cargo.lock index f517276..fa7075b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,21 @@ dependencies = [ "version_check", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -61,6 +76,28 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -170,6 +207,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.4", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -394,6 +445,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" + [[package]] name = "http" version = "0.2.12" @@ -466,17 +523,45 @@ dependencies = [ ] [[package]] -name = "icon-sdk-rust" -version = "0.1.0" +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icon-sdk" +version = "1.0.0" dependencies = [ + "base64 0.22.0", + "chrono", "hex", "num-bigint", "num-traits", + "rand", "reqwest", "rust_decimal", + "secp256k1", "serde", "serde_json", "sha3", + "thiserror", "tokio", ] @@ -766,9 +851,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -849,11 +934,11 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.25" +version = "0.11.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" +checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -957,7 +1042,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", ] [[package]] @@ -981,6 +1066,26 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -1125,20 +1230,20 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "system-configuration" -version = "0.6.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "bitflags 2.4.2", + "bitflags 1.3.2", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ "core-foundation-sys", "libc", @@ -1162,6 +1267,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1424,6 +1549,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index b200c6b..7ea1402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,14 @@ [package] -name = "icon-sdk-rust" -version = "0.1.0" +name = "icon-sdk" +version = "1.0.0" edition = "2021" +description = "ICON(ICX) SDK for Rust" +authors = ["Dimitris Frangiadakis "] +readme = "README.md" +repository = "https://github.com/mitsosf/icon-sdk-rust" +license = "MIT" +keywords = ["icon", "icx", "sdk", "blockchain"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] reqwest = { version = "0.11.25", features = ["json"] } @@ -15,3 +20,8 @@ num-traits = "0.2.18" rust_decimal = "1.34.3" sha3 = "0.10.8" hex = "0.4.3" +rand = "0.8.5" +secp256k1 = { version = "0.28.2", features = ["rand", "recovery", "hashes"] } +thiserror = "1.0.57" +chrono = "0.4.35" +base64 = "0.22.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3db6986 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Dimitrios Frangiadakis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c82327d --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +

+ ICONation logo +

+ +

ICON SDK for Rust

+ +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +This is an SDK to communicate with the ICON blockchain, built for Rust. + +Disclaimer: I cannot guarantee optimal performance of this software. It is provided as is and without any assurances. Use it at your own risk. + +Features +-------- + +Fully or partially supports all Iconservice functions, IRC-2 tokens, and IISS calls. + +Installation +-------- + +To use the SDK in your Rust project, add the following to your `Cargo.toml`: + + +```toml +[dependencies] +icon-sdk = "1.0.1" +``` + +Testing +-------- +To run tests, ensure you have Rust installed and run: +```shell +cargo test +``` + +Usage +-------- +### Get block information +```rust +use icon_sdk::icon_service; + +async fn main() { + // Example: get last block + let last_block = icon_service::get_last_block().await; + println!("{:?}", last_block); + + // Example: get block by height + let block_by_height = icon_service::get_block_by_height("0x3").await; + println!("{:?}", block_by_height); + + // Example: get block by hash + let block_by_hash = icon_service::get_block_by_hash("0x123986e1c834632f6e65915c249d81cd01453ec915e3370d364d6df7be5e6c03").await; + println!("{:?}", block_by_hash); +} +``` + +### Send ICX +```rust +use icon_sdk::{icon_service, wallet::Wallet}; + +#[tokio::main] +async fn main() { + let wallet = Wallet::new(None); //Or load a wallet from a private key + + let from = wallet.get_public_address(); // Sender's address + let to = "hx9ab3078e72c8d9017194d17b34b1a47b661945ca"; + let value = "100"; // Amount to send in ICX or hex encoded value for tokens + let version = "0x3"; + let nid = "0x3"; + let nonce = "0x1234"; + let step_limit = "0x186a0"; + + // Send the transaction + match icon_service::send_transaction(wallet, from, to, value, version, nid, nonce, step_limit).await { + Ok(response) => println!("Transaction sent successfully: {:?}", response), + Err(err) => eprintln!("Error sending transaction: {}", err), + } +} +``` + +Coming soon +-------- +- More examples +- More RPC calls implemented \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..be2b7e2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported Versions + +The following versions of the project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 1.0.0 | :white_check_mark: | + +## Reporting a Vulnerability + +Please send an email to [dimitris@frangiadakis.com](mailto:dimitris@frangiadakis.com) to report potential security vulnerabilities. +All contributions will be acknowledged, when a patch is released. \ No newline at end of file diff --git a/src/icon_service.rs b/src/icon_service.rs index 7566e7a..ca113b6 100644 --- a/src/icon_service.rs +++ b/src/icon_service.rs @@ -1,9 +1,10 @@ use std::error::Error; use serde_json::{Value}; -use crate::transaction_builder::TransactionBuilder; +use crate::transaction::Transaction; +use crate::wallet::Wallet; pub async fn get_last_block() -> Result> { - let transaction_builder = TransactionBuilder::new() + let transaction_builder = Transaction::new() .method("icx_getLastBlock"); let response: Value = transaction_builder.send().await.map_err(|e| Box::new(e) as Box)?; @@ -12,7 +13,7 @@ pub async fn get_last_block() -> Result> { } pub async fn get_block_by_height(block_height: &str) -> Result> { - let transaction_builder = TransactionBuilder::new() + let transaction_builder = Transaction::new() .method("icx_getBlockByHeight").block_height(block_height); let response: Value = transaction_builder.send().await.map_err(|e| Box::new(e) as Box)?; @@ -21,7 +22,7 @@ pub async fn get_block_by_height(block_height: &str) -> Result Result> { - let transaction_builder = TransactionBuilder::new() + let transaction_builder = Transaction::new() .method("icx_getBlockByHash").block_hash(block_hash); let response: Value = transaction_builder.send().await.map_err(|e| Box::new(e) as Box)?; @@ -30,7 +31,7 @@ pub async fn get_block_by_hash(block_hash: &str) -> Result } pub async fn get_balance(address: &str) -> Result> { - let transaction_builder = TransactionBuilder::new() + let transaction_builder = Transaction::new() .method("icx_getBalance").address(address); let response: Value = transaction_builder.send().await.map_err(|e| Box::new(e) as Box)?; @@ -38,19 +39,21 @@ pub async fn get_balance(address: &str) -> Result> { Ok(response) } -pub async fn send_transaction(from: &str, to: &str, value: &str, version: &str, nid: &str, nonce: &str, step_limit: &str) -> Result> { - let transaction_builder = TransactionBuilder::new() +pub async fn send_transaction(wallet: Wallet, from: &str, to: &str, value: &str, version: &str, nid: &str, nonce: &str, step_limit: &str) -> Result> { + let transaction_builder = Transaction::new() + .icon_service_url("https://lisbon.net.solidwallet.io/api/v3") .method("icx_sendTransaction") .from(from) .to(to) .value(value) .version(version) .nid(nid) + .timestamp() .nonce(nonce) .step_limit(step_limit) - .serialize(true); - Ok(transaction_builder) - // let response: Value = transaction_builder.map_err(|e| Box::new(e) as Box)?; - // - // Ok(response) + .sign(wallet.get_private_key().as_str()); + + let response: Value = transaction_builder.send().await.map_err(|e| Box::new(e) as Box)?; + + Ok(response) } diff --git a/src/lib.rs b/src/lib.rs index fc9d1f8..f9b80a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,4 @@ pub mod utils; pub mod icon_service; -mod query; -mod transaction; -pub mod transaction_builder; -mod wallet; - -pub fn add(left: usize, right: usize) -> usize { - left + right -} - - +pub mod transaction; +pub mod wallet; diff --git a/src/query.rs b/src/query.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/transaction.rs b/src/transaction.rs index e69de29..ce0c7ec 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -0,0 +1,209 @@ +use std::str::FromStr; +use reqwest::{Client, StatusCode}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value, Map}; +use secp256k1::{Message, Secp256k1, SecretKey}; +use hex::{encode, decode}; +use crate::utils::serializer::Serializer; +use chrono::Utc; +use base64::{Engine as _, engine::{general_purpose as base64_encoder}}; +use hex::FromHex; +use rust_decimal::Decimal; + +use thiserror::Error; +use crate::utils::helpers::icx_to_hex; + +#[derive(Default, Serialize, Deserialize)] +pub struct Transaction { + icon_service_url: Option, + data: Value, +} + +#[derive(Error, Debug)] +pub enum MyError { + #[error("request failed")] + Request(#[from] reqwest::Error), + + #[error("unexpected response: {0}")] + UnexpectedResponse(String), +} + +impl Transaction { + pub fn new() -> Self { + Self { + icon_service_url: None, + data: json!({ + "jsonrpc": "2.0", + "id": 1234 + }), + } + } + + pub fn icon_service_url(mut self, url: &str) -> Self { + self.icon_service_url = Some(url.to_string()); + self + } + + pub fn method(mut self, method: &str) -> Self { + // Use as_object_mut() to get a mutable reference to the data object + if let Some(obj) = self.data.as_object_mut() { + // Insert or modify the "method" field + obj.insert("method".to_string(), json!(method)); + } + self + } + + pub fn set_params(mut self, params: &Map) -> Self { + // Ensure `data` has a "params" object; create it if not + let data_obj = self.data.as_object_mut().expect("data is not an object"); + let params_obj = data_obj.entry("params").or_insert_with(|| json!({})).as_object_mut().unwrap(); + + // Insert or update the given parameters + for (key, value) in params { + params_obj.insert(key.clone(), value.clone()); + } + + self + } + + pub fn block_height(self, block_height: &str) -> Self { + let mut params = Map::new(); + params.insert("height".to_string(), json!(block_height)); + + self.set_params(¶ms) + } + + pub fn block_hash(self, block_hash: &str) -> Self { + let mut params = Map::new(); + params.insert("hash".to_string(), json!(block_hash)); + + self.set_params(¶ms) + } + + pub fn address(self, address: &str) -> Self { + let mut params = Map::new(); + params.insert("address".to_string(), json!(address)); + + self.set_params(¶ms) + } + + pub fn from(self, from: &str) -> Self { + let mut params = Map::new(); + params.insert("from".to_string(), json!(from)); + + self.set_params(¶ms) + } + + pub fn to(self, to: &str) -> Self { + let mut params = Map::new(); + params.insert("to".to_string(), json!(to)); + + self.set_params(¶ms) + } + + pub fn value(self, value: &str) -> Self { + let mut params = Map::new(); + let mut parsed_value = value.to_string(); + + if !parsed_value.starts_with("0x") { + match icx_to_hex(Decimal::from_str(value).expect("Invalid value")) { + Some(v) => { + parsed_value = v; + } + None => panic!("Failed to convert value to hex"), + } + } + + params.insert("value".to_string(), json!(parsed_value)); + + + self.set_params(¶ms) + } + + pub fn version(self, version: &str) -> Self { + let mut params = Map::new(); + params.insert("version".to_string(), json!(version)); + + self.set_params(¶ms) + } + + pub fn nid(self, nid: &str) -> Self { + let mut params = Map::new(); + params.insert("nid".to_string(), json!(nid)); + + self.set_params(¶ms) + } + + pub fn nonce(self, nonce: &str) -> Self { + let mut params = Map::new(); + params.insert("nonce".to_string(), json!(nonce)); + + self.set_params(¶ms) + } + + pub fn step_limit(self, step_limit: &str) -> Self { + let mut params = Map::new(); + params.insert("stepLimit".to_string(), json!(step_limit)); + + self.set_params(¶ms) + } + + pub fn timestamp(self) -> Self { + let now = Utc::now(); + let timestamp_in_micros = now.timestamp_micros(); + let hex_timestamp = format!("0x{:x}", timestamp_in_micros); + + let mut params = Map::new(); + params.insert("timestamp".to_string(), json!(hex_timestamp.to_string())); + + self.set_params(¶ms) + } + + pub fn sign(self, private_key: &str) -> Self { + let serialized_transaction = Serializer::serialize_transaction(&self.data["params"], true); + let serialized_transaction_bytes = Vec::from_hex(serialized_transaction).expect("Invalid hex string"); + + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&decode(private_key).expect("Invalid private key")).expect("Failed to create secret key"); + + let message = Message::from_digest_slice(serialized_transaction_bytes.as_slice()).expect("Failed to create message"); + let sig = secp.sign_ecdsa_recoverable(&message, &secret_key); + + let (rec_id, sig_bytes) = sig.serialize_compact(); + // Concatenate r, s, and recovery ID + let signature = format!("{}{:02x}", encode(sig_bytes), rec_id.to_i32()); + + let signature_bytes = decode(signature).expect("Failed to decode hex"); + let transaction_signature = base64_encoder::STANDARD.encode(signature_bytes); + + let mut params = Map::new(); + params.insert("signature".to_string(), json!(transaction_signature)); + + self.set_params(¶ms) + } + + pub fn get_signature(self) -> Value{ + self.data["params"]["signature"].clone() + } + + + + pub async fn send(self) -> Result { + let client = Client::new(); + let url = self.icon_service_url.unwrap_or_else(|| "https://api.icon.community/api/v3".to_string()); + let data = self.data; + + let res = client.post(&url) + .json(&data) + .send() + .await?; + + match res.status() { + StatusCode::OK => Ok(res.json().await?), + _ => { + let error_message = res.text().await.unwrap_or_else(|_| "Unknown error".to_string()); + Err(MyError::UnexpectedResponse(error_message)) + }, + } + } +} diff --git a/src/transaction_builder.rs b/src/transaction_builder.rs deleted file mode 100644 index d3bac7b..0000000 --- a/src/transaction_builder.rs +++ /dev/null @@ -1,194 +0,0 @@ -use reqwest::{Client, Error}; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value, Map}; -use sha3::{Digest, Sha3_256}; -use std::collections::BTreeMap; - -#[derive(Default, Serialize, Deserialize)] -pub struct TransactionBuilder { - icon_service_url: Option, - data: Value, -} - -impl TransactionBuilder { - pub fn new() -> Self { - Self { - icon_service_url: None, - // Set default values for `data` here - data: json!({ - "jsonrpc": "2.0", - "id": 1234 - }), - } - } - - pub fn icon_service_url(mut self, url: &str) -> Self { - self.icon_service_url = Some(url.to_string()); - self - } - - pub fn method(mut self, method: &str) -> Self { - // Use as_object_mut() to get a mutable reference to the data object - if let Some(obj) = self.data.as_object_mut() { - // Insert or modify the "method" field - obj.insert("method".to_string(), json!(method)); - } - self - } - - pub fn set_params(mut self, params: &Map) -> Self { - // Ensure `data` has a "params" object; create it if not - let data_obj = self.data.as_object_mut().expect("data is not an object"); - let params_obj = data_obj.entry("params").or_insert_with(|| json!({})).as_object_mut().unwrap(); - - // Insert or update the given parameters - for (key, value) in params { - params_obj.insert(key.clone(), value.clone()); - } - - self - } - - pub fn block_height(self, block_height: &str) -> Self { - let mut params = Map::new(); - params.insert("height".to_string(), json!(block_height)); - - self.set_params(¶ms) - } - - pub fn block_hash(self, block_hash: &str) -> Self { - let mut params = Map::new(); - params.insert("hash".to_string(), json!(block_hash)); - - self.set_params(¶ms) - } - - pub fn address(self, address: &str) -> Self { - let mut params = Map::new(); - params.insert("address".to_string(), json!(address)); - - self.set_params(¶ms) - } - - pub fn from(self, from: &str) -> Self { - let mut params = Map::new(); - params.insert("from".to_string(), json!(from)); - - self.set_params(¶ms) - } - - pub fn to(self, to: &str) -> Self { - let mut params = Map::new(); - params.insert("to".to_string(), json!(to)); - - self.set_params(¶ms) - } - - pub fn value(self, value: &str) -> Self { - let mut params = Map::new(); - params.insert("value".to_string(), json!(value)); - - self.set_params(¶ms) - } - - pub fn version(self, version: &str) -> Self { - let mut params = Map::new(); - params.insert("version".to_string(), json!(version)); - - self.set_params(¶ms) - } - - pub fn nid(self, nid: &str) -> Self { - let mut params = Map::new(); - params.insert("nid".to_string(), json!(nid)); - - self.set_params(¶ms) - } - - pub fn nonce(self, nonce: &str) -> Self { - let mut params = Map::new(); - params.insert("nonce".to_string(), json!(nonce)); - - self.set_params(¶ms) - } - - pub fn step_limit(self, step_limit: &str) -> Self { - let mut params = Map::new(); - params.insert("stepLimit".to_string(), json!(step_limit)); - - self.set_params(¶ms) - } - - pub fn serialize(&self, hashed: bool) -> String { - let result_str = Self::value_traverse(&self.data["params"]); - let result_string_replaced = &result_str[1..result_str.len() - 1]; - let result = format!("icx_sendTransaction.{}", result_string_replaced); - - if hashed { - format!("{}", hex::encode(sha3::Sha3_256::digest(result.as_bytes()))) - } else { - result - } - } - - fn value_traverse(value: &Value) -> String { - match value { - Value::Object(obj) => { - let mut result = "{".to_string(); - let sorted: BTreeMap<_, _> = obj.iter().collect(); - for (key, val) in &sorted { - result.push_str(&format!("{}.", key)); - result.push_str(&Self::value_traverse(val)); - result.push('.'); - } - if result.ends_with('.') { - result.pop(); - } - result.push('}'); - result - }, - Value::Array(arr) => { - let mut result = "[".to_string(); - for val in arr { - result.push_str(&Self::value_traverse(val)); - result.push('.'); - } - if result.ends_with('.') { - result.pop(); - } - result.push(']'); - result - }, - Value::String(s) => Self::escape_string(s), - Value::Number(n) => n.to_string(), - Value::Bool(b) => b.to_string(), - Value::Null => "\\0".to_string(), - } - } - - fn escape_string(value: &str) -> String { - value.replace("\\", "\\\\") - .replace(".", "\\.") - .replace("{", "\\{") - .replace("}", "\\}") - .replace("[", "\\[") - .replace("]", "\\]") - } - - pub async fn send(self) -> Result { - let client = Client::new(); - let url = self.icon_service_url.unwrap_or_else(|| "https://api.icon.community/api/v3".to_string()); - let data = self.data; - println!("{:?}", data); - - let res = client.post(&url) - .json(&data) - .send() - .await?; - - match res.status() { - reqwest::StatusCode::OK => Ok(res.json().await?), - other => panic!("HTTP Request Failed with status: {}", other), - } - } -} diff --git a/src/utils/decoder.rs b/src/utils/decoder.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/encoder.rs b/src/utils/encoder.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/mod.rs b/src/utils/mod.rs index b0785df..5d7201f 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,3 @@ -mod encoder; -mod decoder; pub mod helpers; -pub mod responses; \ No newline at end of file +pub mod responses; +pub mod serializer; diff --git a/src/utils/serializer.rs b/src/utils/serializer.rs new file mode 100644 index 0000000..4d5e8f2 --- /dev/null +++ b/src/utils/serializer.rs @@ -0,0 +1,64 @@ +use serde_json::Value; +use sha3::{Digest, Sha3_256}; +use std::collections::BTreeMap; +use hex::encode; + +pub struct Serializer; + +impl Serializer { + pub fn serialize_transaction(data: &Value, hashed: bool) -> String { + let result_str = Self::value_traverse(data); + let result_string_replaced = &result_str[1..result_str.len() - 1]; + let result = format!("icx_sendTransaction.{}", result_string_replaced); + + if hashed { + format!("{}", encode(Sha3_256::digest(result.as_bytes()))) + } else { + result + } + } + + fn value_traverse(value: &Value) -> String { + match value { + Value::Object(obj) => { + let mut result = "{".to_string(); + let sorted: BTreeMap<_, _> = obj.iter().collect(); + for (key, val) in sorted { + result.push_str(&format!("{}.", key)); + result.push_str(&Self::value_traverse(val)); + result.push('.'); + } + if result.ends_with('.') { + result.pop(); + } + result.push('}'); + result + }, + Value::Array(arr) => { + let mut result = "[".to_string(); + for val in arr { + result.push_str(&Self::value_traverse(val)); + result.push('.'); + } + if result.ends_with('.') { + result.pop(); + } + result.push(']'); + result + }, + Value::String(s) => Self::escape_string(s), + Value::Number(n) => n.to_string(), + Value::Bool(b) => b.to_string(), + Value::Null => "\\0".to_string(), + } + } + + fn escape_string(value: &str) -> String { + value.replace("\\", "\\\\") + .replace(".", "\\.") + .replace("{", "\\{") + .replace("}", "\\}") + .replace("[", "\\[") + .replace("]", "\\]") + } +} diff --git a/src/wallet.rs b/src/wallet.rs index e69de29..db22f81 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -0,0 +1,62 @@ +use secp256k1::rand::rngs::OsRng; +use secp256k1::{Secp256k1, SecretKey, PublicKey}; +use sha3::{Digest, Sha3_256}; +use hex::{encode, decode}; + +pub struct Wallet { + private_key: String, + public_key: String, + public_address: String, +} + +impl Wallet { + pub fn new(private_key: Option) -> Self { + let secp = Secp256k1::new(); + let (private_key, public_key) = match private_key { + Some(key_str) => { + // Decode the provided private key string and create a SecretKey + let private_key = SecretKey::from_slice(&decode(key_str).expect("Invalid private key format")).expect("Invalid private key"); + let public_key = PublicKey::from_secret_key(&secp, &private_key); + (private_key, public_key) + }, + None => { + // Generate a new keypair + let (private_key, public_key) = secp.generate_keypair(&mut OsRng::default()); + (private_key, public_key) + }, + }; + let public_key_hex = encode(public_key.serialize_uncompressed()[1..].to_vec()); + + let public_address = Wallet::pub_key_to_address(&public_key_hex); + + Wallet { + private_key: private_key.display_secret().to_string(), + public_key: public_key_hex, + public_address, + } + } + + fn pub_key_to_address(public_key: &str) -> String { + let digest = Sha3_256::digest(&decode(public_key).expect("Invalid hex in public key")); + let hex_digest = encode(&digest); + if hex_digest.len() >= 40 { + // Ensure the string is long enough before slicing to avoid panics + format!("hx{}", &hex_digest[hex_digest.len() - 40..]) + } else { + panic!("Digest too short"); + } + } + + // Accessor methods + pub fn get_private_key(&self) -> String { + self.private_key.clone() + } + + pub fn get_public_key(&self) -> String { + self.public_key.clone() + } + + pub fn get_public_address(&self) -> String { + self.public_address.clone() + } +} diff --git a/tests/test.rs b/tests/test.rs index 35a819e..d7792a4 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,7 +1,8 @@ use std::str::FromStr; use rust_decimal::Decimal; -use icon_sdk_rust::icon_service; -use icon_sdk_rust::utils::helpers::hex_to_icx; +use icon_sdk::icon_service; +use icon_sdk::utils::helpers; +use icon_sdk::wallet::Wallet; #[tokio::test] async fn test_get_last_block() -> Result<(), ()> { @@ -64,7 +65,7 @@ async fn test_get_balance() -> Result<(), ()> { } #[tokio::test] async fn test_hex_to_icx() -> Result<(), ()> { - let res = icon_sdk_rust::utils::helpers::hex_to_icx("0x63b5429420c741b16a10f"); + let res = helpers::hex_to_icx("0x63b5429420c741b16a10f"); match res { Some(response) => { assert_eq!(response.to_string(), "7533727.039631672546337039"); @@ -77,10 +78,10 @@ async fn test_hex_to_icx() -> Result<(), ()> { #[tokio::test] async fn test_icx_to_hex() -> Result<(), ()> { - let res = icon_sdk_rust::utils::helpers::icx_to_hex(Decimal::from_str("7533727.039631672546337039").unwrap()); + let res = helpers::icx_to_hex(Decimal::from_str("7533727.039631672546337039").unwrap()); match res { Some(response) => { - assert_eq!(response, "0x63d8bac040145a956a22a"); + assert_eq!(response, "0x63b5429420c741b16a10f"); println!("{:?}", response); }, None => println!("Error"), @@ -90,25 +91,35 @@ async fn test_icx_to_hex() -> Result<(), ()> { } #[tokio::test] -async fn test_serialize_transaction() -> Result<(), ()> { +async fn test_send_transaction() -> Result<(), ()> { + let wallet = Wallet::new(Some("f4ade1ff528c9e0bf10d35909e3486ef6ce88df8a183fc1cc2c65bfa9a53d3fd".to_string())); let res = icon_service::send_transaction( - "hx8dc6ae3d93e60a2dddf80bfc5fb1cd16a2bf6160", - "hxf8689d6c4c8f333651469fdea2ac59a18f6c242d", - "0x2386f26fc10000", + wallet, + "hxb14e0c751899676a1a4e655a34063b42260f844b", + "hxf8689d6c4c8f333651469fdea2ac59a18f6c2421", + "1.31231232", + "0x3", "0x2", "0x1", - "0x1", "0x186a0" ).await; match res { Ok(response) => { - assert_eq!(response, "308167c8113b6e6f3f9e7ba28f495af468ed636a72e411d99823a78c61d104b3") - // assert_eq!(response["jsonrpc"], "2.0"); - // assert!(!response.as_object().unwrap().contains_key("error")); + println!("{:?}", response); + assert_eq!(response["jsonrpc"], "2.0"); + assert!(!response.as_object().unwrap().contains_key("error")); }, Err(e) => println!("Error: {:?}", e), } Ok(()) } + +#[tokio::test] +async fn test_wallet() -> Result<(), ()> { + let wallet = Wallet::new(Some("f4ade1ff528c9e0bf10d35909e3486ef6ce88df8a183fc1cc2c65bfa9a53d3fd".to_string())); + assert_eq!(wallet.get_public_address(), "hxb14e0c751899676a1a4e655a34063b42260f844b"); + + Ok(()) +}