--- /dev/null
+def main [host: string, destination: string, ssh_key: path] {
+ let ssh_args = [-i $ssh_key $host]
+ let scp_args = [-r -i $ssh_key]
+ let target = "x86_64-unknown-linux-musl"
+ let app_name = "staking_watchdog"
+ let build = "debug" # "debug" or "release".
+
+ def invoke_ssh [command: string] {
+ let args = $ssh_args ++ $command
+ print $"Executing: ssh ($args)"
+ ssh $args
+ }
+
+ def copy_ssh [source: string, destination: string] {
+ let args = $scp_args ++ [$source $"($host):($destination)"]
+ print $"Executing: scp ($args)"
+ scp $args
+ }
+
+ # Don't know how to dynamically pass variable arguments.
+ if $build == "release" {
+ cargo build --target $target --release
+ } else {
+ cargo build --target $target
+ }
+
+ # invoke_ssh [sudo systemctl stop $app_name]
+ copy_ssh ./target/($target)/($build)/($app_name) $destination
+ invoke_ssh $"chmod u+x ($destination)/($app_name)"
+ # invoke_ssh [sudo systemctl start $app_name]
+ print "Deployment finished"
+}
\ No newline at end of file
--- /dev/null
+/*
+ * API Reference: https://ethereum.github.io/beacon-APIs/
+ */
+
+#![cfg_attr(debug_assertions, allow(unused_variables, unused_imports, dead_code))]
+
+use std::{
+ fs,
+ net::{IpAddr, Ipv4Addr},
+ thread,
+ time::{self, Duration},
+};
+
+use anyhow::{Context, Result};
+use reqwest::StatusCode;
+use serde::Deserialize;
+use serde_json::{json, Value};
+
+use crate::config::Config;
+
+mod config;
+// mod error;
+
+const FILE_CONF: &str = "config.ron";
+const CHECK_PERIOD: Duration = Duration::from_secs(5); // 5s.
+const EMAIL_RESEND_PERIOD: Duration = Duration::from_secs(12 * 60 * 60); // 12h.
+const BASE_URI: &str = "http://localhost:5052/eth/v1/";
+
+fn main() -> Result<()> {
+ println!("Staking Watchdog");
+
+ let config = Config::read(FILE_CONF)?;
+
+ println!("Configuration: {:?}", config);
+
+ let mut time_last_email_send = time::Instant::now() - EMAIL_RESEND_PERIOD;
+
+ loop {
+ let time_beginning_loop = time::Instant::now();
+
+ if let Err(error) = check_validators(&config.pub_keys) {
+ println!("Error: {:?}", error);
+ if time::Instant::now() - time_last_email_send >= EMAIL_RESEND_PERIOD {
+ // Send e-mail.
+ println!("Sending email...");
+
+ time_last_email_send = time::Instant::now();
+ }
+ }
+
+ let elapsed = time::Instant::now() - time_beginning_loop;
+
+ if elapsed < CHECK_PERIOD {
+ let to_wait = CHECK_PERIOD - elapsed;
+ thread::sleep(to_wait);
+ }
+ }
+}
+
+#[derive(Debug)]
+enum CheckError {
+ HttpError(String),
+ NotSync,
+ InvalidSyncStatus,
+ NodeHavingIssues,
+ UnknownCodeFromHealthCheck(u16),
+ ReqwestError(reqwest::Error),
+ ValidatorError { pub_key: String, message: String },
+ ValidatorStatusError { pub_key: String, message: String },
+}
+
+impl From<reqwest::Error> for CheckError {
+ fn from(value: reqwest::Error) -> Self {
+ CheckError::ReqwestError(value)
+ }
+}
+
+#[derive(Deserialize, Debug)]
+struct JsonValidatorState {
+ data: JsonValidatorStateData,
+}
+
+#[derive(Deserialize, Debug)]
+struct JsonValidatorStateData {
+ status: String,
+}
+
+#[derive(Deserialize, Debug)]
+struct JsonError {
+ code: u16,
+ message: String,
+}
+
+fn check_validators(pub_keys: &[String]) -> std::result::Result<(), CheckError> {
+ let url = BASE_URI;
+ let client = reqwest::blocking::Client::new();
+
+ let request_health = client
+ .get(format!("{url}node/health"))
+ .header("accept", "application/json");
+ match request_health.send() {
+ Ok(resp) => {
+ println!("{resp:?}");
+ match resp.status().as_u16() {
+ 200 => (),
+ 206 => return Err(CheckError::NotSync),
+ 400 => return Err(CheckError::InvalidSyncStatus),
+ 503 => return Err(CheckError::NodeHavingIssues),
+ code => return Err(CheckError::UnknownCodeFromHealthCheck(code)),
+ }
+ }
+ Err(error) => {
+ println!("{error:?}");
+ return Err(CheckError::HttpError(error.to_string()));
+ }
+ }
+
+ return Err(CheckError::NotSync);
+
+ for pub_key in pub_keys {
+ let request = client
+ .get(format!("{url}beacon/states/head/validators/0x{pub_key}"))
+ .header("accept", "application/json");
+ match request.send() {
+ Ok(resp) => {
+ println!("{resp:?}");
+ match resp.status().as_u16() {
+ 200 => {
+ let json: JsonValidatorState = resp.json()?;
+ // println!("JSON:\n{:?}", json); // For Debug.
+ if json.data.status != "active_ongoing" {
+ return Err(CheckError::ValidatorStatusError {
+ pub_key: pub_key.clone(),
+ message: format!("Status: {}", json.data.status),
+ });
+ }
+ }
+ code => {
+ let json: JsonError = resp.json()?;
+ // println!("JSON:\n{:?}", json); // For Debug.
+ return Err(CheckError::ValidatorError {
+ pub_key: pub_key.clone(),
+ message: format!(
+ "Http error code: {}, message: {}",
+ code, json.message
+ ),
+ });
+ }
+ }
+ }
+ Err(error) => {
+ println!("{error:?}");
+ return Err(CheckError::ValidatorError {
+ pub_key: pub_key.clone(),
+ message: error.to_string(),
+ });
+ }
+ }
+ }
+
+ // match request_builder
+ // .header("Authorization", format!("Apikey {}", api_key))
+ // .send()
+ // {
+ // Ok(resp) => {
+ // if resp.status().is_success() {
+ // let content = resp.text().unwrap();
+ // Ok(serde_json::from_str(&content).unwrap())
+ // } else {
+ // Err(Box::new(Error {
+ // message: format!("Request unsuccessful to {}: {:#?}", &url, resp),
+ // }))
+ // }
+ // }
+ // Err(error) => Err(Box::new(Error {
+ // message: format!("Error during request to {}: {:?}", &url, error),
+ // })),
+ // }
+
+ // 1) Check health.
+
+ // 2) Check each validators.
+
+ Ok(())
+}