Skip to content

Commit

Permalink
Showing 7 changed files with 314 additions and 148 deletions.
8 changes: 3 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -18,11 +18,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3

- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable

- name: Template tests config file
run: |
envsubst < crates/ash_sdk/tests/conf/quicknode.yml > ${{ runner.temp }}/ash-test-avax-conf.yml
@@ -37,6 +32,9 @@ jobs:
- name: Start a local Avalanche network
run: ~/bin/avalanche network start

- name: Setup cache for Rust
uses: Swatinem/rust-cache@v2

- name: Run cargo check
uses: actions-rs/cargo@v1
with:
2 changes: 1 addition & 1 deletion crates/ash_cli/src/avalanche/network.rs
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ pub(crate) struct NetworkCommand {

#[derive(Subcommand)]
enum NetworkSubcommands {
#[command(about = "List Avalanche networks")]
#[command(about = "List known Avalanche networks")]
List,
}

64 changes: 49 additions & 15 deletions crates/ash_cli/src/avalanche/node.rs
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@

// Module that contains the node subcommand parser

use crate::utils::{error::CliError, templating::template_avalanche_node_info};
use crate::utils::{error::CliError, templating::*};
use ash_sdk::avalanche::nodes::AvalancheNode;
use clap::{Parser, Subcommand};

@@ -12,20 +12,25 @@ use clap::{Parser, Subcommand};
pub(crate) struct NodeCommand {
#[command(subcommand)]
command: NodeSubcommands,
#[arg(
long,
default_value = "127.0.0.1",
help = "Node's HTTP host (IP address or FQDN)",
global = true
)]
http_host: String,
#[arg(long, default_value = "9650", help = "Node's HTTP port", global = true)]
http_port: u16,
}

#[derive(Subcommand)]
enum NodeSubcommands {
#[command(about = "Show Avalanche node information")]
Info {
#[arg(
long,
default_value = "127.0.0.1",
help = "Node's HTTP host (IP address or FQDN)"
)]
http_host: String,
#[arg(long, default_value = "9650", help = "Node's HTTP port")]
http_port: u16,
#[command(about = "Show node information")]
Info,
#[command(about = "Check if a chain is done bootstrapping on the node")]
IsBootstrapped {
#[arg(long, help = "Chain ID or alias")]
chain: String,
},
}

@@ -56,12 +61,41 @@ fn info(http_host: &str, http_port: u16, json: bool) -> Result<(), CliError> {
Ok(())
}

fn is_bootstrapped(
http_host: &str,
http_port: u16,
chain: &str,
json: bool,
) -> Result<(), CliError> {
let node = AvalancheNode {
http_host: http_host.to_string(),
http_port,
..Default::default()
};

let is_bootstrapped = node
.check_chain_bootstrapping(chain)
.map_err(|e| CliError::dataerr(format!("Error checking if chain is bootstrapped: {e}")))?;

if json {
println!("{}", serde_json::to_string(&is_bootstrapped).unwrap());
return Ok(());
}

println!(
"{}",
template_chain_is_bootstrapped(&node, chain, is_bootstrapped, 0)
);

Ok(())
}

// Parse node subcommand
pub(crate) fn parse(node: NodeCommand, json: bool) -> Result<(), CliError> {
match node.command {
NodeSubcommands::Info {
http_host,
http_port,
} => info(&http_host, http_port, json),
NodeSubcommands::Info => info(&node.http_host, node.http_port, json),
NodeSubcommands::IsBootstrapped { chain } => {
is_bootstrapped(&node.http_host, node.http_port, &chain, json)
}
}
}
39 changes: 31 additions & 8 deletions crates/ash_cli/src/utils/templating.rs
Original file line number Diff line number Diff line change
@@ -24,10 +24,10 @@ where
T: std::fmt::Display,
{
match type_of(var).split(':').last().unwrap() {
"String" => var.to_string().yellow(),
"String" | "&&str" => var.to_string().yellow(),
"&u64" | "&u32" | "&u16" | "&u8" | "&usize" => var.to_string().cyan(),
"&i64" | "&i32" | "&i16" | "&i8" | "&isize" => var.to_string().cyan(),
"&f64" | "&f32" => var.to_string().magenta(),
"&f64" | "&f32" | "IpAddr" => var.to_string().magenta(),
"&bool" => var.to_string().blue(),
"Id" => var.to_string().green(),
_ => var.to_string().bright_white(),
@@ -152,8 +152,7 @@ pub(crate) fn template_validator_info(
} else {
info.push_str(&formatdoc!(
"
- {}
",
- {}",
type_colorize(&validator.node_id),
));
}
@@ -254,8 +253,9 @@ pub(crate) fn template_avalanche_node_info(node: &AvalancheNode, indent: u8) ->
"
Node '{}:{}':
ID: {}
Network: {}
Public IP: {}
Stacking port: {}
Staking port: {}
Versions:
AvalancheGo: {}
Database: {}
@@ -264,12 +264,13 @@ pub(crate) fn template_avalanche_node_info(node: &AvalancheNode, indent: u8) ->
AVM: {}
EVM: {}
PlatformVM: {}
Uptime:
Rewarding stake: {}%
Weighted average: {}%",
Uptime:
Rewarding stake: {}%
Weighted average: {}%",
type_colorize(&node.http_host),
type_colorize(&node.http_port),
type_colorize(&node.id),
type_colorize(&node.network),
type_colorize(&node.public_ip),
type_colorize(&node.staking_port),
type_colorize(&node.versions.avalanchego_version),
@@ -284,3 +285,25 @@ pub(crate) fn template_avalanche_node_info(node: &AvalancheNode, indent: u8) ->

indent::indent_all_by(indent.into(), info)
}

pub(crate) fn template_chain_is_bootstrapped(
node: &AvalancheNode,
chain: &str,
is_bootstrapped: bool,
indent: u8,
) -> String {
let mut info = String::new();

info.push_str(&formatdoc!(
"Chain '{}' on node '{}:{}': {}",
type_colorize(&chain),
type_colorize(&node.http_host),
type_colorize(&node.http_port),
match is_bootstrapped {
true => "Bootstrapped ✓".green(),
false => "Not yet bootstrapped ✗".red(),
}
));

indent::indent_all_by(indent.into(), info)
}
167 changes: 80 additions & 87 deletions crates/ash_sdk/src/avalanche/jsonrpc/info.rs
Original file line number Diff line number Diff line change
@@ -3,116 +3,109 @@

// Module that contains code to interact with Avalanche Info API

use crate::avalanche::nodes::{AvalancheNodeUptime, AvalancheNodeVersions};
use avalanche_types::{ids::node::Id, jsonrpc::info::*};
use serde::Deserialize;
use serde_aux::prelude::*;
use crate::{
avalanche::jsonrpc::{get_json_rpc_req_result, JsonRpcResponse},
avalanche::nodes::{AvalancheNodeUptime, AvalancheNodeVersions},
errors::*,
impl_json_rpc_response,
};
use avalanche_types::{
ids::node::Id,
jsonrpc::{info::*, ResponseError},
};
use std::net::SocketAddr;

/// Info API endpoint
pub const AVAX_INFO_API_ENDPOINT: &str = "ext/info";

#[derive(Deserialize)]
#[allow(dead_code)]
struct InfoApiGetNodeIdResponse {
jsonrpc: String,
#[serde(deserialize_with = "deserialize_number_from_string")]
id: u8,
result: InfoApiGetNodeIdResult,
impl_json_rpc_response!(GetNodeIdResponse, GetNodeIdResult);
impl_json_rpc_response!(GetNodeIpResponse, GetNodeIpResult);
impl_json_rpc_response!(GetNodeVersionResponse, GetNodeVersionResult);
impl_json_rpc_response!(UptimeResponse, UptimeResult);
impl_json_rpc_response!(GetNetworkNameResponse, GetNetworkNameResult);
impl_json_rpc_response!(IsBootstrappedResponse, IsBootstrappedResult);

/// Get the ID of a node by querying the Info API
pub fn get_node_id(rpc_url: &str) -> Result<Id, RpcError> {
let node_id = get_json_rpc_req_result::<GetNodeIdResponse, GetNodeIdResult>(
rpc_url,
"info.getNodeID",
None,
)?
.node_id;

Ok(node_id)
}

#[derive(Deserialize)]
struct InfoApiGetNodeIdResult {
#[serde(rename = "nodeID")]
node_id: Id,
}
/// Get the IP of a node by querying the Info API
pub fn get_node_ip(rpc_url: &str) -> Result<SocketAddr, RpcError> {
let ip = get_json_rpc_req_result::<GetNodeIpResponse, GetNodeIpResult>(
rpc_url,
"info.getNodeIP",
None,
)?
.ip;

#[derive(Deserialize)]
#[allow(dead_code)]
struct InfoApiGetNodeIpResponse {
jsonrpc: String,
#[serde(deserialize_with = "deserialize_number_from_string")]
id: u8,
result: InfoApiGetNodeIpResult,
Ok(ip)
}

#[derive(Deserialize)]
struct InfoApiGetNodeIpResult {
ip: String,
}
/// Get the version of a node by querying the Info API
pub fn get_node_version(rpc_url: &str) -> Result<AvalancheNodeVersions, RpcError> {
let node_version = get_json_rpc_req_result::<GetNodeVersionResponse, GetNodeVersionResult>(
rpc_url,
"info.getNodeVersion",
None,
)?
.into();

// Get the ID of a node by querying the Info API
pub fn get_node_id(rpc_url: &str) -> Result<Id, ureq::Error> {
let resp: InfoApiGetNodeIdResponse = ureq::post(rpc_url)
.send_json(ureq::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "info.getNodeID",
"params": {}
}))?
.into_json()?;

Ok(resp.result.node_id)
Ok(node_version)
}

// Get the IP of a node by querying the Info API
pub fn get_node_ip(rpc_url: &str) -> Result<String, ureq::Error> {
let resp: InfoApiGetNodeIpResponse = ureq::post(rpc_url)
.send_json(ureq::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "info.getNodeIP",
"params": {}
}))?
.into_json()?;

Ok(resp.result.ip)
/// Get the uptime of a node by querying the Info API
pub fn get_node_uptime(rpc_url: &str) -> Result<AvalancheNodeUptime, RpcError> {
let uptime =
get_json_rpc_req_result::<UptimeResponse, UptimeResult>(rpc_url, "info.uptime", None)?
.into();

Ok(uptime)
}

// Get the version of a node by querying the Info API
pub fn get_node_version(rpc_url: &str) -> Result<AvalancheNodeVersions, ureq::Error> {
let resp: GetNodeVersionResponse = ureq::post(rpc_url)
.send_json(ureq::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "info.getNodeVersion",
"params": {}
}))?
.into_json()?;

let node_version = resp.result.unwrap();
Ok(AvalancheNodeVersions {
avalanchego_version: node_version.version,
database_version: node_version.database_version,
git_commit: node_version.git_commit,
vm_versions: node_version.vm_versions,
})
/// Get the name of the network a node is participating in by querying the Info API
pub fn get_network_name(rpc_url: &str) -> Result<String, RpcError> {
let network_name = get_json_rpc_req_result::<GetNetworkNameResponse, GetNetworkNameResult>(
rpc_url,
"info.getNetworkName",
None,
)?
.network_name;

Ok(network_name)
}

// Get the uptime of a node by querying the Info API
pub fn get_node_uptime(rpc_url: &str) -> Result<AvalancheNodeUptime, ureq::Error> {
let resp: UptimeResponse = ureq::post(rpc_url)
.send_json(ureq::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "info.uptime",
"params": {}
}))?
.into_json()?;

let node_uptime = resp.result.unwrap();
Ok(AvalancheNodeUptime {
rewarding_stake_percentage: node_uptime.rewarding_stake_percentage,
weighted_average_percentage: node_uptime.weighted_average_percentage,
})
/// Check if a given chain is done boostrapping by querying the Info API
/// `chain` is the chain ID or alias of the chain to check
pub fn is_bootstrapped(rpc_url: &str, chain: &str) -> Result<bool, RpcError> {
let is_bootstrapped = get_json_rpc_req_result::<IsBootstrappedResponse, IsBootstrappedResult>(
rpc_url,
"info.isBootstrapped",
Some(ureq::json!({
"chain": chain.to_string()
})),
)?
.is_bootstrapped;

Ok(is_bootstrapped)
}

#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};

use super::*;
use avalanche_types::jsonrpc::info::VmVersions;

// Using avalanche-network-runner to run a test network
const ASH_TEST_HTTP_HOST: &str = "127.0.0.1";
const ASH_TEST_HTTP_HOST: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const ASH_TEST_HTTP_PORT: u16 = 9650;
const ASH_TEST_STACKING_PORT: u16 = 9651;
const ASH_TEST_NODE_ID: &str = "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg";
@@ -138,7 +131,7 @@ mod tests {
let node_ip = get_node_ip(&rpc_url).unwrap();
assert_eq!(
node_ip,
format!("{ASH_TEST_HTTP_HOST}:{ASH_TEST_STACKING_PORT}")
SocketAddr::new(ASH_TEST_HTTP_HOST, ASH_TEST_STACKING_PORT)
);
}

6 changes: 3 additions & 3 deletions crates/ash_sdk/src/avalanche/jsonrpc/platformvm.rs
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ impl_json_rpc_response!(GetSubnetsResponse, GetSubnetsResult);
impl_json_rpc_response!(GetBlockchainsResponse, GetBlockchainsResult);
impl_json_rpc_response!(GetCurrentValidatorsResponse, GetCurrentValidatorsResult);

// Get the Subnets of the network by querying the P-Chain API
/// Get the Subnets of the network by querying the P-Chain API
pub fn get_network_subnets(
rpc_url: &str,
network_name: &str,
@@ -50,7 +50,7 @@ pub fn get_network_subnets(
Ok(network_subnets)
}

// Get the blockchains of the network by querying the P-Chain API
/// Get the blockchains of the network by querying the P-Chain API
pub fn get_network_blockchains(
rpc_url: &str,
network_name: &str,
@@ -73,7 +73,7 @@ pub fn get_network_blockchains(
Ok(network_blockchains)
}

// Get the current validators of a Subnet by querying the P-Chain API
/// Get the current validators of a Subnet by querying the P-Chain API
pub fn get_current_validators(
rpc_url: &str,
subnet_id: &str,
176 changes: 147 additions & 29 deletions crates/ash_sdk/src/avalanche/nodes.rs
Original file line number Diff line number Diff line change
@@ -4,40 +4,40 @@
// Module that contains code to interact with Avalanche nodes

use crate::{avalanche::jsonrpc::info::*, errors::*};
use avalanche_types::{ids::node::Id, jsonrpc::info::VmVersions};
use avalanche_types::{
ids::node::Id,
jsonrpc::info::{GetNodeVersionResult, UptimeResult, VmVersions},
};
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr};

/// Avalanche node
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AvalancheNode {
pub id: Id,
pub network: String,
pub http_host: String,
pub http_port: u16,
pub public_ip: String,
pub public_ip: IpAddr,
pub staking_port: u16,
pub versions: AvalancheNodeVersions,
pub uptime: AvalancheNodeUptime,
}

/// Avalanche node version
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AvalancheNodeVersions {
pub avalanchego_version: String,
pub database_version: String,
pub git_commit: String,
pub vm_versions: VmVersions,
// Not yet implemented in avalanche_types
// pub rpc_protocol_version: String,
}

/// Avalanche node uptime
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AvalancheNodeUptime {
pub rewarding_stake_percentage: f64,
pub weighted_average_percentage: f64,
impl Default for AvalancheNode {
fn default() -> Self {
Self {
id: Id::default(),
http_host: String::from("127.0.0.1"),
http_port: 9650,
public_ip: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
staking_port: 9651,
versions: AvalancheNodeVersions::default(),
uptime: AvalancheNodeUptime::default(),
network: String::from("local"),
}
}
}

impl AvalancheNode {
@@ -60,36 +60,133 @@ impl AvalancheNode {
target_value: node_host.to_string(),
msg: e.to_string(),
})?;
let node_ip_split: Vec<&str> = node_ip.split(':').collect();
self.public_ip = node_ip_split[0].to_string();
self.staking_port = node_ip_split[1].parse().unwrap();
self.public_ip = node_ip.ip();
self.staking_port = node_ip.port();

self.versions = get_node_version(&api_path).map_err(|e| RpcError::GetFailure {
data_type: "version".to_string(),
target_type: "node".to_string(),
target_value: node_host.to_string(),
msg: e.to_string(),
})?;
self.uptime = get_node_uptime(&api_path).map_err(|e| RpcError::GetFailure {
data_type: "uptime".to_string(),

self.network = get_network_name(&api_path).map_err(|e| RpcError::GetFailure {
data_type: "network".to_string(),
target_type: "node".to_string(),
target_value: node_host.to_string(),
msg: e.to_string(),
})?;

// If the node is not a validator, the `info.uptime` method will return an error
// This should not get in the way of the node's information update
let uptime = get_node_uptime(&api_path);
match uptime {
Ok(uptime) => self.uptime = uptime,
Err(e) => match e {
RpcError::ResponseError {
code,
message,
data,
} => {
if code == -32000 && message.contains("node is not a validator") {
self.uptime = AvalancheNodeUptime::default();
} else {
return Err(AshError::RpcError(RpcError::GetFailure {
data_type: "uptime".to_string(),
target_type: "node".to_string(),
target_value: node_host,
msg: format!(
"{:?}",
RpcError::ResponseError {
code,
message,
data,
}
),
}));
}
}
_ => {
return Err(AshError::RpcError(RpcError::GetFailure {
data_type: "uptime".to_string(),
target_type: "node".to_string(),
target_value: node_host,
msg: e.to_string(),
}));
}
},
}

Ok(())
}

/// Check whether a given chain is done bootstrapping
pub fn check_chain_bootstrapping(&self, chain: &str) -> Result<bool, AshError> {
let node_host = format!("{}:{}", self.http_host, self.http_port);
let api_path = format!("http://{node_host}/{AVAX_INFO_API_ENDPOINT}",);

let is_bootstrapped =
is_bootstrapped(&api_path, chain).map_err(|e| RpcError::GetFailure {
data_type: format!("{} chain bootstrapping", chain),
target_type: "node".to_string(),
target_value: node_host.to_string(),
msg: e.to_string(),
})?;

Ok(is_bootstrapped)
}
}

/// Avalanche node version
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AvalancheNodeVersions {
pub avalanchego_version: String,
pub database_version: String,
pub git_commit: String,
pub vm_versions: VmVersions,
// Not yet implemented in avalanche_types
// pub rpc_protocol_version: String,
}

impl From<GetNodeVersionResult> for AvalancheNodeVersions {
fn from(node_version: GetNodeVersionResult) -> Self {
Self {
avalanchego_version: node_version.version,
database_version: node_version.database_version,
git_commit: node_version.git_commit,
vm_versions: node_version.vm_versions,
}
}
}

/// Avalanche node uptime
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AvalancheNodeUptime {
pub rewarding_stake_percentage: f64,
pub weighted_average_percentage: f64,
}

impl From<UptimeResult> for AvalancheNodeUptime {
fn from(node_uptime: UptimeResult) -> Self {
Self {
rewarding_stake_percentage: node_uptime.rewarding_stake_percentage,
weighted_average_percentage: node_uptime.weighted_average_percentage,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

// Using avalanche-network-runner to run a test network
const ASH_TEST_HTTP_HOST: &str = "127.0.0.1";
const ASH_TEST_HTTP_PORT: u16 = 9650;
const ASH_TEST_HTTP_HOST: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
const ASH_TEST_STACKING_PORT: u16 = 9651;
const ASH_TEST_NODE_ID: &str = "NodeID-7Xhw2mDxuDS44j42TCB6U5579esbSt3Lg";
const ASH_TEST_NETWORK_NAME: &str = "network-1337";

#[test]
#[ignore]
@@ -101,13 +198,14 @@ mod tests {
};

// Test that the node has the right http_host and http_port
assert_eq!(node.http_host, ASH_TEST_HTTP_HOST);
assert_eq!(node.http_host, ASH_TEST_HTTP_HOST.to_string());
assert_eq!(node.http_port, ASH_TEST_HTTP_PORT);

node.update_info().unwrap();

// Test the node id, public_ip and stacking_port
// Test the node ID, network, public_ip and stacking_port
assert_eq!(node.id.to_string(), ASH_TEST_NODE_ID);
assert_eq!(node.network, ASH_TEST_NETWORK_NAME);
assert_eq!(node.public_ip, ASH_TEST_HTTP_HOST);
assert_eq!(node.staking_port, ASH_TEST_STACKING_PORT);

@@ -121,4 +219,24 @@ mod tests {
assert_ne!(node.uptime.rewarding_stake_percentage, 0.0);
assert_ne!(node.uptime.weighted_average_percentage, 0.0);
}

#[test]
#[ignore]
fn test_avalanche_node_chain_bootstrapping() {
let node = AvalancheNode {
http_host: ASH_TEST_HTTP_HOST.to_string(),
http_port: ASH_TEST_HTTP_PORT,
..Default::default()
};

// Get the node's bootstrapping status for the P, X and C chains
let is_bootstrapped_p = node.check_chain_bootstrapping("P").unwrap();
let is_bootstrapped_x = node.check_chain_bootstrapping("X").unwrap();
let is_bootstrapped_c = node.check_chain_bootstrapping("C").unwrap();

// Test that the node is bootstrapped for the P, X and C chains
assert!(is_bootstrapped_p);
assert!(is_bootstrapped_x);
assert!(is_bootstrapped_c);
}
}

0 comments on commit 2657c59

Please sign in to comment.