Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI status command #214

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
13 changes: 12 additions & 1 deletion trustchain-cli/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use trustchain_api::{
api::{TrustchainDIDAPI, TrustchainDataAPI, TrustchainVCAPI},
TrustchainAPI,
};
use trustchain_cli::config::cli_config;
use trustchain_cli::{config::cli_config, print_status};
use trustchain_core::{
utils::extract_keys,
vc::{CredentialError, DataCredentialError},
Expand Down Expand Up @@ -44,6 +44,13 @@ fn cli() -> Command {
.subcommand_required(true)
.arg_required_else_help(true)
.allow_external_subcommands(true)
.subcommand(
Command::new("status")
.about("Trustchain node status.")
.subcommand_required(false)
.arg_required_else_help(false)
.allow_external_subcommands(false)
)
.subcommand(
Command::new("did")
.about("DID functionality: create, attest, resolve, verify.")
Expand Down Expand Up @@ -189,6 +196,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let resolver = verifier.resolver();
let mut context_loader = ContextLoader::default();
match matches.subcommand() {
Some(("status", _)) => {
// TODO: update the trustchain-api crate.
thobson88 marked this conversation as resolved.
Show resolved Hide resolved
print_status().await;
}
Some(("did", sub_matches)) => {
match sub_matches.subcommand() {
Some(("create", sub_matches)) => {
Expand Down
62 changes: 62 additions & 0 deletions trustchain-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,63 @@
use config::cli_config;
use trustchain_ion::{
utils::{bitcoind_status, ion_ok, ipfs_ok, mongodb_ok, BitcoindStatus},
TrustchainBitcoinError,
};

pub mod config;

/// Prints the current status of ION and its dependencies.
pub async fn print_status() {
let bitcoind_status = bitcoind_status().await;
let mut is_mainnet = false;
if let BitcoindStatus::Ok(x) = bitcoind_status {
Copy link
Collaborator

Choose a reason for hiding this comment

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

As discussed could also check if the bitcoin network running matches the CLI's ION network with something like:

        use trustchain_ion::ion::IONTest as ION;
        if (ION::NETWORK == "" && !is_mainnet) || (ION::NETWORK == "test" && is_mainnet) {
            println!("Wrong bitcoin network running: {}");
        }

is_mainnet = x;
}

let str = "IPFS......... ".to_string();
let msg = Some("IPFS daemon not found");
println!("{}", status_str(str, ipfs_ok().await, msg));

let str = "MongoDB...... ".to_string();
let msg = Some("Mongo daemon not found");
println!("{}", status_str(str, mongodb_ok(is_mainnet).await, msg));

let str = "Bitcoin...... ".to_string();
match bitcoind_status {
BitcoindStatus::Ok(_) => {
println!("{}", status_str(str, true, None));
}
BitcoindStatus::Synching(blocks, headers) => {
let msg = Some(format!("Synching blocks: {}/{}", blocks, headers));
println!("{}", status_str(str, false, msg.as_deref()));
}
BitcoindStatus::Error(e) => {
let msg = match e {
err @ TrustchainBitcoinError::BitcoinCoreRPCError(_) => err.to_string(),
_ => "Bitcoin RPC returned an error".to_string(),
};
println!("{}", status_str(str, false, Some(&msg)));
}
};

let str = "ION.......... ".to_string();
let msg = Some("ION DID resolution attempt failed");
let is_ok = ion_ok(is_mainnet, cli_config().ion_endpoint.port).await;
println!("{}", status_str(str, is_ok, msg));

// TODO: check trustchain-http server status (report only if positive).
}

pub fn status_str(mut str: String, is_ok: bool, details: Option<&str>) -> String {
if is_ok {
str.push_str("✅");
return str;
}
str.push_str("❌");
if let Some(detail) = details {
str.push_str(" [");
str.push_str(detail);
str.push_str("]");
}
str
}
13 changes: 13 additions & 0 deletions trustchain-ion/src/data.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
//! Test fixtures for crate.
#![allow(dead_code)]

pub(crate) const SAMPLE_CID: &str = "QmRvgZm4J3JSxfk4wRjE2u2Hi2U7VmobYnpqhqH5QP6J97";
pub(crate) const SAMPLE_DID_TESTNET: &str =
"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg";
pub(crate) const SAMPLE_DID_MAINNET: &str =
"did:ion:EiClkZMDxPKqC9c-umQfTkR8vvZ9JPhl_xLDI9Nfk38w5w";

pub fn sample_did(is_mainnet: bool) -> String {
match is_mainnet {
true => SAMPLE_DID_MAINNET.to_string(),
false => SAMPLE_DID_TESTNET.to_string(),
}
}

// Note on test fixtures:
//
// This file contains samples of content from the three ION file types written to IPFS:
Expand Down
67 changes: 64 additions & 3 deletions trustchain-ion/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! ION-related utilities.
use crate::data::{sample_did, SAMPLE_CID};
use crate::{
config::ion_config, MONGO_FILTER_OP_INDEX, MONGO_FILTER_TXN_NUMBER, MONGO_FILTER_TXN_TIME,
};
use bitcoin::{BlockHash, BlockHeader, Transaction};
use bitcoincore_rpc::json::GetBlockchainInfoResult;
use bitcoincore_rpc::{bitcoincore_rpc_json::BlockStatsFields, RpcApi};
use chrono::NaiveDate;
use flate2::read::GzDecoder;
Expand All @@ -12,12 +14,14 @@ use mongodb::{bson::doc, options::ClientOptions, Cursor};
use serde_json::{json, Value};
use std::io::Read;
use std::{cmp::Ordering, collections::HashMap};
use trustchain_core::resolver::TrustchainResolver;
use trustchain_core::{utils::get_did_suffix, verifier::VerifierError};

use crate::{
TrustchainBitcoinError, TrustchainIpfsError, TrustchainMongodbError, BITS_KEY,
HASH_PREV_BLOCK_KEY, MERKLE_ROOT_KEY, MONGO_COLLECTION_OPERATIONS, MONGO_CREATE_OPERATION,
MONGO_FILTER_DID_SUFFIX, MONGO_FILTER_TYPE, NONCE_KEY, TIMESTAMP_KEY, VERSION_KEY,
trustchain_resolver, TrustchainBitcoinError, TrustchainIpfsError, TrustchainMongodbError,
BITS_KEY, HASH_PREV_BLOCK_KEY, MERKLE_ROOT_KEY, MONGO_COLLECTION_OPERATIONS,
MONGO_CREATE_OPERATION, MONGO_FILTER_DID_SUFFIX, MONGO_FILTER_TYPE, NONCE_KEY, TIMESTAMP_KEY,
VERSION_KEY,
};

const ION_METHOD_WITH_DELIMITER: &str = "ion:";
Expand Down Expand Up @@ -169,6 +173,18 @@ pub fn rpc_client() -> bitcoincore_rpc::Client {
.unwrap()
}

/// Gets Bitcoin blockchain info via the RPC API.
pub fn blockchain_info(
client: Option<&bitcoincore_rpc::Client>,
) -> Result<GetBlockchainInfoResult, TrustchainBitcoinError> {
// If necessary, construct a Bitcoin RPC client to communicate with the ION Bitcoin node.
if client.is_none() {
let rpc_client = rpc_client();
return blockchain_info(Some(&rpc_client));
};
Ok(client.unwrap().get_blockchain_info()?)
}

/// Gets a Bitcoin block header via the RPC API.
pub fn block_header(
block_hash: &BlockHash,
Expand Down Expand Up @@ -393,6 +409,45 @@ pub fn block_height_range_on_date(
Ok((first_block, last_block))
}

#[derive(Debug)]
pub enum BitcoindStatus {
Ok(bool),
thobson88 marked this conversation as resolved.
Show resolved Hide resolved
Synching(u64, u64),
Error(TrustchainBitcoinError),
}

/// Returns the current status of bitcoind.
pub async fn bitcoind_status() -> BitcoindStatus {
let info = blockchain_info(None);
if info.is_err() {
return BitcoindStatus::Error(info.err().unwrap());
}
let info = info.unwrap();
if info.blocks == info.headers {
return BitcoindStatus::Ok(info.chain == "main");
}
BitcoindStatus::Synching(info.blocks, info.headers)
}

/// Returns true if the IPFS daemon is running on the expected port.
pub async fn ipfs_ok() -> bool {
query_ipfs(SAMPLE_CID, &IpfsClient::default()).await.is_ok()
}

/// Returns true if the MongoDB daemon is running on the expected port.
pub async fn mongodb_ok(is_mainnet: bool) -> bool {
query_mongodb(get_did_suffix(&sample_did(is_mainnet)))
.await
.is_ok()
}

/// Returns true if the ION Core microservice is running on the expected port.
pub async fn ion_ok(is_mainnet: bool, ion_port: u16) -> bool {
let resolver = trustchain_resolver(&format!("http://localhost:{}/", ion_port));
let result = resolver.resolve_as_result(&sample_did(is_mainnet)).await;
result.is_ok()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -747,4 +802,10 @@ mod tests {
// The last testnet block mined on 2022-10-20 (UTC) was at height 2377519.
assert_eq!(result, (2377360, 2377519));
}

#[tokio::test]
#[ignore = "Integration test requires Bitcoin"]
async fn test_bitcoind_status() {
let _ = bitcoind_status().await;
}
}
Loading