Skip to content

Commit 1202c41

Browse files
committed
test(dapi): dapi-cli example in dapi-grpc
1 parent d6f9d9e commit 1202c41

File tree

16 files changed

+1416
-57
lines changed

16 files changed

+1416
-57
lines changed

Cargo.lock

Lines changed: 60 additions & 51 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/dapi-grpc/Cargo.toml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,25 @@ path = "clients/core/v0/rust/core_example.rs"
7878
name = "platform_example"
7979
path = "clients/platform/v0/rust/platform_example.rs"
8080

81+
[[example]]
82+
name = "dapi-cli"
83+
path = "examples/dapi-cli/main.rs"
84+
85+
[dev-dependencies]
86+
# dapi-cli example dependencies
87+
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "befd0356bebfcd0d06d1028d8a03bfa4c78bd219", features = [
88+
"serde",
89+
] }
90+
serde = { version = "1.0.228", features = ["derive"] }
91+
serde_json = { version = "1.0.145" }
92+
sha2 = { version = "0.10.9" }
93+
hex = { version = "0.4.3" }
94+
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] }
95+
ciborium = { version = "0.2.2" }
96+
thiserror = { version = "2.0.17" }
97+
clap = { version = "4.5.48", features = ["derive"] }
98+
tracing = "0.1.41"
99+
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
100+
81101
[package.metadata.cargo-machete]
82-
ignored = [
83-
"platform-version",
84-
"serde_bytes",
85-
"futures-core",
86-
"dapi-grpc-macros",
87-
]
102+
ignored = ["platform-version", "futures-core", "getrandom", "tonic-prost-build"]
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use clap::Args;
2+
use dapi_grpc::core::v0::{core_client::CoreClient, GetBlockRequest};
3+
use dapi_grpc::tonic::transport::Channel;
4+
use tracing::info;
5+
6+
use crate::error::{CliError, CliResult};
7+
8+
#[derive(Args, Debug)]
9+
pub struct BlockHashCommand {
10+
/// Block height to query (>= 1)
11+
#[arg(long)]
12+
pub height: u32,
13+
}
14+
15+
pub async fn run(url: &str, cmd: BlockHashCommand) -> CliResult<()> {
16+
if cmd.height < 1 {
17+
return Err(
18+
std::io::Error::new(std::io::ErrorKind::InvalidInput, "height must be >= 1").into(),
19+
);
20+
}
21+
22+
info!(url = %url, height = cmd.height, "Querying block hash");
23+
24+
let channel = Channel::from_shared(url.to_string())
25+
.map_err(|source| CliError::InvalidUrl {
26+
url: url.to_string(),
27+
source: Box::new(source),
28+
})?
29+
.connect()
30+
.await?;
31+
let mut client = CoreClient::new(channel);
32+
33+
let request = GetBlockRequest {
34+
block: Some(dapi_grpc::core::v0::get_block_request::Block::Height(
35+
cmd.height,
36+
)),
37+
};
38+
39+
let response = client.get_block(request).await?;
40+
let block_bytes = response.into_inner().block;
41+
42+
// Deserialize and compute hash
43+
use dashcore::consensus::encode::deserialize;
44+
use dashcore::Block;
45+
46+
let block: Block = match deserialize(&block_bytes) {
47+
Ok(b) => b,
48+
Err(e) => {
49+
tracing::error!(block_bytes = hex::encode(&block_bytes), error = %e, "Failed to deserialize block");
50+
return Err(CliError::DashCoreEncoding(e));
51+
}
52+
};
53+
let block_json = serde_json::to_string_pretty(&block)?;
54+
let hash_hex = block.block_hash().to_string();
55+
56+
println!("Block {} hash: {}\n{}\n", cmd.height, hash_hex, block_json);
57+
Ok(())
58+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use clap::Args;
2+
use dapi_grpc::core::v0::{
3+
block_headers_with_chain_locks_request::FromBlock, core_client::CoreClient,
4+
BlockHeadersWithChainLocksRequest,
5+
};
6+
use dapi_grpc::tonic::transport::Channel;
7+
use tracing::{info, warn};
8+
9+
use crate::error::{CliError, CliResult};
10+
11+
#[derive(Args, Debug)]
12+
pub struct ChainLocksCommand {
13+
/// Optional starting block height for historical context
14+
#[arg(long)]
15+
pub from_height: Option<u32>,
16+
}
17+
18+
pub async fn run(url: &str, cmd: ChainLocksCommand) -> CliResult<()> {
19+
info!(url = %url, "Connecting to DAPI Core gRPC for chain locks");
20+
21+
let channel = Channel::from_shared(url.to_string())
22+
.map_err(|source| CliError::InvalidUrl {
23+
url: url.to_string(),
24+
source: Box::new(source),
25+
})?
26+
.connect()
27+
.await?;
28+
let mut client = CoreClient::new(channel);
29+
30+
let request = BlockHeadersWithChainLocksRequest {
31+
count: 0,
32+
from_block: cmd.from_height.map(FromBlock::FromBlockHeight),
33+
};
34+
35+
println!("📡 Subscribing to chain locks at {}", url);
36+
if let Some(height) = cmd.from_height {
37+
println!(
38+
" Requesting history starting from block height {}",
39+
height
40+
);
41+
} else {
42+
println!(" Streaming live chain locks\n");
43+
}
44+
45+
let response = client
46+
.subscribe_to_block_headers_with_chain_locks(request)
47+
.await?;
48+
49+
let mut stream = response.into_inner();
50+
let mut block_header_batches = 0usize;
51+
let mut chain_locks = 0usize;
52+
53+
while let Some(message) = stream.message().await? {
54+
use dapi_grpc::core::v0::block_headers_with_chain_locks_response::Responses;
55+
56+
match message.responses {
57+
Some(Responses::BlockHeaders(headers)) => {
58+
block_header_batches += 1;
59+
let header_count = headers.headers.len();
60+
let total_bytes: usize = headers.headers.iter().map(|h| h.len()).sum();
61+
println!(
62+
"🧱 Received block headers batch #{} ({} header(s), {} bytes)",
63+
block_header_batches, header_count, total_bytes
64+
);
65+
}
66+
Some(Responses::ChainLock(data)) => {
67+
chain_locks += 1;
68+
println!(
69+
"🔒 Received chain lock #{}, payload size {} bytes",
70+
chain_locks,
71+
data.len()
72+
);
73+
}
74+
None => {
75+
warn!("Received empty chain lock response message");
76+
}
77+
}
78+
println!();
79+
}
80+
81+
println!("👋 Chain lock stream ended");
82+
Ok(())
83+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
use ciborium::de::from_reader;
2+
use clap::Args;
3+
use dapi_grpc::core::v0::{core_client::CoreClient, MasternodeListRequest};
4+
use dapi_grpc::tonic::transport::Channel;
5+
use serde::Deserialize;
6+
use serde_json::Value;
7+
use std::io::Cursor;
8+
use tracing::warn;
9+
10+
use crate::error::{CliError, CliResult};
11+
12+
#[derive(Args, Debug)]
13+
pub struct MasternodeCommand {}
14+
15+
pub async fn run(url: &str, _cmd: MasternodeCommand) -> CliResult<()> {
16+
let channel = Channel::from_shared(url.to_string())
17+
.map_err(|source| CliError::InvalidUrl {
18+
url: url.to_string(),
19+
source: Box::new(source),
20+
})?
21+
.connect()
22+
.await?;
23+
24+
let mut client = CoreClient::new(channel);
25+
26+
println!("📡 Subscribing to masternode list updates at {}", url);
27+
28+
let response = client
29+
.subscribe_to_masternode_list(MasternodeListRequest {})
30+
.await?;
31+
32+
let mut stream = response.into_inner();
33+
let mut update_index = 0usize;
34+
35+
while let Some(update) = stream.message().await? {
36+
update_index += 1;
37+
let diff_bytes = update.masternode_list_diff;
38+
39+
println!("🔁 Masternode list update #{}", update_index);
40+
println!(" Diff payload size: {} bytes", diff_bytes.len());
41+
42+
match from_reader::<MasternodeListDiff, _>(Cursor::new(&diff_bytes)) {
43+
Ok(diff) => print_diff_summary(&diff),
44+
Err(err) => {
45+
warn!(error = %err, "Failed to decode masternode diff payload");
46+
println!(" Unable to decode diff payload (see logs for details).\n");
47+
continue;
48+
}
49+
}
50+
51+
println!();
52+
}
53+
54+
println!("👋 Stream ended");
55+
Ok(())
56+
}
57+
58+
fn print_diff_summary(diff: &MasternodeListDiff) {
59+
let base_hash = diff.base_block_hash.as_deref().unwrap_or("<unknown>");
60+
let block_hash = diff.block_hash.as_deref().unwrap_or("<unknown>");
61+
62+
println!(" Base block hash : {}", base_hash);
63+
println!(" Target block hash: {}", block_hash);
64+
65+
let added = diff.added_mns.len();
66+
let updated = diff.updated_mns.len();
67+
let removed = diff.removed_mns.len();
68+
69+
if added > 0 || updated > 0 || removed > 0 {
70+
println!(
71+
" Added: {} | Updated: {} | Removed: {}",
72+
added, updated, removed
73+
);
74+
}
75+
76+
let snapshot = if !diff.full_list.is_empty() {
77+
diff.full_list.len()
78+
} else if !diff.masternode_list.is_empty() {
79+
diff.masternode_list.len()
80+
} else {
81+
0
82+
};
83+
84+
if snapshot > 0 {
85+
println!(" Snapshot size: {} masternodes", snapshot);
86+
}
87+
88+
if let Some(total) = diff.total_mn_count {
89+
println!(" Reported total masternodes: {}", total);
90+
}
91+
92+
let quorum_updates = diff.quorum_diff_updates();
93+
if quorum_updates > 0 {
94+
println!(" Quorum updates: {}", quorum_updates);
95+
}
96+
97+
if added == 0 && updated == 0 && removed == 0 && snapshot == 0 && quorum_updates == 0 {
98+
println!(
99+
" No masternode or quorum changes detected in this diff (metadata update only)."
100+
);
101+
}
102+
}
103+
104+
#[derive(Debug, Deserialize)]
105+
struct MasternodeListDiff {
106+
#[serde(rename = "baseBlockHash")]
107+
base_block_hash: Option<String>,
108+
#[serde(rename = "blockHash")]
109+
block_hash: Option<String>,
110+
#[serde(rename = "addedMNs", default)]
111+
added_mns: Vec<Value>,
112+
#[serde(rename = "updatedMNs", default)]
113+
updated_mns: Vec<Value>,
114+
#[serde(rename = "removedMNs", default)]
115+
removed_mns: Vec<Value>,
116+
#[serde(rename = "mnList", default)]
117+
full_list: Vec<Value>,
118+
#[serde(rename = "masternodeList", default)]
119+
masternode_list: Vec<Value>,
120+
#[serde(rename = "totalMnCount")]
121+
total_mn_count: Option<u64>,
122+
#[serde(rename = "quorumDiffs", default)]
123+
quorum_diffs: Vec<QuorumDiffEntry>,
124+
#[serde(rename = "newQuorums", default)]
125+
new_quorums: Vec<Value>,
126+
#[serde(rename = "deletedQuorums", default)]
127+
deleted_quorums: Vec<Value>,
128+
#[serde(default)]
129+
quorums: Vec<Value>,
130+
}
131+
132+
impl MasternodeListDiff {
133+
fn quorum_diff_updates(&self) -> usize {
134+
let nested: usize = self
135+
.quorum_diffs
136+
.iter()
137+
.map(|entry| entry.quorum_updates())
138+
.sum();
139+
140+
nested + self.new_quorums.len() + self.deleted_quorums.len() + self.quorums.len()
141+
}
142+
}
143+
144+
#[derive(Debug, Deserialize)]
145+
struct QuorumDiffEntry {
146+
#[serde(rename = "newQuorums", default)]
147+
new_quorums: Vec<Value>,
148+
#[serde(rename = "deletedQuorums", default)]
149+
deleted_quorums: Vec<Value>,
150+
#[serde(default)]
151+
quorums: Vec<Value>,
152+
}
153+
154+
impl QuorumDiffEntry {
155+
fn quorum_updates(&self) -> usize {
156+
self.new_quorums.len() + self.deleted_quorums.len() + self.quorums.len()
157+
}
158+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use clap::Args;
2+
use dapi_grpc::core::v0::{
3+
core_client::CoreClient, get_masternode_status_response::Status as GrpcStatus,
4+
GetMasternodeStatusRequest,
5+
};
6+
use dapi_grpc::tonic::transport::Channel;
7+
8+
use crate::error::{CliError, CliResult};
9+
10+
#[derive(Args, Debug)]
11+
pub struct MasternodeStatusCommand {}
12+
13+
pub async fn run(url: &str, _cmd: MasternodeStatusCommand) -> CliResult<()> {
14+
let channel = Channel::from_shared(url.to_string())
15+
.map_err(|source| CliError::InvalidUrl {
16+
url: url.to_string(),
17+
source: Box::new(source),
18+
})?
19+
.connect()
20+
.await?;
21+
22+
let mut client = CoreClient::new(channel);
23+
24+
let response = client
25+
.get_masternode_status(GetMasternodeStatusRequest {})
26+
.await?
27+
.into_inner();
28+
29+
let status = GrpcStatus::try_from(response.status).unwrap_or(GrpcStatus::Unknown);
30+
let pro_tx_hash = if response.pro_tx_hash.is_empty() {
31+
"<unset>".to_string()
32+
} else {
33+
hex::encode(response.pro_tx_hash)
34+
};
35+
36+
println!("Masternode status via {}", url);
37+
println!("Status : {}", human_status(status));
38+
println!("ProTx Hash : {}", pro_tx_hash);
39+
println!("PoSe Penalty : {}", response.pose_penalty);
40+
println!("Core Synced : {}", yes_no(response.is_synced));
41+
println!("Sync Progress : {:.2}%", response.sync_progress * 100.0);
42+
43+
Ok(())
44+
}
45+
46+
fn human_status(status: GrpcStatus) -> &'static str {
47+
match status {
48+
GrpcStatus::Unknown => "Unknown",
49+
GrpcStatus::WaitingForProtx => "Waiting for ProTx",
50+
GrpcStatus::PoseBanned => "PoSe banned",
51+
GrpcStatus::Removed => "Removed",
52+
GrpcStatus::OperatorKeyChanged => "Operator key changed",
53+
GrpcStatus::ProtxIpChanged => "ProTx IP changed",
54+
GrpcStatus::Ready => "Ready",
55+
GrpcStatus::Error => "Error",
56+
}
57+
}
58+
59+
fn yes_no(flag: bool) -> &'static str {
60+
if flag {
61+
"yes"
62+
} else {
63+
"no"
64+
}
65+
}

0 commit comments

Comments
 (0)