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