diff --git a/scripts/tests.e2e.sh b/scripts/tests.e2e.sh index 5c575c1b..73c61f37 100755 --- a/scripts/tests.e2e.sh +++ b/scripts/tests.e2e.sh @@ -1,17 +1,17 @@ #!/usr/bin/env bash set -e -# build spacesvm-rs binary +# build spacesvm binary # ./scripts/build.release.sh # # download from github, keep network running -# VM_PLUGIN_PATH=$(pwd)/target/release/spacesvm-rs ./scripts/tests.e2e.sh +# VM_PLUGIN_PATH=$(pwd)/target/release/spacesvm ./scripts/tests.e2e.sh # # download from github, shut down network -# NETWORK_RUNNER_ENABLE_SHUTDOWN=1 VM_PLUGIN_PATH=$(pwd)/target/release/spacesvm-rs ./scripts/tests.e2e.sh +# NETWORK_RUNNER_ENABLE_SHUTDOWN=1 VM_PLUGIN_PATH=$(pwd)/target/release/spacesvm ./scripts/tests.e2e.sh # # use custom avalanchego binary -# VM_PLUGIN_PATH=$(pwd)/target/release/timestampvm ./scripts/tests.e2e.sh ~/go/src/github.com/ava-labs/avalanchego/build/avalanchego +# VM_PLUGIN_PATH=$(pwd)/target/release/spacesvm ./scripts/tests.e2e.sh ~/go/src/github.com/ava-labs/avalanchego/build/avalanchego # if ! [[ "$0" =~ scripts/tests.e2e.sh ]]; then echo "must be run from repository root" diff --git a/spaces-cli/Cargo.toml b/spaces-cli/Cargo.toml index 8b0dbe03..688f1837 100644 --- a/spaces-cli/Cargo.toml +++ b/spaces-cli/Cargo.toml @@ -17,7 +17,7 @@ clap = { version = "4.0", features = ["derive"] } hex = "0.4.3" jsonrpc-core = "18.0.0" log = "0.4.17" -serde = { version = "1.0.147", features = ["derive"] } -serde_json = "1.0.87" +serde = { version = "1.0.148", features = ["derive"] } +serde_json = "1.0.89" spacesvm = { path = "../spacesvm" } -tokio = { version = "1.22.0", features = ["full"] } +tokio = { version = "1.22.0", features = [] } diff --git a/spaces-cli/src/bin/spaces-cli/main.rs b/spaces-cli/src/bin/spaces-cli/main.rs index d1736293..58f68856 100644 --- a/spaces-cli/src/bin/spaces-cli/main.rs +++ b/spaces-cli/src/bin/spaces-cli/main.rs @@ -3,11 +3,8 @@ use std::error; use clap::{Parser, Subcommand}; use jsonrpc_core::futures; use spacesvm::{ - api::{ - client::{claim_tx, delete_tx, get_or_create_pk, set_tx, Client, Uri}, - DecodeTxArgs, IssueTxArgs, ResolveArgs, - }, - chain::tx::{decoder, unsigned::TransactionData}, + api::client::{claim_tx, delete_tx, get_or_create_pk, set_tx, Client, Uri}, + chain::tx::unsigned::TransactionData, }; #[derive(Subcommand, Debug)] @@ -46,21 +43,20 @@ struct Cli { #[command(subcommand)] command: Command, } - #[tokio::main] async fn main() -> Result<(), Box> { let cli = Cli::parse(); - let secret_key = get_or_create_pk(&cli.private_key_file)?; + let private_key = get_or_create_pk(&cli.private_key_file)?; let uri = cli.endpoint.parse::()?; - let mut client = Client::new(uri); + let client = Client::new(uri); + client.set_private_key(private_key).await; if let Command::Get { space, key } = &cli.command { - let resp = futures::executor::block_on(client.resolve(ResolveArgs { - space: space.as_bytes().to_vec(), - key: key.as_bytes().to_vec(), - })) - .map_err(|e| e.to_string())?; + let resp = client + .resolve(space, key) + .await + .map_err(|e| e.to_string())?; log::debug!("resolve response: {:?}", resp); println!("{}", serde_json::to_string(&resp)?); @@ -68,7 +64,7 @@ async fn main() -> Result<(), Box> { } if let Command::Ping {} = &cli.command { - let resp = futures::executor::block_on(client.ping()).map_err(|e| e.to_string())?; + let resp = client.ping().await.map_err(|e| e.to_string())?; println!("{}", serde_json::to_string(&resp)?); return Ok(()); @@ -76,21 +72,15 @@ async fn main() -> Result<(), Box> { // decode tx let tx_data = command_to_tx(cli.command)?; - let resp = futures::executor::block_on(client.decode_tx(DecodeTxArgs { tx_data })) - .map_err(|e| e.to_string())?; + let resp = futures::executor::block_on(client.decode_tx(tx_data)).map_err(|e| e.to_string())?; let typed_data = &resp.typed_data; - // create signature - let dh = decoder::hash_structured_data(typed_data)?; - let sig = secret_key.sign_digest(&dh.as_bytes())?; - // issue tx - let resp = futures::executor::block_on(client.issue_tx(IssueTxArgs { - typed_data: resp.typed_data, - signature: sig.to_bytes().to_vec(), - })) - .map_err(|e| e.to_string())?; + let resp = client + .issue_tx(typed_data) + .await + .map_err(|e| e.to_string())?; println!("{}", serde_json::to_string(&resp)?); Ok(()) @@ -99,9 +89,9 @@ async fn main() -> Result<(), Box> { /// Takes a TX command and returns transaction data. fn command_to_tx(command: Command) -> std::io::Result { match command { - Command::Claim { space } => Ok(claim_tx(space)), - Command::Set { space, key, value } => Ok(set_tx(space, key, value.as_bytes().to_vec())), - Command::Delete { space, key } => Ok(delete_tx(space, key)), + Command::Claim { space } => Ok(claim_tx(&space)), + Command::Set { space, key, value } => Ok(set_tx(&space, &key, &value)), + Command::Delete { space, key } => Ok(delete_tx(&space, &key)), _ => Err(std::io::Error::new( std::io::ErrorKind::Other, "not a supported tx", diff --git a/spacesvm/Cargo.toml b/spacesvm/Cargo.toml index 71dcfb7e..a2c4eee7 100644 --- a/spacesvm/Cargo.toml +++ b/spacesvm/Cargo.toml @@ -15,16 +15,16 @@ name = "spacesvm" path = "src/bin/spaces/main.rs" [dependencies] -avalanche-types = { version = "0.0.140", features = ["subnet"] } +avalanche-types = { version = "0.0.144", features = ["subnet"] } byteorder = "1.4.3" -chrono = "0.4.22" +chrono = "0.4.23" crossbeam-channel = "0.5.6" derivative = "2.2.0" dyn-clone = "1.0.9" ethereum-types = { version = "0.14.0" } -clap = { version = "4.0.22", features = ["cargo", "derive"] } +clap = { version = "4.0.27", features = ["cargo", "derive"] } eip-712 = "0.1.0" -env_logger = "0.9.3" +env_logger = "0.10.0" hex = "0.4.3" http = "0.2.8" hyper = "0.14.23" @@ -32,15 +32,15 @@ jsonrpc-core = "18.0.0" jsonrpc-core-client = { version = "18.0.0" } jsonrpc-derive = "18.0" log = "0.4.17" -lru = "0.8.0" +lru = "0.8.1" prost = "0.11.2" ripemd = "0.1.3" semver = "1.0.14" -serde = { version = "1.0.147", features = ["derive"] } -serde_json = "1.0.87" +serde = { version = "1.0.148", features = ["derive"] } +serde_json = "1.0.89" serde_yaml = "0.9.14" sha3 = "0.10.6" -tokio = { version = "1.21.2", features = ["fs", "rt-multi-thread"] } +tokio = { version = "1.22.0", features = ["fs", "rt-multi-thread"] } tokio-stream = { version = "0.1.11", features = ["net"] } tonic = { version = "0.8.2", features = ["gzip"] } tonic-health = "0.7" @@ -48,7 +48,7 @@ typetag = "0.2" [dev-dependencies] jsonrpc-tcp-server = "18.0.0" -futures-test = "0.3.24" +futures-test = "0.3.25" [[test]] name = "integration" diff --git a/spacesvm/src/api/client.rs b/spacesvm/src/api/client.rs index 9e85e02d..d4c402ee 100644 --- a/spacesvm/src/api/client.rs +++ b/spacesvm/src/api/client.rs @@ -1,7 +1,8 @@ use std::{ fs::File, - io::{Result, Write}, + io::{Error, ErrorKind, Result, Write}, path::Path, + sync::Arc, }; use crate::{ @@ -9,85 +10,142 @@ use crate::{ DecodeTxArgs, DecodeTxResponse, IssueTxArgs, IssueTxResponse, PingResponse, ResolveArgs, ResolveResponse, }, - chain::tx::{tx::TransactionType, unsigned::TransactionData}, + chain::tx::{ + decoder::{self, TypedData}, + tx::TransactionType, + unsigned::TransactionData, + }, +}; +use avalanche_types::key::{ + self, + secp256k1::{private_key::Key, signature::Sig}, }; -use avalanche_types::key; use http::{Method, Request}; use hyper::{body, client::HttpConnector, Body, Client as HyperClient}; -use jsonrpc_core::{Call, Id, MethodCall, Params, Version}; +use jsonrpc_core::{Call, Id, MethodCall, Params, Value, Version}; use serde::de; pub use http::Uri; +use tokio::sync::RwLock; -/// HTTP client for interacting with the API, assumes single threaded use. +/// Thread safe HTTP client for interacting with the API. pub struct Client { + inner: Arc>>, +} + +pub struct ClientInner { id: u64, client: HyperClient, - pub uri: Uri, + endpoint: Uri, + private_key: Option, } impl Client { - pub fn new(uri: Uri) -> Self { + pub fn new(endpoint: Uri) -> Self { let client = HyperClient::new(); - Self { id: 0, client, uri } + Self { + inner: Arc::new(RwLock::new(ClientInner { + id: 0, + client, + endpoint, + private_key: None, + })), + } } } impl Client { - fn next_id(&mut self) -> Id { - let id = self.id; - self.id = id + 1; + async fn next_id(&self) -> Id { + let mut client = self.inner.write().await; + let id = client.id; + client.id = id + 1; Id::Num(id) } + pub async fn set_endpoint(&self, endpoint: Uri) { + let mut inner = self.inner.write().await; + inner.endpoint = endpoint; + } + + pub async fn set_private_key(&self, private_key: Key) { + let mut inner = self.inner.write().await; + inner.private_key = Some(private_key); + } + /// Returns a serialized json request as string and the request id. - pub fn raw_request(&mut self, method: &str, params: &Params) -> (Id, String) { - let id = self.next_id(); + pub async fn raw_request(&self, method: &str, params: &Params) -> Result<(Id, String)> { + let id = self.next_id().await; let request = jsonrpc_core::Request::Single(Call::MethodCall(MethodCall { jsonrpc: Some(Version::V2), method: method.to_owned(), params: params.to_owned(), id: id.clone(), })); - ( - id, - serde_json::to_string(&request).expect("jsonrpc request should be serializable"), - ) + let request = serde_json::to_string(&request).map_err(|e| { + Error::new( + ErrorKind::Other, + format!("jsonrpc request should be serializable: {}", e), + ) + })?; + + Ok((id, request)) + } + + /// Returns a recoverable signature from 32 byte SHA256 message. + pub async fn sign_digest(&self, dh: &[u8]) -> Result { + let inner = self.inner.read().await; + if let Some(pk) = &inner.private_key { + return pk.sign_digest(dh); + } + Err(Error::new(ErrorKind::Other, "private key not set")) } /// Returns a PingResponse from client request. - pub async fn ping(&mut self) -> Result { - let (_id, json_request) = self.raw_request("ping", &Params::None); + pub async fn ping(&self) -> Result { + let (_id, json_request) = self.raw_request("ping", &Params::None).await?; let resp = self.post_de::(&json_request).await?; Ok(resp) } /// Returns a DecodeTxResponse from client request. - pub async fn decode_tx(&mut self, args: DecodeTxArgs) -> Result { - let arg_bytes = serde_json::to_vec(&args)?; - let params: Params = serde_json::from_slice(&arg_bytes)?; - let (_id, json_request) = self.raw_request("decodeTx", ¶ms); + pub async fn decode_tx(&self, tx_data: TransactionData) -> Result { + let arg_value = serde_json::to_value(&DecodeTxArgs { tx_data })?; + let (_id, json_request) = self + .raw_request("decodeTx", &Params::Array(vec![arg_value])) + .await?; let resp = self.post_de::(&json_request).await?; Ok(resp) } /// Returns a IssueTxResponse from client request. - pub async fn issue_tx(&mut self, args: IssueTxArgs) -> Result { - let arg_bytes = serde_json::to_vec(&args)?; - let params: Params = serde_json::from_slice(&arg_bytes)?; - let (_id, json_request) = self.raw_request("issueTx", ¶ms); + pub async fn issue_tx(&self, typed_data: &TypedData) -> Result { + let dh = decoder::hash_structured_data(typed_data)?; + let sig = self.sign_digest(&dh.as_bytes()).await?.to_bytes().to_vec(); + log::debug!("signature: {:?}", sig); + + let arg_value = serde_json::to_value(&IssueTxArgs { + typed_data: typed_data.to_owned(), + signature: sig, + })?; + let (_id, json_request) = self + .raw_request("issueTx", &Params::Array(vec![arg_value])) + .await?; let resp = self.post_de::(&json_request).await?; Ok(resp) } /// Returns a ResolveResponse from client request. - pub async fn resolve(&mut self, args: ResolveArgs) -> Result { - let arg_bytes = serde_json::to_vec(&args)?; - let params: Params = serde_json::from_slice(&arg_bytes)?; - let (_id, json_request) = self.raw_request("resolve", ¶ms); + pub async fn resolve(&self, space: &str, key: &str) -> Result { + let arg_value = serde_json::to_value(&ResolveArgs { + space: space.as_bytes().to_vec(), + key: key.as_bytes().to_vec(), + })?; + let (_id, json_request) = self + .raw_request("resolve", &Params::Array(vec![arg_value])) + .await?; let resp = self.post_de::(&json_request).await?; Ok(resp) @@ -95,9 +153,11 @@ impl Client { /// Returns a deserialized response from client request. pub async fn post_de(&self, json: &str) -> Result { + let inner = self.inner.read().await; + let req = Request::builder() .method(Method::POST) - .uri(self.uri.to_string()) + .uri(inner.endpoint.to_string()) .header("content-type", "application/json-rpc") .body(Body::from(json.to_owned())) .map_err(|e| { @@ -107,20 +167,30 @@ impl Client { ) })?; - let resp = self.client.request(req).await.map_err(|e| { + let mut resp = inner.client.request(req).await.map_err(|e| { std::io::Error::new( std::io::ErrorKind::Other, format!("client post request failed: {}", e), ) })?; - let bytes = body::to_bytes(resp.into_body()) + let bytes = body::to_bytes(resp.body_mut()) .await .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; - let resp = serde_json::from_slice(&bytes).map_err(|e| { + + // deserialize bytes to value + let v: Value = serde_json::from_slice(&bytes).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("failed to deserialize response to value: {}", e), + ) + })?; + + // deserialize result to T + let resp = serde_json::from_value(v["result"].to_owned()).map_err(|e| { std::io::Error::new( std::io::ErrorKind::Other, - format!("failed to create client request: {}", e), + format!("failed to deserialize response: {}", e), ) })?; @@ -128,29 +198,29 @@ impl Client { } } -pub fn claim_tx(space: String) -> TransactionData { +pub fn claim_tx(space: &str) -> TransactionData { TransactionData { typ: TransactionType::Claim, - space, + space: space.to_owned(), key: String::new(), value: vec![], } } -pub fn set_tx(space: String, key: String, value: Vec) -> TransactionData { +pub fn set_tx(space: &str, key: &str, value: &str) -> TransactionData { TransactionData { typ: TransactionType::Set, - space, - key, - value, + space: space.to_owned(), + key: key.to_owned(), + value: value.as_bytes().to_vec(), } } -pub fn delete_tx(space: String, key: String) -> TransactionData { +pub fn delete_tx(space: &str, key: &str) -> TransactionData { TransactionData { typ: TransactionType::Delete, - space, - key, + space: space.to_owned(), + key: key.to_owned(), value: vec![], } } @@ -174,10 +244,10 @@ pub fn get_or_create_pk(path: &str) -> Result #[tokio::test] async fn test_raw_request() { - let mut cli = Client::new(Uri::from_static("http://test.url")); - let (id, _) = cli.raw_request("ping", &Params::None); + let cli = Client::new(Uri::from_static("http://test.url")); + let (id, _) = cli.raw_request("ping", &Params::None).await.unwrap(); assert_eq!(id, jsonrpc_core::Id::Num(0)); - let (id, req) = cli.raw_request("ping", &Params::None); + let (id, req) = cli.raw_request("ping", &Params::None).await.unwrap(); assert_eq!(id, jsonrpc_core::Id::Num(1)); assert_eq!( req, diff --git a/spacesvm/src/bin/spaces/main.rs b/spacesvm/src/bin/spaces/main.rs index b784c814..125d4741 100644 --- a/spacesvm/src/bin/spaces/main.rs +++ b/spacesvm/src/bin/spaces/main.rs @@ -45,7 +45,7 @@ async fn main() -> Result<()> { ) = tokio::sync::broadcast::channel(1); info!("starting spacesvm-rs"); - let vm_server = subnet::rpc::vm::server::Server::new(Box::new(vm::ChainVm::new()), stop_ch_tx); + let vm_server = subnet::rpc::vm::server::Server::new(vm::ChainVm::new(), stop_ch_tx); subnet::rpc::plugin::serve(vm_server, stop_ch_rx) .await diff --git a/spacesvm/tests/vm/mod.rs b/spacesvm/tests/vm/mod.rs index 4b7a378b..ecbc201d 100644 --- a/spacesvm/tests/vm/mod.rs +++ b/spacesvm/tests/vm/mod.rs @@ -26,10 +26,8 @@ async fn test_api() { // setup stop channel for grpc services. let (stop_ch_tx, stop_ch_rx): (Sender<()>, Receiver<()>) = tokio::sync::broadcast::channel(1); - let vm_server = avalanche_types::subnet::rpc::vm::server::Server::new( - Box::new(vm::ChainVm::new()), - stop_ch_tx, - ); + let vm_server = + avalanche_types::subnet::rpc::vm::server::Server::new(vm::ChainVm::new(), stop_ch_tx); // start Vm service let vm_addr = utils::new_socket_addr(); @@ -112,10 +110,13 @@ async fn test_api() { .await .unwrap(); - let mut client = spacesvm::api::client::Client::new(http::Uri::from_static("http://test.url")); + let client = spacesvm::api::client::Client::new(http::Uri::from_static("http://test.url")); // ping - let (_id, json_str) = client.raw_request("ping", &Params::None); + let (_id, json_str) = client + .raw_request("ping", &Params::None) + .await + .expect("raw_request success"); let req = http::request::Builder::new() .body(json_str.as_bytes().to_vec()) .unwrap(); @@ -138,10 +139,14 @@ async fn test_api() { let body = std::str::from_utf8(&resp.body()).unwrap(); log::info!("ping response {}", body); - let tx_data = claim_tx("test_claim".to_owned()); - let arg_bytes = serde_json::to_value(&DecodeTxArgs { tx_data }).unwrap(); + let tx_data = claim_tx("test_claim"); + let arg_value = serde_json::to_value(&DecodeTxArgs { tx_data }).unwrap(); - let (_id, json_str) = client.raw_request("decodeTx", &Params::Array(vec![arg_bytes])); + let (_id, json_str) = client + .raw_request("decodeTx", &Params::Array(vec![arg_value])) + .await + .expect("raw_request success"); + log::info!("decodeTx request: {}", json_str); let req = http::request::Builder::new() .body(json_str.as_bytes().to_vec()) .unwrap(); diff --git a/tests/e2e/Cargo.toml b/tests/e2e/Cargo.toml index bbce91f8..1ddf9cfb 100644 --- a/tests/e2e/Cargo.toml +++ b/tests/e2e/Cargo.toml @@ -13,11 +13,11 @@ homepage = "https://avax.network" [dev-dependencies] avalanche-installer = "0.0.8" avalanche-network-runner-sdk = "0.3.0" # https://crates.io/crates/avalanche-network-runner-sdk -avalanche-types = { version = "0.0.135", features = ["client", "subnet"] } # https://crates.io/crates/avalanche-types -env_logger = "0.9.1" +avalanche-types = { version = "0.0.144", features = ["client", "subnet"] } # https://crates.io/crates/avalanche-types +env_logger = "0.10.0" log = "0.4.17" random-manager = "0.0.1" serde_json = "1.0.87" # https://github.com/serde-rs/json/releases tempfile = "3.3.0" spacesvm = { path = "../../spacesvm" } -tokio = { version = "1.21.2", features = [] } # https://github.com/tokio-rs/tokio/releases \ No newline at end of file +tokio = { version = "1.22.0", features = [] } # https://github.com/tokio-rs/tokio/releases \ No newline at end of file diff --git a/tests/e2e/src/tests/mod.rs b/tests/e2e/src/tests/mod.rs index d85f549e..403d6ff3 100644 --- a/tests/e2e/src/tests/mod.rs +++ b/tests/e2e/src/tests/mod.rs @@ -1,13 +1,17 @@ use std::{ fs, path::Path, + str::FromStr, thread, time::{Duration, Instant}, }; use avalanche_network_runner_sdk::{BlockchainSpec, Client, GlobalConfig, StartRequest}; -use avalanche_types::subnet; -use spacesvm; +use avalanche_types::{ids, subnet}; +use spacesvm::{ + self, + api::client::{claim_tx, get_or_create_pk, set_tx, Uri}, +}; #[tokio::test] async fn e2e() { @@ -30,7 +34,7 @@ async fn e2e() { assert!(exists); assert!(Path::new(&vm_plugin_path).exists()); - let vm_id = subnet::vm_name_to_id("minikvvm").unwrap(); + let vm_id = subnet::vm_name_to_id("spacesvm").unwrap(); let (mut avalanchego_exec_path, _) = crate::get_avalanchego_path(); let plugins_dir = if !avalanchego_exec_path.is_empty() { @@ -47,7 +51,7 @@ async fn e2e() { // keep this in sync with "proto" crate // ref. https://github.com/ava-labs/avalanchego/blob/v1.9.2/version/constants.go#L15-L17 let (exec_path, plugins_dir) = - avalanche_installer::avalanchego::download(None, None, Some("v1.9.2".to_string())) + avalanche_installer::avalanchego::download(None, None, Some("v1.9.3".to_string())) .await .unwrap(); avalanchego_exec_path = exec_path; @@ -92,7 +96,7 @@ async fn e2e() { .unwrap(), ), blockchain_specs: vec![BlockchainSpec { - vm_name: String::from("minikvvm"), + vm_name: String::from("spacesvm"), genesis: genesis_file_path.to_string(), ..Default::default() }], @@ -177,6 +181,69 @@ async fn e2e() { log::info!("{}: {}", node_name, iv.uri); rpc_eps.push(iv.uri.clone()); } + let mut blockchain_id = ids::Id::empty(); + for (k, v) in cluster_info.custom_chains.iter() { + log::info!("custom chain info: {}={:?}", k, v); + if v.chain_name == "spacesvm" { + blockchain_id = ids::Id::from_str(&v.chain_id).unwrap(); + break; + } + } + + log::info!("avalanchego RPC endpoints: {:?}", rpc_eps); + + let private_key = get_or_create_pk("/tmp/.spacesvm-cli-pk").expect("generate new private key"); + let chain_url = format!("{}/ext/bc/{}/public", rpc_eps[0], blockchain_id); + let scli = + spacesvm::api::client::Client::new(chain_url.parse::().expect("valid endpoint")); + scli.set_private_key(private_key).await; + for ep in rpc_eps.iter() { + let chain_url = format!("{}/ext/bc/{}/public", ep, blockchain_id) + .parse::() + .expect("valid endpoint"); + scli.set_endpoint(chain_url).await; + let resp = scli.ping().await.unwrap(); + log::info!("ping response from {}: {:?}", ep, resp); + assert!(resp.success); + + thread::sleep(Duration::from_millis(300)); + } + + scli.set_endpoint(chain_url.parse::().expect("valid endpoint")) + .await; + + log::info!("decode claim tx request..."); + let resp = scli + .decode_tx(claim_tx("test")) + .await + .expect("decodeTx success"); + log::info!("decode claim tx response from {}: {:?}", chain_url, resp); + + log::info!("issue claim tx request..."); + let resp = scli + .issue_tx(&resp.typed_data) + .await + .expect("issue_tx success"); + log::info!("issue claim tx response from {}: {:?}", chain_url, resp); + + log::info!("decode set tx request..."); + let resp = scli + .decode_tx(set_tx("test", "foo", "bar")) + .await + .expect("decodeTx success"); + log::info!("decode set tx response from {}: {:?}", chain_url, resp); + + log::info!("issue set tx request..."); + let resp = scli + .issue_tx(&resp.typed_data) + .await + .expect("issue_tx success"); + log::info!("issue set tx response from {}: {:?}", chain_url, resp); + + log::info!("issue resolve request..."); + let resp = scli.resolve("test", "foo").await.expect("resolve success"); + log::info!("resolve response from {}: {:?}", chain_url, resp); + assert_eq!(std::str::from_utf8(&resp.value).unwrap(), "bar"); if crate::get_network_runner_enable_shutdown() { log::info!("shutdown is enabled... stopping...");