From 2ebb6b82c5abf42d287b01c56f94d630e063e254 Mon Sep 17 00:00:00 2001 From: Dimitris Frangiadakis Date: Wed, 20 Mar 2024 19:43:49 +0100 Subject: [PATCH] IRC2 support --- Cargo.lock | 25 ++++- Cargo.toml | 2 +- README.md | 8 +- src/irc2.rs | 146 +++++++++++++++++++++++++ src/lib.rs | 3 +- tests/test_helpers.rs | 30 +++++ tests/{test.rs => test_iconservice.rs} | 38 ------- tests/test_irc2.rs | 119 ++++++++++++++++++++ tests/test_wallet.rs | 9 ++ 9 files changed, 334 insertions(+), 46 deletions(-) create mode 100644 src/irc2.rs create mode 100644 tests/test_helpers.rs rename tests/{test.rs => test_iconservice.rs} (84%) create mode 100644 tests/test_irc2.rs create mode 100644 tests/test_wallet.rs diff --git a/Cargo.lock b/Cargo.lock index f14a149..7cfa322 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,22 @@ 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" @@ -429,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" @@ -525,7 +547,7 @@ dependencies = [ [[package]] name = "icon-sdk" -version = "1.1.0" +version = "1.2.0" dependencies = [ "base64 0.22.0", "chrono", @@ -1050,6 +1072,7 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ + "bitcoin_hashes", "rand", "secp256k1-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 364a475..7f24b94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "icon-sdk" -version = "1.1.0" +version = "1.2.0" edition = "2021" description = "ICON(ICX) SDK for Rust" authors = ["Dimitris Frangiadakis "] diff --git a/README.md b/README.md index 88d9cea..b99701a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ Features - Wallet management - Read data from the blockchain -- Send transactions +- Send ICX transactions +- Full IRC2 token support - Perform SCORE calls - Transaction builder @@ -30,7 +31,7 @@ To use the SDK in your Rust project, add the following to your `Cargo.toml`: ```toml [dependencies] -icon-sdk = "1.1.0" +icon-sdk = "1.2.0" ``` Testing @@ -121,6 +122,3 @@ let icon_service = icon_service::IconService::new(Some("https://lisbon.net.solid ### Use the transaction builder See `icon_service.rs` to see how to use the transaction builder. -Coming soon --------- -- IRC2 token support \ No newline at end of file diff --git a/src/irc2.rs b/src/irc2.rs new file mode 100644 index 0000000..1fdda5c --- /dev/null +++ b/src/irc2.rs @@ -0,0 +1,146 @@ +use std::error::Error; +use std::str::FromStr; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use crate::icon_service::IconService; +use crate::transaction_builder::TransactionBuilder; +use crate::utils::helpers::icx_to_hex; +use crate::wallet::Wallet; + +#[derive(Default, Serialize, Deserialize)] +pub struct IRC2 { + icon_service: IconService, + contract_address: String, +} + +impl IRC2 { + pub fn new(icon_service: IconService, contract_address: String) -> Self { + Self { + icon_service, + contract_address, + } + } + + pub async fn name(&self) -> Result> { + let transaction = TransactionBuilder::new(&self.icon_service) + .method("icx_call") + .to(&self.contract_address) + .call( + json!({ + "method": "name", + }) + ) + .build(); + + let response: Value = transaction.send().await.map_err(|e| Box::new(e) as Box)?; + + Ok(response) + } + + pub async fn symbol(&self) -> Result> { + let transaction = TransactionBuilder::new(&self.icon_service) + .method("icx_call") + .to(&self.contract_address) + .call( + json!({ + "method": "symbol", + }) + ) + .build(); + + let response: Value = transaction.send().await.map_err(|e| Box::new(e) as Box)?; + + Ok(response) + } + + pub async fn decimals(&self) -> Result> { + let transaction = TransactionBuilder::new(&self.icon_service) + .method("icx_call") + .to(&self.contract_address) + .call( + json!({ + "method": "decimals", + }) + ) + .build(); + + let response: Value = transaction.send().await.map_err(|e| Box::new(e) as Box)?; + + Ok(response) + } + + pub async fn total_supply(&self) -> Result> { + let transaction = TransactionBuilder::new(&self.icon_service) + .method("icx_call") + .to(&self.contract_address) + .call( + json!({ + "method": "totalSupply", + }) + ) + .build(); + + let response: Value = transaction.send().await.map_err(|e| Box::new(e) as Box)?; + + Ok(response) + } + + pub async fn balance_of(&self, account: String) -> Result> { + let transaction = TransactionBuilder::new(&self.icon_service) + .method("icx_call") + .to(&self.contract_address) + .call( + json!({ + "method": "balanceOf", + "params": { + "_owner": account, + } + }) + ) + .build(); + + let response: Value = transaction.send().await.map_err(|e| Box::new(e) as Box)?; + + Ok(response) + } + + pub async fn transfer(&self, wallet: Wallet, to: &str, value: &str, version: &str, nid: &str, nonce: &str, step_limit: &str) -> Result> { + 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"), + } + } + + let transaction = TransactionBuilder::new(&self.icon_service) + .method("icx_sendTransaction") + .from(wallet.get_public_address().as_str()) + .to(&self.contract_address) + .version(version) + .nid(nid) + .timestamp() + .nonce(nonce) + .step_limit(step_limit) + .call( + json!({ + "method": "transfer", + "params": { + "_to": to, + "_value": parsed_value, + } + }) + ) + .sign(wallet.get_private_key().as_str()) + .build(); + + let response: Value = transaction.send().await.map_err(|e| Box::new(e) as Box)?; + + Ok(response) + } + +} diff --git a/src/lib.rs b/src/lib.rs index d5fbd0d..6436878 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,5 @@ pub mod utils; pub mod icon_service; pub mod transaction; pub mod wallet; -mod transaction_builder; +pub mod transaction_builder; +pub mod irc2; diff --git a/tests/test_helpers.rs b/tests/test_helpers.rs new file mode 100644 index 0000000..1818475 --- /dev/null +++ b/tests/test_helpers.rs @@ -0,0 +1,30 @@ +use std::str::FromStr; +use rust_decimal::Decimal; +use icon_sdk::utils::helpers; + +#[tokio::test] +async fn test_hex_to_icx() -> Result<(), ()> { + let res = helpers::hex_to_icx("0x63b5429420c741b16a10f"); + match res { + Some(response) => { + assert_eq!(response.to_string(), "7533727.039631672546337039"); + }, + None => panic!("Error"), + } + + Ok(()) +} + +#[tokio::test] +async fn test_icx_to_hex() -> Result<(), ()> { + let res = helpers::icx_to_hex(Decimal::from_str("7533727.039631672546337039").unwrap()); + match res { + Some(response) => { + assert_eq!(response, "0x63b5429420c741b16a10f"); + println!("{:?}", response); + }, + None => panic!("Error"), + } + + Ok(()) +} diff --git a/tests/test.rs b/tests/test_iconservice.rs similarity index 84% rename from tests/test.rs rename to tests/test_iconservice.rs index 43d03de..cf563b3 100644 --- a/tests/test.rs +++ b/tests/test_iconservice.rs @@ -1,8 +1,5 @@ -use std::str::FromStr; -use rust_decimal::Decimal; use serde_json::json; use icon_sdk::icon_service; -use icon_sdk::utils::helpers; use icon_sdk::wallet::Wallet; #[tokio::test] @@ -124,33 +121,6 @@ async fn test_call() -> Result<(), ()> { Ok(()) } -#[tokio::test] -async fn test_hex_to_icx() -> Result<(), ()> { - let res = helpers::hex_to_icx("0x63b5429420c741b16a10f"); - match res { - Some(response) => { - assert_eq!(response.to_string(), "7533727.039631672546337039"); - }, - None => panic!("Error"), - } - - Ok(()) -} - -#[tokio::test] -async fn test_icx_to_hex() -> Result<(), ()> { - let res = helpers::icx_to_hex(Decimal::from_str("7533727.039631672546337039").unwrap()); - match res { - Some(response) => { - assert_eq!(response, "0x63b5429420c741b16a10f"); - println!("{:?}", response); - }, - None => panic!("Error"), - } - - Ok(()) -} - #[tokio::test] async fn test_send_transaction() -> Result<(), ()> { let wallet = Wallet::new(Some("f4ade1ff528c9e0bf10d35909e3486ef6ce88df8a183fc1cc2c65bfa9a53d3fd".to_string())); @@ -205,11 +175,3 @@ async fn test_send_transaction_with_message() -> Result<(), ()> { Ok(()) } - -#[tokio::test] -async fn test_wallet() -> Result<(), ()> { - let wallet = Wallet::new(Some("f4ade1ff528c9e0bf10d35909e3486ef6ce88df8a183fc1cc2c65bfa9a53d3fd".to_string())); - assert_eq!(wallet.get_public_address(), "hxb14e0c751899676a1a4e655a34063b42260f844b"); - - Ok(()) -} diff --git a/tests/test_irc2.rs b/tests/test_irc2.rs new file mode 100644 index 0000000..4414846 --- /dev/null +++ b/tests/test_irc2.rs @@ -0,0 +1,119 @@ +use icon_sdk::{icon_service, irc2}; + +#[tokio::test] +async fn test_name() -> Result<(), ()> { + let icon_service = icon_service::IconService::new(Some("https://lisbon.net.solidwallet.io/api/v3".to_string())); + let irc2 = irc2::IRC2::new(icon_service, "cx273548dff8bb77ffaac5a342c4c04aeae0bc48fa".to_string()); + let res = irc2.name().await; + match res { + Ok(response) => { + println!("{:?}", response); + assert_eq!(response["jsonrpc"], "2.0"); + assert!(!response.as_object().unwrap().contains_key("error")); + assert_eq!(response["result"], "MyIRC2Token") + }, + Err(e) => panic!("Error: {:?}", e), + } + + Ok(()) +} + +#[tokio::test] +async fn test_symbol() -> Result<(), ()> { + let icon_service = icon_service::IconService::new(Some("https://lisbon.net.solidwallet.io/api/v3".to_string())); + let irc2 = irc2::IRC2::new(icon_service, "cx273548dff8bb77ffaac5a342c4c04aeae0bc48fa".to_string()); + let res = irc2.symbol().await; + match res { + Ok(response) => { + println!("{:?}", response); + assert_eq!(response["jsonrpc"], "2.0"); + assert!(!response.as_object().unwrap().contains_key("error")); + assert_eq!(response["result"], "MIT") + }, + Err(e) => panic!("Error: {:?}", e), + } + + Ok(()) +} + +#[tokio::test] +async fn test_decimals() -> Result<(), ()> { + let icon_service = icon_service::IconService::new(Some("https://lisbon.net.solidwallet.io/api/v3".to_string())); + let irc2 = irc2::IRC2::new(icon_service, "cx273548dff8bb77ffaac5a342c4c04aeae0bc48fa".to_string()); + let res = irc2.decimals().await; + match res { + Ok(response) => { + println!("{:?}", response); + assert_eq!(response["jsonrpc"], "2.0"); + assert!(!response.as_object().unwrap().contains_key("error")); + assert_eq!(response["result"], "0x12") + }, + Err(e) => panic!("Error: {:?}", e), + } + + Ok(()) +} + +#[tokio::test] +async fn test_total_supply() -> Result<(), ()> { + let icon_service = icon_service::IconService::new(Some("https://lisbon.net.solidwallet.io/api/v3".to_string())); + let irc2 = irc2::IRC2::new(icon_service, "cx273548dff8bb77ffaac5a342c4c04aeae0bc48fa".to_string()); + let res = irc2.total_supply().await; + match res { + Ok(response) => { + println!("{:?}", response); + assert_eq!(response["jsonrpc"], "2.0"); + assert!(!response.as_object().unwrap().contains_key("error")); + assert_eq!(response["result"], "0x3635c9adc5dea00000") + }, + Err(e) => panic!("Error: {:?}", e), + } + + Ok(()) +} + +#[tokio::test] +async fn test_balance_of() -> Result<(), ()> { + let icon_service = icon_service::IconService::new(Some("https://lisbon.net.solidwallet.io/api/v3".to_string())); + let irc2 = irc2::IRC2::new(icon_service, "cx273548dff8bb77ffaac5a342c4c04aeae0bc48fa".to_string()); + let res = irc2.balance_of("hx8dc6ae3d93e60a2dddf80bfc5fb1cd16a2bf6160".to_string()).await; + match res { + Ok(response) => { + println!("{:?}", response); + assert_eq!(response["jsonrpc"], "2.0"); + assert!(!response.as_object().unwrap().contains_key("error")); + assert!(response.as_object().unwrap().contains_key("result")); + }, + Err(e) => panic!("Error: {:?}", e), + } + + Ok(()) +} + +#[tokio::test] +async fn test_transfer() -> Result<(), ()> { + let wallet = icon_sdk::wallet::Wallet::new(Some("3468ea815d8896ef4552f10768caf2660689b965975c3ec2c1f5fe84bc3a77a5".to_string())); + let icon_service = icon_service::IconService::new(Some("https://lisbon.net.solidwallet.io/api/v3".to_string())); + let irc2 = irc2::IRC2::new(icon_service, "cx273548dff8bb77ffaac5a342c4c04aeae0bc48fa".to_string()); + let res = irc2.transfer( + wallet, + "hx8dc6ae3d93e60a2dddf80bfc5fb1cd16a2bf6160", + "12.317", + "0x3", + "0x2", + "0x1", + "0x186a00" + ).await; + + match res { + Ok(response) => { + println!("{:?}", response); + assert_eq!(response["jsonrpc"], "2.0"); + assert!(!response.as_object().unwrap().contains_key("error")); + assert!(response.as_object().unwrap().contains_key("result")); + }, + Err(e) => panic!("Error: {:?}", e), + } + + Ok(()) +} diff --git a/tests/test_wallet.rs b/tests/test_wallet.rs new file mode 100644 index 0000000..70da837 --- /dev/null +++ b/tests/test_wallet.rs @@ -0,0 +1,9 @@ +use icon_sdk::wallet::Wallet; + +#[tokio::test] +async fn test_wallet() -> Result<(), ()> { + let wallet = Wallet::new(Some("f4ade1ff528c9e0bf10d35909e3486ef6ce88df8a183fc1cc2c65bfa9a53d3fd".to_string())); + assert_eq!(wallet.get_public_address(), "hxb14e0c751899676a1a4e655a34063b42260f844b"); + + Ok(()) +}