Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 60 additions & 51 deletions Cargo.lock

Large diffs are not rendered by default.

27 changes: 21 additions & 6 deletions packages/dapi-grpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
58 changes: 58 additions & 0 deletions packages/dapi-grpc/examples/dapi-cli/core/block_hash.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
83 changes: 83 additions & 0 deletions packages/dapi-grpc/examples/dapi-cli/core/chainlocks.rs
Original file line number Diff line number Diff line change
@@ -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<u32>,
}

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(())
}
158 changes: 158 additions & 0 deletions packages/dapi-grpc/examples/dapi-cli/core/masternode.rs
Original file line number Diff line number Diff line change
@@ -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::<MasternodeListDiff, _>(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("<unknown>");
let block_hash = diff.block_hash.as_deref().unwrap_or("<unknown>");

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<String>,
#[serde(rename = "blockHash")]
block_hash: Option<String>,
#[serde(rename = "addedMNs", default)]
added_mns: Vec<Value>,
#[serde(rename = "updatedMNs", default)]
updated_mns: Vec<Value>,
#[serde(rename = "removedMNs", default)]
removed_mns: Vec<Value>,
#[serde(rename = "mnList", default)]
full_list: Vec<Value>,
#[serde(rename = "masternodeList", default)]
masternode_list: Vec<Value>,
#[serde(rename = "totalMnCount")]
total_mn_count: Option<u64>,
#[serde(rename = "quorumDiffs", default)]
quorum_diffs: Vec<QuorumDiffEntry>,
#[serde(rename = "newQuorums", default)]
new_quorums: Vec<Value>,
#[serde(rename = "deletedQuorums", default)]
deleted_quorums: Vec<Value>,
#[serde(default)]
quorums: Vec<Value>,
}

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<Value>,
#[serde(rename = "deletedQuorums", default)]
deleted_quorums: Vec<Value>,
#[serde(default)]
quorums: Vec<Value>,
}

impl QuorumDiffEntry {
fn quorum_updates(&self) -> usize {
self.new_quorums.len() + self.deleted_quorums.len() + self.quorums.len()
}
}
65 changes: 65 additions & 0 deletions packages/dapi-grpc/examples/dapi-cli/core/masternode_status.rs
Original file line number Diff line number Diff line change
@@ -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() {
"<unset>".to_string()
} else {
hex::encode(response.pro_tx_hash)
};
Comment on lines +29 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix enum conversion: use from_i32, not try_from

Prost enums expose from_i32; TryFrom isn’t implemented by default and will not compile.

-    let status = GrpcStatus::try_from(response.status).unwrap_or(GrpcStatus::Unknown);
+    let status = GrpcStatus::from_i32(response.status).unwrap_or(GrpcStatus::Unknown);

🏁 Script executed:

#!/bin/bash
# Scan for similar enum conversions that may not compile
rg -nP --type=rust -C2 '\b::try_from\s*\(\s*response\.\w+\s*\)' examples

Length of output: 48


#!/bin/bash
# Rescan for enum conversions in Rust .rs files across the repo
rg -nP --type=rust -C2 'try_from\s*\(\s*response\.' --glob '*.rs' .

-->


🏁 Script executed:

#!/bin/bash
# Locate and preview imports in masternode_status.rs
echo "File path(s):"
fd -t f masternode_status.rs .
echo "---- File preview ----"
sed -n '1,50p' $(fd -t f masternode_status.rs .)

Length of output: 1716


Replace enum conversion: use from_i32, not try_from
Prost-generated enums provide from_i32; TryFrom<i32> isn’t implemented by default and will not compile.

-    let status = GrpcStatus::try_from(response.status).unwrap_or(GrpcStatus::Unknown);
+    let status = GrpcStatus::from_i32(response.status).unwrap_or(GrpcStatus::Unknown);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let status = GrpcStatus::try_from(response.status).unwrap_or(GrpcStatus::Unknown);
let pro_tx_hash = if response.pro_tx_hash.is_empty() {
"<unset>".to_string()
} else {
hex::encode(response.pro_tx_hash)
};
let status = GrpcStatus::from_i32(response.status).unwrap_or(GrpcStatus::Unknown);
let pro_tx_hash = if response.pro_tx_hash.is_empty() {
"<unset>".to_string()
} else {
hex::encode(response.pro_tx_hash)
};
🤖 Prompt for AI Agents
In packages/dapi-grpc/examples/dapi-cli/core/masternode_status.rs around lines
29 to 34, the code uses GrpcStatus::try_from(response.status) which won't
compile because prost enums expose from_i32 rather than TryFrom; replace the
conversion with
GrpcStatus::from_i32(response.status).unwrap_or(GrpcStatus::Unknown) (or
equivalent handling of the Option) so the integer status is converted via
from_i32 and defaults to Unknown when unrecognized.


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"
}
}
Loading
Loading