From 1202c4136f5eb01aa58a1f4dd76a57e7ee509781 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:12:41 +0200 Subject: [PATCH] test(dapi): dapi-cli example in dapi-grpc --- Cargo.lock | 111 ++++--- packages/dapi-grpc/Cargo.toml | 27 +- .../examples/dapi-cli/core/block_hash.rs | 58 ++++ .../examples/dapi-cli/core/chainlocks.rs | 83 +++++ .../examples/dapi-cli/core/masternode.rs | 158 +++++++++ .../dapi-cli/core/masternode_status.rs | 65 ++++ .../dapi-grpc/examples/dapi-cli/core/mod.rs | 33 ++ .../examples/dapi-cli/core/transactions.rs | 126 +++++++ packages/dapi-grpc/examples/dapi-cli/error.rs | 43 +++ packages/dapi-grpc/examples/dapi-cli/main.rs | 62 ++++ .../examples/dapi-cli/platform/identity.rs | 111 +++++++ .../examples/dapi-cli/platform/mod.rs | 37 +++ .../examples/dapi-cli/platform/protocol.rs | 313 ++++++++++++++++++ .../dapi-cli/platform/state_transition/mod.rs | 21 ++ .../platform/state_transition/monitor.rs | 117 +++++++ .../platform/state_transition/workflow.rs | 108 ++++++ 16 files changed, 1416 insertions(+), 57 deletions(-) create mode 100644 packages/dapi-grpc/examples/dapi-cli/core/block_hash.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/core/chainlocks.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/core/masternode.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/core/masternode_status.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/core/mod.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/core/transactions.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/error.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/main.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/platform/identity.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/platform/mod.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/platform/protocol.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/platform/state_transition/mod.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/platform/state_transition/monitor.rs create mode 100644 packages/dapi-grpc/examples/dapi-cli/platform/state_transition/workflow.rs diff --git a/Cargo.lock b/Cargo.lock index b55b5c7500c..84830a16782 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -659,7 +659,7 @@ dependencies = [ "sha2", "sha3", "subtle", - "thiserror 2.0.16", + "thiserror 2.0.17", "uint-zigzag", "vsss-rs", "zeroize", @@ -973,9 +973,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" dependencies = [ "clap_builder", "clap_derive", @@ -983,9 +983,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.47" +version = "4.5.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" dependencies = [ "anstream", "anstyle", @@ -1316,18 +1316,27 @@ dependencies = [ name = "dapi-grpc" version = "2.1.0-dev.8" dependencies = [ + "ciborium", + "clap", "dapi-grpc-macros", + "dashcore", "futures-core", "getrandom 0.2.16", + "hex", "platform-version", "prost 0.14.1", "serde", "serde_bytes", "serde_json", + "sha2", "tenderdash-proto", + "thiserror 2.0.17", + "tokio", "tonic 0.14.2", "tonic-prost", "tonic-prost-build", + "tracing", + "tracing-subscriber", ] [[package]] @@ -1449,7 +1458,7 @@ dependencies = [ "serde", "serde_json", "test-case", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-test", "tokio-util", @@ -1532,7 +1541,7 @@ dependencies = [ "rustversion", "secp256k1", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1587,7 +1596,7 @@ dependencies = [ "platform-value", "platform-version", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1602,7 +1611,7 @@ dependencies = [ "platform-value", "platform-version", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "token-history-contract", "wallet-utils-contract", "withdrawals-contract", @@ -1744,7 +1753,7 @@ dependencies = [ "platform-value", "platform-version", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1798,7 +1807,7 @@ dependencies = [ "serde_repr", "sha2", "strum 0.26.3", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1840,7 +1849,7 @@ dependencies = [ "serde_json", "sqlparser", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] @@ -1915,7 +1924,7 @@ dependencies = [ "serde", "serde_json", "tenderdash-abci", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] @@ -2159,7 +2168,7 @@ dependencies = [ "platform-value", "platform-version", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2495,7 +2504,7 @@ dependencies = [ "reqwest", "sha2", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-util", "tower-http", @@ -2509,7 +2518,7 @@ source = "git+https://github.com/dashpay/grovedb?rev=1ecedf530fbc5b5e12edf1bc607 dependencies = [ "integer-encoding", "intmap", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2521,7 +2530,7 @@ dependencies = [ "hex", "integer-encoding", "intmap", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2545,7 +2554,7 @@ dependencies = [ "integer-encoding", "num_cpus", "rand 0.8.5", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2572,7 +2581,7 @@ dependencies = [ "rocksdb 0.24.0", "strum 0.27.2", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -2580,7 +2589,7 @@ name = "grovedb-version" version = "3.0.0" source = "git+https://github.com/dashpay/grovedb?rev=1ecedf530fbc5b5e12edf1bc607bd288c187ddde#1ecedf530fbc5b5e12edf1bc607bd288c187ddde" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", "versioned-feature-core 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -2764,7 +2773,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -2787,7 +2796,7 @@ dependencies = [ "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -3329,7 +3338,7 @@ dependencies = [ "json-schema-compatibility-validator", "once_cell", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3445,7 +3454,7 @@ dependencies = [ "platform-value", "platform-version", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3596,7 +3605,7 @@ dependencies = [ "platform-value", "platform-version", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -4334,7 +4343,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "treediff", ] @@ -4353,7 +4362,7 @@ dependencies = [ "bincode 2.0.0-rc.3", "grovedb-version", "once_cell", - "thiserror 2.0.16", + "thiserror 2.0.17", "versioned-feature-core 1.0.0 (git+https://github.com/dashpay/versioned-feature-core)", ] @@ -4675,7 +4684,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2 0.6.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -4696,7 +4705,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -5065,7 +5074,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tonic-web-wasm-client", "tower-service", @@ -5096,7 +5105,7 @@ dependencies = [ "serde", "serde_json", "simple-signer", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "zeroize", @@ -5116,7 +5125,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-test", "tracing", @@ -5460,9 +5469,9 @@ checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -5510,18 +5519,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -6032,7 +6041,7 @@ dependencies = [ "lhash", "semver", "tenderdash-proto", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-util", "tracing", @@ -6055,7 +6064,7 @@ dependencies = [ "serde", "subtle-encoding", "tenderdash-proto-compiler", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "tonic 0.14.2", "tonic-prost", @@ -6135,11 +6144,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -6155,9 +6164,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -6254,7 +6263,7 @@ dependencies = [ "platform-value", "platform-version", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -6545,7 +6554,7 @@ dependencies = [ "httparse", "js-sys", "pin-project", - "thiserror 2.0.16", + "thiserror 2.0.17", "tonic 0.14.2", "tower-service", "wasm-bindgen", @@ -6928,7 +6937,7 @@ dependencies = [ "platform-value", "platform-version", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -7076,7 +7085,7 @@ dependencies = [ "serde", "serde-wasm-bindgen 0.5.0", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "wasm-bindgen", "wasm-bindgen-futures", "wasm-logger", @@ -7144,7 +7153,7 @@ dependencies = [ "serde_json", "sha2", "simple-signer", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "tracing-subscriber", "tracing-wasm", @@ -7692,7 +7701,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] diff --git a/packages/dapi-grpc/Cargo.toml b/packages/dapi-grpc/Cargo.toml index a7bc50ae828..130c9ac217a 100644 --- a/packages/dapi-grpc/Cargo.toml +++ b/packages/dapi-grpc/Cargo.toml @@ -78,10 +78,25 @@ path = "clients/core/v0/rust/core_example.rs" name = "platform_example" path = "clients/platform/v0/rust/platform_example.rs" +[[example]] +name = "dapi-cli" +path = "examples/dapi-cli/main.rs" + +[dev-dependencies] +# dapi-cli example dependencies +dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", features = [ + "serde", +] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = { version = "1.0.145" } +sha2 = { version = "0.10.9" } +hex = { version = "0.4.3" } +tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] } +ciborium = { version = "0.2.2" } +thiserror = { version = "2.0.17" } +clap = { version = "4.5.48", features = ["derive"] } +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } + [package.metadata.cargo-machete] -ignored = [ - "platform-version", - "serde_bytes", - "futures-core", - "dapi-grpc-macros", -] +ignored = ["platform-version", "futures-core", "getrandom", "tonic-prost-build"] diff --git a/packages/dapi-grpc/examples/dapi-cli/core/block_hash.rs b/packages/dapi-grpc/examples/dapi-cli/core/block_hash.rs new file mode 100644 index 00000000000..a5f1171d26b --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/core/block_hash.rs @@ -0,0 +1,58 @@ +use clap::Args; +use dapi_grpc::core::v0::{core_client::CoreClient, GetBlockRequest}; +use dapi_grpc::tonic::transport::Channel; +use tracing::info; + +use crate::error::{CliError, CliResult}; + +#[derive(Args, Debug)] +pub struct BlockHashCommand { + /// Block height to query (>= 1) + #[arg(long)] + pub height: u32, +} + +pub async fn run(url: &str, cmd: BlockHashCommand) -> CliResult<()> { + if cmd.height < 1 { + return Err( + std::io::Error::new(std::io::ErrorKind::InvalidInput, "height must be >= 1").into(), + ); + } + + info!(url = %url, height = cmd.height, "Querying block hash"); + + let channel = Channel::from_shared(url.to_string()) + .map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })? + .connect() + .await?; + let mut client = CoreClient::new(channel); + + let request = GetBlockRequest { + block: Some(dapi_grpc::core::v0::get_block_request::Block::Height( + cmd.height, + )), + }; + + let response = client.get_block(request).await?; + let block_bytes = response.into_inner().block; + + // Deserialize and compute hash + use dashcore::consensus::encode::deserialize; + use dashcore::Block; + + let block: Block = match deserialize(&block_bytes) { + Ok(b) => b, + Err(e) => { + tracing::error!(block_bytes = hex::encode(&block_bytes), error = %e, "Failed to deserialize block"); + return Err(CliError::DashCoreEncoding(e)); + } + }; + let block_json = serde_json::to_string_pretty(&block)?; + let hash_hex = block.block_hash().to_string(); + + println!("Block {} hash: {}\n{}\n", cmd.height, hash_hex, block_json); + Ok(()) +} diff --git a/packages/dapi-grpc/examples/dapi-cli/core/chainlocks.rs b/packages/dapi-grpc/examples/dapi-cli/core/chainlocks.rs new file mode 100644 index 00000000000..e9f803d4379 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/core/chainlocks.rs @@ -0,0 +1,83 @@ +use clap::Args; +use dapi_grpc::core::v0::{ + block_headers_with_chain_locks_request::FromBlock, core_client::CoreClient, + BlockHeadersWithChainLocksRequest, +}; +use dapi_grpc::tonic::transport::Channel; +use tracing::{info, warn}; + +use crate::error::{CliError, CliResult}; + +#[derive(Args, Debug)] +pub struct ChainLocksCommand { + /// Optional starting block height for historical context + #[arg(long)] + pub from_height: Option, +} + +pub async fn run(url: &str, cmd: ChainLocksCommand) -> CliResult<()> { + info!(url = %url, "Connecting to DAPI Core gRPC for chain locks"); + + let channel = Channel::from_shared(url.to_string()) + .map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })? + .connect() + .await?; + let mut client = CoreClient::new(channel); + + let request = BlockHeadersWithChainLocksRequest { + count: 0, + from_block: cmd.from_height.map(FromBlock::FromBlockHeight), + }; + + println!("📡 Subscribing to chain locks at {}", url); + if let Some(height) = cmd.from_height { + println!( + " Requesting history starting from block height {}", + height + ); + } else { + println!(" Streaming live chain locks\n"); + } + + let response = client + .subscribe_to_block_headers_with_chain_locks(request) + .await?; + + let mut stream = response.into_inner(); + let mut block_header_batches = 0usize; + let mut chain_locks = 0usize; + + while let Some(message) = stream.message().await? { + use dapi_grpc::core::v0::block_headers_with_chain_locks_response::Responses; + + match message.responses { + Some(Responses::BlockHeaders(headers)) => { + block_header_batches += 1; + let header_count = headers.headers.len(); + let total_bytes: usize = headers.headers.iter().map(|h| h.len()).sum(); + println!( + "🧱 Received block headers batch #{} ({} header(s), {} bytes)", + block_header_batches, header_count, total_bytes + ); + } + Some(Responses::ChainLock(data)) => { + chain_locks += 1; + println!( + "🔒 Received chain lock #{}, payload size {} bytes", + chain_locks, + data.len() + ); + } + None => { + warn!("Received empty chain lock response message"); + } + } + println!(); + } + + println!("👋 Chain lock stream ended"); + Ok(()) +} diff --git a/packages/dapi-grpc/examples/dapi-cli/core/masternode.rs b/packages/dapi-grpc/examples/dapi-cli/core/masternode.rs new file mode 100644 index 00000000000..d197a01eff8 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/core/masternode.rs @@ -0,0 +1,158 @@ +use ciborium::de::from_reader; +use clap::Args; +use dapi_grpc::core::v0::{core_client::CoreClient, MasternodeListRequest}; +use dapi_grpc::tonic::transport::Channel; +use serde::Deserialize; +use serde_json::Value; +use std::io::Cursor; +use tracing::warn; + +use crate::error::{CliError, CliResult}; + +#[derive(Args, Debug)] +pub struct MasternodeCommand {} + +pub async fn run(url: &str, _cmd: MasternodeCommand) -> CliResult<()> { + let channel = Channel::from_shared(url.to_string()) + .map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })? + .connect() + .await?; + + let mut client = CoreClient::new(channel); + + println!("📡 Subscribing to masternode list updates at {}", url); + + let response = client + .subscribe_to_masternode_list(MasternodeListRequest {}) + .await?; + + let mut stream = response.into_inner(); + let mut update_index = 0usize; + + while let Some(update) = stream.message().await? { + update_index += 1; + let diff_bytes = update.masternode_list_diff; + + println!("🔁 Masternode list update #{}", update_index); + println!(" Diff payload size: {} bytes", diff_bytes.len()); + + match from_reader::(Cursor::new(&diff_bytes)) { + Ok(diff) => print_diff_summary(&diff), + Err(err) => { + warn!(error = %err, "Failed to decode masternode diff payload"); + println!(" Unable to decode diff payload (see logs for details).\n"); + continue; + } + } + + println!(); + } + + println!("👋 Stream ended"); + Ok(()) +} + +fn print_diff_summary(diff: &MasternodeListDiff) { + let base_hash = diff.base_block_hash.as_deref().unwrap_or(""); + let block_hash = diff.block_hash.as_deref().unwrap_or(""); + + println!(" Base block hash : {}", base_hash); + println!(" Target block hash: {}", block_hash); + + let added = diff.added_mns.len(); + let updated = diff.updated_mns.len(); + let removed = diff.removed_mns.len(); + + if added > 0 || updated > 0 || removed > 0 { + println!( + " Added: {} | Updated: {} | Removed: {}", + added, updated, removed + ); + } + + let snapshot = if !diff.full_list.is_empty() { + diff.full_list.len() + } else if !diff.masternode_list.is_empty() { + diff.masternode_list.len() + } else { + 0 + }; + + if snapshot > 0 { + println!(" Snapshot size: {} masternodes", snapshot); + } + + if let Some(total) = diff.total_mn_count { + println!(" Reported total masternodes: {}", total); + } + + let quorum_updates = diff.quorum_diff_updates(); + if quorum_updates > 0 { + println!(" Quorum updates: {}", quorum_updates); + } + + if added == 0 && updated == 0 && removed == 0 && snapshot == 0 && quorum_updates == 0 { + println!( + " No masternode or quorum changes detected in this diff (metadata update only)." + ); + } +} + +#[derive(Debug, Deserialize)] +struct MasternodeListDiff { + #[serde(rename = "baseBlockHash")] + base_block_hash: Option, + #[serde(rename = "blockHash")] + block_hash: Option, + #[serde(rename = "addedMNs", default)] + added_mns: Vec, + #[serde(rename = "updatedMNs", default)] + updated_mns: Vec, + #[serde(rename = "removedMNs", default)] + removed_mns: Vec, + #[serde(rename = "mnList", default)] + full_list: Vec, + #[serde(rename = "masternodeList", default)] + masternode_list: Vec, + #[serde(rename = "totalMnCount")] + total_mn_count: Option, + #[serde(rename = "quorumDiffs", default)] + quorum_diffs: Vec, + #[serde(rename = "newQuorums", default)] + new_quorums: Vec, + #[serde(rename = "deletedQuorums", default)] + deleted_quorums: Vec, + #[serde(default)] + quorums: Vec, +} + +impl MasternodeListDiff { + fn quorum_diff_updates(&self) -> usize { + let nested: usize = self + .quorum_diffs + .iter() + .map(|entry| entry.quorum_updates()) + .sum(); + + nested + self.new_quorums.len() + self.deleted_quorums.len() + self.quorums.len() + } +} + +#[derive(Debug, Deserialize)] +struct QuorumDiffEntry { + #[serde(rename = "newQuorums", default)] + new_quorums: Vec, + #[serde(rename = "deletedQuorums", default)] + deleted_quorums: Vec, + #[serde(default)] + quorums: Vec, +} + +impl QuorumDiffEntry { + fn quorum_updates(&self) -> usize { + self.new_quorums.len() + self.deleted_quorums.len() + self.quorums.len() + } +} diff --git a/packages/dapi-grpc/examples/dapi-cli/core/masternode_status.rs b/packages/dapi-grpc/examples/dapi-cli/core/masternode_status.rs new file mode 100644 index 00000000000..6bfa708a960 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/core/masternode_status.rs @@ -0,0 +1,65 @@ +use clap::Args; +use dapi_grpc::core::v0::{ + core_client::CoreClient, get_masternode_status_response::Status as GrpcStatus, + GetMasternodeStatusRequest, +}; +use dapi_grpc::tonic::transport::Channel; + +use crate::error::{CliError, CliResult}; + +#[derive(Args, Debug)] +pub struct MasternodeStatusCommand {} + +pub async fn run(url: &str, _cmd: MasternodeStatusCommand) -> CliResult<()> { + let channel = Channel::from_shared(url.to_string()) + .map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })? + .connect() + .await?; + + let mut client = CoreClient::new(channel); + + let response = client + .get_masternode_status(GetMasternodeStatusRequest {}) + .await? + .into_inner(); + + let status = GrpcStatus::try_from(response.status).unwrap_or(GrpcStatus::Unknown); + let pro_tx_hash = if response.pro_tx_hash.is_empty() { + "".to_string() + } else { + hex::encode(response.pro_tx_hash) + }; + + println!("Masternode status via {}", url); + println!("Status : {}", human_status(status)); + println!("ProTx Hash : {}", pro_tx_hash); + println!("PoSe Penalty : {}", response.pose_penalty); + println!("Core Synced : {}", yes_no(response.is_synced)); + println!("Sync Progress : {:.2}%", response.sync_progress * 100.0); + + Ok(()) +} + +fn human_status(status: GrpcStatus) -> &'static str { + match status { + GrpcStatus::Unknown => "Unknown", + GrpcStatus::WaitingForProtx => "Waiting for ProTx", + GrpcStatus::PoseBanned => "PoSe banned", + GrpcStatus::Removed => "Removed", + GrpcStatus::OperatorKeyChanged => "Operator key changed", + GrpcStatus::ProtxIpChanged => "ProTx IP changed", + GrpcStatus::Ready => "Ready", + GrpcStatus::Error => "Error", + } +} + +fn yes_no(flag: bool) -> &'static str { + if flag { + "yes" + } else { + "no" + } +} diff --git a/packages/dapi-grpc/examples/dapi-cli/core/mod.rs b/packages/dapi-grpc/examples/dapi-cli/core/mod.rs new file mode 100644 index 00000000000..8c30acecde2 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/core/mod.rs @@ -0,0 +1,33 @@ +use clap::Subcommand; + +use crate::error::CliResult; + +pub mod block_hash; +pub mod chainlocks; +pub mod masternode; +pub mod masternode_status; +pub mod transactions; + +#[derive(Subcommand, Debug)] +pub enum CoreCommand { + /// Get block hash by height + BlockHash(block_hash::BlockHashCommand), + /// Stream Core transactions with proofs + Transactions(transactions::TransactionsCommand), + /// Stream masternode list diffs + Masternode(masternode::MasternodeCommand), + /// Get masternode status summary + MasternodeStatus(masternode_status::MasternodeStatusCommand), + /// Stream chain locks and corresponding block headers + ChainLocks(chainlocks::ChainLocksCommand), +} + +pub async fn run(url: &str, command: CoreCommand) -> CliResult<()> { + match command { + CoreCommand::BlockHash(cmd) => block_hash::run(url, cmd).await, + CoreCommand::Transactions(cmd) => transactions::run(url, cmd).await, + CoreCommand::Masternode(cmd) => masternode::run(url, cmd).await, + CoreCommand::MasternodeStatus(cmd) => masternode_status::run(url, cmd).await, + CoreCommand::ChainLocks(cmd) => chainlocks::run(url, cmd).await, + } +} diff --git a/packages/dapi-grpc/examples/dapi-cli/core/transactions.rs b/packages/dapi-grpc/examples/dapi-cli/core/transactions.rs new file mode 100644 index 00000000000..437b104573b --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/core/transactions.rs @@ -0,0 +1,126 @@ +use clap::Args; +use dapi_grpc::core::v0::{ + core_client::CoreClient, transactions_with_proofs_request::FromBlock, + TransactionsWithProofsRequest, +}; +use dapi_grpc::tonic::transport::Channel; +use tracing::{info, warn}; + +use crate::error::{CliError, CliResult}; + +#[derive(Args, Debug)] +pub struct TransactionsCommand { + /// Starting block height for historical streaming + #[arg(long, default_value_t = 1)] + pub from_height: u32, + + /// Send transaction hashes instead of full transactions + #[arg(long, default_value_t = false)] + pub hashes_only: bool, +} + +pub async fn run(url: &str, cmd: TransactionsCommand) -> CliResult<()> { + info!(url = %url, "Connecting to DAPI Core gRPC"); + + let channel = Channel::from_shared(url.to_string()) + .map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })? + .connect() + .await?; + let mut client = CoreClient::new(channel); + + let request = TransactionsWithProofsRequest { + bloom_filter: None, + from_block: Some(FromBlock::FromBlockHeight(cmd.from_height)), + count: 0, + send_transaction_hashes: cmd.hashes_only, + }; + + println!("📡 Subscribing to transactions with proofs from {}", url); + println!(" Starting from block height {}", cmd.from_height); + if cmd.hashes_only { + println!(" Streaming transaction hashes only\n"); + } else { + println!(" Streaming full transaction payloads\n"); + } + + let response = client + .subscribe_to_transactions_with_proofs(request) + .await?; + let mut stream = response.into_inner(); + + let mut transaction_count = 0usize; + let mut merkle_block_count = 0usize; + let mut instant_lock_count = 0usize; + + while let Some(response) = stream.message().await? { + match response.responses { + Some(dapi_grpc::core::v0::transactions_with_proofs_response::Responses::RawTransactions(raw_txs)) => { + transaction_count += raw_txs.transactions.len(); + println!( + "đŸ“Ļ Received {} transaction(s) (total: {})", + raw_txs.transactions.len(), + transaction_count + ); + + if !cmd.hashes_only { + for (i, tx_data) in raw_txs.transactions.iter().enumerate() { + let hash_preview = hash_preview(tx_data); + println!( + " 📝 Transaction {}: {} bytes (preview: {}...)", + i + 1, + tx_data.len(), + hash_preview + ); + } + } + } + Some(dapi_grpc::core::v0::transactions_with_proofs_response::Responses::RawMerkleBlock(merkle_block)) => { + merkle_block_count += 1; + println!( + "đŸŒŗ Received Merkle Block #{} ({} bytes)", + merkle_block_count, + merkle_block.len() + ); + + println!( + " 🔗 Block preview: {}...", + hash_preview(&merkle_block) + ); + } + Some(dapi_grpc::core::v0::transactions_with_proofs_response::Responses::InstantSendLockMessages(locks)) => { + instant_lock_count += locks.messages.len(); + println!( + "⚡ Received {} InstantSend lock(s) (total: {})", + locks.messages.len(), + instant_lock_count + ); + + for (i, lock_data) in locks.messages.iter().enumerate() { + println!(" InstantLock {}: {} bytes", i + 1, lock_data.len()); + } + } + other => { + warn!(?other, "Received unexpected transactions response variant"); + } + } + + println!(); + } + + println!("👋 Stream ended"); + Ok(()) +} + +fn hash_preview(data: &[u8]) -> String { + if data.len() >= 8 { + format!( + "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7] + ) + } else { + "short".to_string() + } +} diff --git a/packages/dapi-grpc/examples/dapi-cli/error.rs b/packages/dapi-grpc/examples/dapi-cli/error.rs new file mode 100644 index 00000000000..b51c36ba3ee --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/error.rs @@ -0,0 +1,43 @@ +use std::io; + +use ciborium::de::Error as CborError; +use thiserror::Error; +use tokio::time::error::Elapsed; + +pub type CliResult = Result; + +#[derive(Debug, Error)] +pub enum CliError { + #[error("invalid DAPI URL '{url}': {source}")] + InvalidUrl { + url: String, + #[source] + source: Box, + }, + #[error("failed to connect to DAPI service: {0}")] + Transport(#[from] tonic::transport::Error), + #[error(transparent)] + Status(#[from] tonic::Status), + #[error("invalid state transition hash '{hash}': {source}")] + InvalidHash { + hash: String, + #[source] + source: hex::FromHexError, + }, + #[error("invalid state transition payload: {0}")] + InvalidStateTransition(#[from] hex::FromHexError), + #[error(transparent)] + Timeout(#[from] Elapsed), + #[error("CBOR decode error: {0}")] + Cbor(#[from] CborError), + #[error(transparent)] + Io(#[from] io::Error), + #[error("received empty response from {0}")] + EmptyResponse(&'static str), + + #[error(transparent)] + DashCoreEncoding(#[from] dashcore::consensus::encode::Error), + + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), +} diff --git a/packages/dapi-grpc/examples/dapi-cli/main.rs b/packages/dapi-grpc/examples/dapi-cli/main.rs new file mode 100644 index 00000000000..96a8a06af9e --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/main.rs @@ -0,0 +1,62 @@ +mod core; +mod error; +mod platform; + +use clap::{ArgAction, Parser, Subcommand}; +use error::CliResult; + +#[derive(Parser, Debug)] +#[command( + name = "dapi-cli", + version, + about = "Interactive utilities for rs-dapi" +)] +struct Cli { + /// DAPI gRPC endpoint (applies to all commands) + #[arg(long, global = true, default_value = "http://127.0.0.1:3005")] + url: String, + + /// Increase logging verbosity (-v for debug, -vv for trace) + #[arg(short, long, global = true, action = ArgAction::Count)] + verbose: u8, + + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Core gRPC helpers + #[command(subcommand)] + Core(core::CoreCommand), + /// Platform gRPC helpers + #[command(subcommand)] + Platform(platform::PlatformCommand), +} + +fn init_tracing(verbosity: u8) { + let level = match verbosity { + 0 => std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()), + 1 => "debug".to_string(), + _ => "trace".to_string(), + }; + + let _ = tracing_subscriber::fmt() + .with_env_filter(level) + .with_target(false) + .try_init(); +} + +#[tokio::main] +async fn main() -> CliResult<()> { + let cli = Cli::parse(); + + init_tracing(cli.verbose); + + match cli.command { + Command::Core(command) => core::run(&cli.url, command).await?, + Command::Platform(command) => platform::run(&cli.url, command).await?, + } + + Ok(()) +} diff --git a/packages/dapi-grpc/examples/dapi-cli/platform/identity.rs b/packages/dapi-grpc/examples/dapi-cli/platform/identity.rs new file mode 100644 index 00000000000..46803426966 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/platform/identity.rs @@ -0,0 +1,111 @@ +use clap::{Args, Subcommand}; +use dapi_grpc::platform::v0::get_identity_by_public_key_hash_request::GetIdentityByPublicKeyHashRequestV0; +use dapi_grpc::platform::v0::get_identity_by_public_key_hash_response::{ + self as get_identity_by_public_key_hash_response, + get_identity_by_public_key_hash_response_v0::Result as ByKeyResult, +}; +use dapi_grpc::platform::v0::{ + get_identity_by_public_key_hash_request, platform_client::PlatformClient, + GetIdentityByPublicKeyHashRequest, +}; +use dapi_grpc::tonic::{transport::Channel, Request}; + +use crate::error::{CliError, CliResult}; + +#[derive(Subcommand, Debug)] +pub enum IdentityCommand { + /// Fetch identity by unique public key hash + ByKey(ByKeyCommand), +} + +#[derive(Args, Debug)] +pub struct ByKeyCommand { + /// Public key hash (20-byte hex string) + #[arg(value_name = "HEX")] + pub public_key_hash: String, + /// Request cryptographic proof alongside the identity + #[arg(long, default_value_t = false)] + pub prove: bool, +} + +pub async fn run(url: &str, command: IdentityCommand) -> CliResult<()> { + match command { + IdentityCommand::ByKey(cmd) => by_key(url, cmd).await, + } +} + +async fn by_key(url: &str, cmd: ByKeyCommand) -> CliResult<()> { + let pk_hash = hex::decode(&cmd.public_key_hash).map_err(|source| CliError::InvalidHash { + hash: cmd.public_key_hash.clone(), + source, + })?; + + let channel = Channel::from_shared(url.to_string()).map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })?; + let mut client = PlatformClient::connect(channel).await?; + + let request = GetIdentityByPublicKeyHashRequest { + version: Some(get_identity_by_public_key_hash_request::Version::V0( + GetIdentityByPublicKeyHashRequestV0 { + public_key_hash: pk_hash, + prove: cmd.prove, + }, + )), + }; + + let response = client + .get_identity_by_public_key_hash(Request::new(request)) + .await? + .into_inner(); + + let Some(get_identity_by_public_key_hash_response::Version::V0(v0)) = response.version else { + return Err(CliError::EmptyResponse("getIdentityByPublicKeyHash")); + }; + + print_metadata(v0.metadata.as_ref()); + + match v0.result { + Some(ByKeyResult::Identity(identity_bytes)) => { + if identity_bytes.is_empty() { + println!("❌ Identity not found for the provided public key hash"); + } else { + println!( + "✅ Identity bytes: {} ({} bytes)", + hex::encode_upper(&identity_bytes), + identity_bytes.len() + ); + } + } + Some(ByKeyResult::Proof(proof)) => { + print_proof(&proof); + } + None => println!("â„šī¸ Response did not include identity data"), + } + + Ok(()) +} + +fn print_metadata(metadata: Option<&dapi_grpc::platform::v0::ResponseMetadata>) { + if let Some(meta) = metadata { + println!("â„šī¸ Metadata:"); + println!(" height: {}", meta.height); + println!( + " core_chain_locked_height: {}", + meta.core_chain_locked_height + ); + println!(" epoch: {}", meta.epoch); + println!(" protocol_version: {}", meta.protocol_version); + println!(" chain_id: {}", meta.chain_id); + println!(" time_ms: {}", meta.time_ms); + } +} + +fn print_proof(proof: &dapi_grpc::platform::v0::Proof) { + println!("🔐 Proof received:"); + println!(" quorum_hash: {}", hex::encode_upper(&proof.quorum_hash)); + println!(" signature bytes: {}", proof.signature.len()); + println!(" grovedb_proof bytes: {}", proof.grovedb_proof.len()); + println!(" round: {}", proof.round); +} diff --git a/packages/dapi-grpc/examples/dapi-cli/platform/mod.rs b/packages/dapi-grpc/examples/dapi-cli/platform/mod.rs new file mode 100644 index 00000000000..eafb0ecea53 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/platform/mod.rs @@ -0,0 +1,37 @@ +use clap::Subcommand; + +use crate::error::CliResult; + +pub mod identity; +pub mod protocol; +pub mod state_transition; + +#[derive(Subcommand, Debug)] +pub enum PlatformCommand { + /// Platform state transition helpers + #[command(subcommand)] + StateTransition(state_transition::StateTransitionCommand), + /// Platform identity helpers + #[command(subcommand)] + Identity(identity::IdentityCommand), + /// Fetch general platform status + GetStatus, + /// Fetch protocol version upgrade state summary + ProtocolUpgradeState(protocol::UpgradeStateCommand), + /// Fetch protocol version upgrade vote status details + ProtocolUpgradeVoteStatus(protocol::UpgradeVoteStatusCommand), +} + +pub async fn run(url: &str, command: PlatformCommand) -> CliResult<()> { + match command { + PlatformCommand::StateTransition(command) => state_transition::run(url, command).await, + PlatformCommand::Identity(command) => identity::run(url, command).await, + PlatformCommand::GetStatus => protocol::run_get_status(url).await, + PlatformCommand::ProtocolUpgradeState(command) => { + protocol::run_upgrade_state(url, command).await + } + PlatformCommand::ProtocolUpgradeVoteStatus(command) => { + protocol::run_upgrade_vote_status(url, command).await + } + } +} diff --git a/packages/dapi-grpc/examples/dapi-cli/platform/protocol.rs b/packages/dapi-grpc/examples/dapi-cli/platform/protocol.rs new file mode 100644 index 00000000000..046af3625ba --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/platform/protocol.rs @@ -0,0 +1,313 @@ +use dapi_grpc::platform::v0::get_status_response::GetStatusResponseV0; +use dapi_grpc::platform::v0::{platform_client::PlatformClient, GetStatusRequest}; +use dapi_grpc::tonic::{transport::Channel, Request}; +use tracing::info; + +use crate::error::{CliError, CliResult}; +use clap::Args; +use dapi_grpc::platform::v0::{ + GetProtocolVersionUpgradeStateRequest, + GetProtocolVersionUpgradeVoteStatusRequest, + get_protocol_version_upgrade_state_request, + get_protocol_version_upgrade_state_request::GetProtocolVersionUpgradeStateRequestV0, + get_protocol_version_upgrade_state_response, + get_protocol_version_upgrade_state_response::get_protocol_version_upgrade_state_response_v0::Result as UpgradeStateResult, + get_protocol_version_upgrade_state_response::get_protocol_version_upgrade_state_response_v0::Versions, + get_protocol_version_upgrade_vote_status_request, + get_protocol_version_upgrade_vote_status_request::GetProtocolVersionUpgradeVoteStatusRequestV0, + get_protocol_version_upgrade_vote_status_response, + get_protocol_version_upgrade_vote_status_response::get_protocol_version_upgrade_vote_status_response_v0::Result as VoteStatusResult, + get_protocol_version_upgrade_vote_status_response::get_protocol_version_upgrade_vote_status_response_v0::VersionSignals, +}; + +#[derive(Args, Debug)] +pub struct UpgradeStateCommand { + /// Request cryptographic proof alongside the state information + #[arg(long, default_value_t = false)] + pub prove: bool, +} + +pub async fn run_upgrade_state(url: &str, cmd: UpgradeStateCommand) -> CliResult<()> { + info!( + prove = cmd.prove, + "Requesting protocol version upgrade state" + ); + + let channel = Channel::from_shared(url.to_string()).map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })?; + let mut client = PlatformClient::connect(channel).await?; + + let request = GetProtocolVersionUpgradeStateRequest { + version: Some(get_protocol_version_upgrade_state_request::Version::V0( + GetProtocolVersionUpgradeStateRequestV0 { prove: cmd.prove }, + )), + }; + + let response = client + .get_protocol_version_upgrade_state(Request::new(request)) + .await? + .into_inner(); + + let Some(get_protocol_version_upgrade_state_response::Version::V0(v0)) = response.version + else { + return Err(CliError::EmptyResponse("getProtocolVersionUpgradeState")); + }; + + print_metadata(v0.metadata.as_ref()); + + match v0.result { + Some(UpgradeStateResult::Versions(Versions { versions })) => { + if versions.is_empty() { + println!("â„šī¸ No protocol version entries returned"); + } else { + println!("📊 Protocol version entries ({}):", versions.len()); + for entry in versions { + println!( + " â€ĸ version {} => {} vote(s)", + entry.version_number, entry.vote_count + ); + } + } + } + Some(UpgradeStateResult::Proof(proof)) => { + print_proof(&proof); + } + None => println!("â„šī¸ Response did not include version information"), + } + + Ok(()) +} + +#[derive(Args, Debug)] +pub struct UpgradeVoteStatusCommand { + /// Optional starting ProTx hash (hex) for pagination + #[arg(long, value_name = "HEX")] + pub start_pro_tx_hash: Option, + /// Maximum number of vote entries to return (0 means default server limit) + #[arg(long, default_value_t = 0)] + pub count: u32, + /// Request cryptographic proof alongside the vote information + #[arg(long, default_value_t = false)] + pub prove: bool, +} + +pub async fn run_upgrade_vote_status(url: &str, cmd: UpgradeVoteStatusCommand) -> CliResult<()> { + info!( + prove = cmd.prove, + count = cmd.count, + "Requesting protocol version upgrade vote status" + ); + + let start_pro_tx_hash = if let Some(ref hash) = cmd.start_pro_tx_hash { + hex::decode(hash).map_err(|source| CliError::InvalidHash { + hash: hash.clone(), + source, + })? + } else { + Vec::new() + }; + + let channel = Channel::from_shared(url.to_string()).map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })?; + let mut client = PlatformClient::connect(channel).await?; + + let request = GetProtocolVersionUpgradeVoteStatusRequest { + version: Some( + get_protocol_version_upgrade_vote_status_request::Version::V0( + GetProtocolVersionUpgradeVoteStatusRequestV0 { + start_pro_tx_hash, + count: cmd.count, + prove: cmd.prove, + }, + ), + ), + }; + + let response = client + .get_protocol_version_upgrade_vote_status(Request::new(request)) + .await? + .into_inner(); + + let Some(get_protocol_version_upgrade_vote_status_response::Version::V0(v0)) = response.version + else { + return Err(CliError::EmptyResponse( + "getProtocolVersionUpgradeVoteStatus", + )); + }; + + print_metadata(v0.metadata.as_ref()); + + match v0.result { + Some(VoteStatusResult::Versions(VersionSignals { version_signals })) => { + if version_signals.is_empty() { + println!("â„šī¸ No vote status entries returned"); + } else { + println!("đŸ—ŗī¸ Vote status entries ({}):", version_signals.len()); + for signal in version_signals { + let pro_tx_hash = hex::encode_upper(signal.pro_tx_hash); + println!( + " â€ĸ proTxHash {} => version {}", + pro_tx_hash, signal.version + ); + } + } + } + Some(VoteStatusResult::Proof(proof)) => { + print_proof(&proof); + } + None => println!("â„šī¸ Response did not include vote status information"), + } + + Ok(()) +} + +pub async fn run_get_status(url: &str) -> CliResult<()> { + let channel = Channel::from_shared(url.to_string()).map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })?; + let mut client = PlatformClient::connect(channel).await?; + + let request = GetStatusRequest { + version: Some(dapi_grpc::platform::v0::get_status_request::Version::V0( + dapi_grpc::platform::v0::get_status_request::GetStatusRequestV0 {}, + )), + }; + + let response = client.get_status(Request::new(request)).await?.into_inner(); + + let Some(dapi_grpc::platform::v0::get_status_response::Version::V0(v0)) = response.version + else { + return Err(CliError::EmptyResponse("getStatus")); + }; + + print_status(&v0); + Ok(()) +} + +fn print_metadata(metadata: Option<&dapi_grpc::platform::v0::ResponseMetadata>) { + if let Some(meta) = metadata { + println!("â„šī¸ Metadata:"); + println!(" height: {}", meta.height); + println!( + " core_chain_locked_height: {}", + meta.core_chain_locked_height + ); + println!(" epoch: {}", meta.epoch); + println!(" protocol_version: {}", meta.protocol_version); + println!(" chain_id: {}", meta.chain_id); + println!(" time_ms: {}", meta.time_ms); + } +} + +fn print_status(status: &GetStatusResponseV0) { + if let Some(version) = &status.version { + println!("đŸ“Ļ Software Versions:"); + if let Some(software) = &version.software { + println!(" dapi: {}", software.dapi); + if let Some(drive) = &software.drive { + println!(" drive: {}", drive); + } + if let Some(tenderdash) = &software.tenderdash { + println!(" tenderdash: {}", tenderdash); + } + } + if let Some(protocol) = &version.protocol { + if let Some(td) = &protocol.tenderdash { + println!("🔄 Tenderdash protocol: p2p={}, block={}", td.p2p, td.block); + } + if let Some(drive) = &protocol.drive { + println!( + "🔄 Drive protocol: current={} latest={}", + drive.current, drive.latest + ); + } + } + println!(); + } + + if let Some(node) = &status.node { + println!("đŸ–Ĩī¸ Node Information:"); + if !node.id.is_empty() { + println!(" id: {}", hex::encode_upper(&node.id)); + } + if let Some(protx) = &node.pro_tx_hash { + println!(" proTxHash: {}", hex::encode_upper(protx)); + } + println!(); + } + + if let Some(chain) = &status.chain { + println!("â›“ī¸ Chain Info:"); + println!(" catching_up: {}", chain.catching_up); + println!(" latest_block_height: {}", chain.latest_block_height); + println!(" max_peer_block_height: {}", chain.max_peer_block_height); + if let Some(cclh) = chain.core_chain_locked_height { + println!(" core_chain_locked_height: {}", cclh); + } + if !chain.latest_block_hash.is_empty() { + println!( + " latest_block_hash: {}", + hex::encode_upper(&chain.latest_block_hash) + ); + } + println!(); + } + + if let Some(network) = &status.network { + println!("🌐 Network:"); + println!(" chain_id: {}", network.chain_id); + println!(" peers_count: {}", network.peers_count); + println!(" listening: {}", network.listening); + println!(); + } + + if let Some(state_sync) = &status.state_sync { + println!("🔁 State Sync:"); + println!(" total_synced_time: {}", state_sync.total_synced_time); + println!(" remaining_time: {}", state_sync.remaining_time); + println!(" total_snapshots: {}", state_sync.total_snapshots); + println!( + " chunk_process_avg_time: {}", + state_sync.chunk_process_avg_time + ); + println!(" snapshot_height: {}", state_sync.snapshot_height); + println!( + " snapshot_chunks_count: {}", + state_sync.snapshot_chunks_count + ); + println!(" backfilled_blocks: {}", state_sync.backfilled_blocks); + println!( + " backfill_blocks_total: {}", + state_sync.backfill_blocks_total + ); + println!(); + } + + if let Some(time) = &status.time { + println!("🕒 Time:"); + println!(" local: {}", time.local); + if let Some(block) = time.block { + println!(" block: {}", block); + } + if let Some(genesis) = time.genesis { + println!(" genesis: {}", genesis); + } + if let Some(epoch) = time.epoch { + println!(" epoch: {}", epoch); + } + println!(); + } +} + +fn print_proof(proof: &dapi_grpc::platform::v0::Proof) { + println!("🔐 Proof received:"); + println!(" quorum_hash: {}", hex::encode_upper(&proof.quorum_hash)); + println!(" signature bytes: {}", proof.signature.len()); + println!(" grovedb_proof bytes: {}", proof.grovedb_proof.len()); + println!(" round: {}", proof.round); +} diff --git a/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/mod.rs b/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/mod.rs new file mode 100644 index 00000000000..38481166775 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/mod.rs @@ -0,0 +1,21 @@ +mod monitor; +mod workflow; + +use clap::Subcommand; + +use crate::error::CliResult; + +#[derive(Subcommand, Debug)] +pub enum StateTransitionCommand { + /// Wait for a state transition result by hash + Monitor(monitor::MonitorCommand), + /// Broadcast a state transition and wait for the result + Workflow(workflow::WorkflowCommand), +} + +pub async fn run(url: &str, command: StateTransitionCommand) -> CliResult<()> { + match command { + StateTransitionCommand::Monitor(cmd) => monitor::run(url, cmd).await, + StateTransitionCommand::Workflow(cmd) => workflow::run(url, cmd).await, + } +} diff --git a/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/monitor.rs b/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/monitor.rs new file mode 100644 index 00000000000..ecacb5cb551 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/monitor.rs @@ -0,0 +1,117 @@ +use clap::Args; +use dapi_grpc::platform::v0::{ + platform_client::PlatformClient, + wait_for_state_transition_result_request::{Version, WaitForStateTransitionResultRequestV0}, + wait_for_state_transition_result_response::{ + self, wait_for_state_transition_result_response_v0, + }, + WaitForStateTransitionResultRequest, +}; +use dapi_grpc::tonic::{transport::Channel, Request}; +use tracing::{info, warn}; + +use crate::error::{CliError, CliResult}; + +#[derive(Args, Debug)] +pub struct MonitorCommand { + /// Hex-encoded state transition hash to monitor + #[arg(long, value_name = "HASH")] + pub hash: String, + + /// Request cryptographic proof in the response + #[arg(long, default_value_t = false)] + pub prove: bool, +} + +pub async fn run(url: &str, cmd: MonitorCommand) -> CliResult<()> { + info!(hash = %cmd.hash, prove = cmd.prove, "Monitoring state transition"); + + let state_transition_hash = hex::decode(&cmd.hash).map_err(|source| CliError::InvalidHash { + hash: cmd.hash.clone(), + source, + })?; + + let channel = Channel::from_shared(url.to_string()).map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })?; + let mut client = PlatformClient::connect(channel).await?; + + let request = Request::new(WaitForStateTransitionResultRequest { + version: Some(Version::V0(WaitForStateTransitionResultRequestV0 { + state_transition_hash, + prove: cmd.prove, + })), + }); + + let response = client.wait_for_state_transition_result(request).await?; + + let response_inner = response.into_inner(); + + match response_inner.version { + Some(wait_for_state_transition_result_response::Version::V0(v0)) => { + print_response_metadata(&v0.metadata); + + match v0.result { + Some(wait_for_state_transition_result_response_v0::Result::Proof(proof)) => { + info!("✅ State transition processed successfully"); + print_proof_info(&proof); + } + Some(wait_for_state_transition_result_response_v0::Result::Error(error)) => { + warn!("âš ī¸ State transition failed"); + print_error_info(&error); + } + None => { + info!("✅ State transition processed (no proof requested)"); + } + } + } + None => return Err(CliError::EmptyResponse("waitForStateTransitionResult")), + } + + Ok(()) +} + +pub(super) fn print_response_metadata( + metadata: &Option, +) { + if let Some(metadata) = metadata { + info!("Response metadata:"); + info!(" Block Height: {}", metadata.height); + info!( + " Core Chain Locked Height: {}", + metadata.core_chain_locked_height + ); + info!(" Epoch: {}", metadata.epoch); + info!(" Time: {} ms", metadata.time_ms); + info!(" Protocol Version: {}", metadata.protocol_version); + info!(" Chain ID: {}", metadata.chain_id); + } +} + +pub(super) fn print_proof_info(proof: &dapi_grpc::platform::v0::Proof) { + info!("Cryptographic proof details:"); + info!(" GroveDB Proof Size: {} bytes", proof.grovedb_proof.len()); + info!(" Quorum Hash: {}", hex::encode(&proof.quorum_hash)); + info!(" Signature Size: {} bytes", proof.signature.len()); + info!(" Round: {}", proof.round); + info!(" Block ID Hash: {}", hex::encode(&proof.block_id_hash)); + info!(" Quorum Type: {}", proof.quorum_type); + + if !proof.grovedb_proof.is_empty() { + info!(" GroveDB Proof: {}", hex::encode(&proof.grovedb_proof)); + } + + if !proof.signature.is_empty() { + info!(" Signature: {}", hex::encode(&proof.signature)); + } +} + +pub(super) fn print_error_info(error: &dapi_grpc::platform::v0::StateTransitionBroadcastError) { + warn!("Error details:"); + warn!(" Code: {}", error.code); + warn!(" Message: {}", error.message); + if !error.data.is_empty() { + warn!(" Data: {}", hex::encode(&error.data)); + } +} diff --git a/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/workflow.rs b/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/workflow.rs new file mode 100644 index 00000000000..50287a44082 --- /dev/null +++ b/packages/dapi-grpc/examples/dapi-cli/platform/state_transition/workflow.rs @@ -0,0 +1,108 @@ +use super::monitor::{print_error_info, print_proof_info, print_response_metadata}; +use clap::Args; +use dapi_grpc::platform::v0::{ + platform_client::PlatformClient, + wait_for_state_transition_result_request::{Version, WaitForStateTransitionResultRequestV0}, + wait_for_state_transition_result_response::{ + self, wait_for_state_transition_result_response_v0, + }, + BroadcastStateTransitionRequest, WaitForStateTransitionResultRequest, +}; +use dapi_grpc::tonic::{transport::Channel, Request}; +use sha2::{Digest, Sha256}; +use std::time::Duration; +use tokio::time::timeout; +use tracing::{info, warn}; + +use crate::error::{CliError, CliResult}; + +#[derive(Args, Debug)] +pub struct WorkflowCommand { + /// Hex-encoded state transition to broadcast + #[arg(long, value_name = "HEX")] + pub state_transition_hex: String, + + /// Request cryptographic proof in the result + #[arg(long, default_value_t = false)] + pub prove: bool, + + /// Timeout (seconds) when waiting for the result + #[arg(long, default_value_t = 60)] + pub timeout_secs: u64, +} + +pub async fn run(url: &str, cmd: WorkflowCommand) -> CliResult<()> { + info!(prove = cmd.prove, "Starting state transition workflow"); + + let state_transition = + hex::decode(&cmd.state_transition_hex).map_err(CliError::InvalidStateTransition)?; + + info!(bytes = state_transition.len(), "Parsed state transition"); + + let hash = Sha256::digest(&state_transition).to_vec(); + let hash_hex = hex::encode(&hash); + info!(hash = %hash_hex, "Computed state transition hash"); + + let channel = Channel::from_shared(url.to_string()).map_err(|source| CliError::InvalidUrl { + url: url.to_string(), + source: Box::new(source), + })?; + let mut client = PlatformClient::connect(channel).await?; + + info!("Broadcasting state transition"); + let broadcast_request = Request::new(BroadcastStateTransitionRequest { + state_transition: state_transition.clone(), + }); + + let broadcast_start = std::time::Instant::now(); + let response = client.broadcast_state_transition(broadcast_request).await?; + + info!(duration = ?broadcast_start.elapsed(), "Broadcast succeeded"); + info!("Response: {:?}", response.into_inner()); + + info!( + timeout_secs = cmd.timeout_secs, + "Waiting for state transition result" + ); + let wait_request = Request::new(WaitForStateTransitionResultRequest { + version: Some(Version::V0(WaitForStateTransitionResultRequestV0 { + state_transition_hash: hash, + prove: cmd.prove, + })), + }); + + let wait_future = client.wait_for_state_transition_result(wait_request); + let wait_start = std::time::Instant::now(); + + let response = match timeout(Duration::from_secs(cmd.timeout_secs), wait_future).await { + Ok(result) => result?, + Err(elapsed) => return Err(CliError::Timeout(elapsed)), + }; + + info!(duration = ?wait_start.elapsed(), "State transition result received"); + + let response_inner = response.into_inner(); + + match response_inner.version { + Some(wait_for_state_transition_result_response::Version::V0(v0)) => { + print_response_metadata(&v0.metadata); + + match v0.result { + Some(wait_for_state_transition_result_response_v0::Result::Proof(proof)) => { + info!("State transition processed successfully with proof"); + print_proof_info(&proof); + } + Some(wait_for_state_transition_result_response_v0::Result::Error(error)) => { + warn!("State transition failed during processing"); + print_error_info(&error); + } + None => { + info!("State transition processed successfully (no proof requested)"); + } + } + } + None => return Err(CliError::EmptyResponse("waitForStateTransitionResult")), + } + + Ok(()) +}