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