Periodically print a "no error" status
[stakingWatchdog.git] / src / main.rs
1 /*
2 * API Reference: https://ethereum.github.io/beacon-APIs/
3 */
4
5 #![cfg_attr(debug_assertions, allow(unused_variables, unused_imports, dead_code))]
6
7 use std::{
8 fs,
9 net::{IpAddr, Ipv4Addr},
10 thread,
11 time::{self, Duration},
12 };
13
14 use anyhow::{Context, Result};
15 use lettre::{
16 message::header::ContentType, transport::smtp::authentication::Credentials, Message,
17 SmtpTransport, Transport,
18 };
19 use reqwest::StatusCode;
20 use serde::Deserialize;
21 use serde_json::{json, Value};
22
23 use crate::config::Config;
24
25 mod config;
26 // mod error;
27
28 const FILE_CONF: &str = "config.ron";
29 const CHECK_PERIOD: Duration = Duration::from_secs(10); // 10 s.
30 const EMAIL_RESEND_PERIOD: Duration = Duration::from_secs(6 * 60 * 60); // 6 h.
31 const STATE_PRINT_PERIOD: Duration = Duration::from_secs(15 * 60); // 15 min.
32 const BASE_URI: &str = "http://localhost:5052/eth/v1/";
33
34 fn main() -> Result<()> {
35 println!("Staking Watchdog");
36
37 let config = Config::read(FILE_CONF)?;
38
39 println!(
40 "Configuration: {:?}",
41 Config {
42 smtp_password: "*****".to_string(),
43 ..config.clone()
44 }
45 );
46
47 let mut time_last_email_send = time::Instant::now() - EMAIL_RESEND_PERIOD;
48 let mut time_last_state_printed = time::Instant::now() - STATE_PRINT_PERIOD;
49 let mut error_state = false;
50
51 loop {
52 let time_beginning_loop = time::Instant::now();
53
54 if let Err(error) = check_validators(&config.pub_keys) {
55 error_state = true;
56 println!("Error: {:?}", error);
57 if time::Instant::now() - time_last_email_send >= EMAIL_RESEND_PERIOD {
58 // Send e-mail.
59 println!("Sending email...");
60 match send_email(
61 "Staking ERROR",
62 &format!("Error: {:?}", error),
63 &config.smtp_login,
64 &config.smtp_password,
65 ) {
66 Err(email_error) => println!("Error sending email: {:?}", email_error),
67 _ => {
68 println!("Email successfully sent");
69 time_last_email_send = time::Instant::now();
70 }
71 }
72 }
73 } else {
74 if error_state {
75 error_state = false;
76 println!("End of erroneous state");
77 }
78
79 if time::Instant::now() - time_last_state_printed >= STATE_PRINT_PERIOD {
80 println!("No error detected");
81 time_last_state_printed = time::Instant::now();
82 }
83 }
84
85 let elapsed = time::Instant::now() - time_beginning_loop;
86
87 if elapsed < CHECK_PERIOD {
88 let to_wait = CHECK_PERIOD - elapsed;
89 thread::sleep(to_wait);
90 }
91 }
92 }
93
94 #[derive(Debug)]
95 enum CheckError {
96 HttpError(String),
97 NotSync,
98 InvalidSyncStatus,
99 NodeHavingIssues,
100 UnknownCodeFromHealthCheck(u16),
101 ReqwestError(reqwest::Error),
102 ValidatorError { pub_key: String, message: String },
103 ValidatorStatusError { pub_key: String, message: String },
104 }
105
106 impl From<reqwest::Error> for CheckError {
107 fn from(value: reqwest::Error) -> Self {
108 CheckError::ReqwestError(value)
109 }
110 }
111
112 #[derive(Deserialize, Debug)]
113 struct JsonValidatorState {
114 data: JsonValidatorStateData,
115 }
116
117 #[derive(Deserialize, Debug)]
118 struct JsonValidatorStateData {
119 status: String,
120 }
121
122 #[derive(Deserialize, Debug)]
123 struct JsonError {
124 code: u16,
125 message: String,
126 }
127
128 fn check_validators(pub_keys: &[String]) -> std::result::Result<(), CheckError> {
129 let url = BASE_URI;
130 let client = reqwest::blocking::Client::new();
131
132 let request_health = client
133 .get(format!("{url}node/health"))
134 .header("accept", "application/json");
135 match request_health.send() {
136 Ok(resp) => {
137 // println!("{resp:?}"); // For debug.
138 match resp.status().as_u16() {
139 200 => (),
140 206 => return Err(CheckError::NotSync),
141 400 => return Err(CheckError::InvalidSyncStatus),
142 503 => return Err(CheckError::NodeHavingIssues),
143 code => return Err(CheckError::UnknownCodeFromHealthCheck(code)),
144 }
145 }
146 Err(error) => {
147 println!("{error:?}");
148 return Err(CheckError::HttpError(error.to_string()));
149 }
150 }
151
152 for pub_key in pub_keys {
153 let request = client
154 .get(format!("{url}beacon/states/head/validators/0x{pub_key}"))
155 .header("accept", "application/json");
156 match request.send() {
157 Ok(resp) => {
158 // println!("{resp:?}"); // For debug.
159 match resp.status().as_u16() {
160 200 => {
161 let json: JsonValidatorState = resp.json()?;
162 if json.data.status != "active_ongoing" {
163 return Err(CheckError::ValidatorStatusError {
164 pub_key: pub_key.clone(),
165 message: format!("Status: {}", json.data.status),
166 });
167 }
168 }
169 code => {
170 let json: JsonError = resp.json()?;
171 return Err(CheckError::ValidatorError {
172 pub_key: pub_key.clone(),
173 message: format!(
174 "Http error code: {}, message: {}",
175 code, json.message
176 ),
177 });
178 }
179 }
180 }
181 Err(error) => {
182 return Err(CheckError::ValidatorError {
183 pub_key: pub_key.clone(),
184 message: error.to_string(),
185 });
186 }
187 }
188 }
189
190 Ok(())
191 }
192
193 fn send_email(title: &str, body: &str, login: &str, pass: &str) -> Result<()> {
194 let email = Message::builder()
195 .message_id(None)
196 .from("Staking Watchdog <redmine@d-lan.net>".parse()?)
197 .to("Greg Burri <greg.burri@gmail.com>".parse()?)
198 .subject(title)
199 .header(ContentType::TEXT_PLAIN)
200 .body(body.to_string())?;
201
202 let creds = Credentials::new(login.to_string(), pass.to_string());
203
204 // Open a remote connection to gmail
205 let mailer = SmtpTransport::relay("mail.gandi.net")?
206 .credentials(creds)
207 .build();
208
209 // Send the email
210 let response = mailer.send(&email)?;
211
212 println!("{:?}", response);
213
214 Ok(())
215 }