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