1use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use crate::blocks::Tipset;
7use crate::cli::humantoken::TokenAmountPretty;
8use crate::rpc::{self, prelude::*};
9use crate::shim::address::Address;
10use crate::shim::clock::{BLOCKS_PER_EPOCH, ChainEpoch, EPOCH_DURATION_SECONDS};
11use crate::shim::econ::TokenAmount;
12use chrono::{DateTime, Utc};
13use clap::Subcommand;
14use humantime::format_duration;
15
16#[derive(Debug, Subcommand)]
17pub enum InfoCommand {
18 Show,
19}
20
21#[derive(Debug)]
22pub struct NodeStatusInfo {
23 pub lag: i64,
25 pub health: f64,
30 pub epoch: ChainEpoch,
32 pub base_fee: TokenAmount,
34 pub sync_status: SyncStatus,
35 pub start_time: DateTime<Utc>,
37 pub network: String,
38 pub default_wallet_address: Option<Address>,
39 pub default_wallet_address_balance: Option<TokenAmount>,
40}
41
42#[derive(Debug, strum::Display, PartialEq)]
43pub enum SyncStatus {
44 Ok,
45 Slow,
46 Behind,
47 Fast,
48}
49
50impl NodeStatusInfo {
51 pub fn new(
52 cur_duration: Duration,
53 blocks_per_tipset_last_finality: f64,
54 head: &Tipset,
55 start_time: DateTime<Utc>,
56 network: String,
57 default_wallet_address: Option<Address>,
58 default_wallet_address_balance: Option<TokenAmount>,
59 ) -> NodeStatusInfo {
60 let ts = head.min_timestamp() as i64;
61 let cur_duration_secs = cur_duration.as_secs() as i64;
62 let lag = cur_duration_secs - ts;
63
64 let sync_status = if lag < 0 {
65 SyncStatus::Fast
66 } else if lag < EPOCH_DURATION_SECONDS * 3 / 2 {
67 SyncStatus::Ok
69 } else if lag < EPOCH_DURATION_SECONDS * 5 {
70 SyncStatus::Slow
72 } else {
73 SyncStatus::Behind
74 };
75
76 let base_fee = head.min_ticket_block().parent_base_fee.clone();
77
78 let health = 100. * blocks_per_tipset_last_finality / BLOCKS_PER_EPOCH as f64;
80
81 Self {
82 lag,
83 health,
84 epoch: head.epoch(),
85 base_fee,
86 sync_status,
87 start_time,
88 network,
89 default_wallet_address,
90 default_wallet_address_balance,
91 }
92 }
93
94 fn format(&self, now: DateTime<Utc>) -> String {
95 let network = format!("Network: {}", self.network);
96
97 let uptime = {
98 let uptime = (now - self.start_time)
99 .to_std()
100 .expect("failed converting to std duration");
101 let uptime = Duration::from_secs(uptime.as_secs());
102 let fmt_uptime = format_duration(uptime);
103 format!(
104 "Uptime: {fmt_uptime} (Started at: {})",
105 self.start_time.with_timezone(&chrono::offset::Local)
106 )
107 };
108
109 let chain = {
110 let base_fee_fmt = self.base_fee.pretty();
111 let lag_time = humantime::format_duration(Duration::from_secs(self.lag.unsigned_abs()));
112 let behind = if self.lag < 0 {
113 format!("{lag_time} ahead")
114 } else {
115 format!("{lag_time} behind")
116 };
117
118 format!(
119 "Chain: [sync: {}! ({})] [basefee: {base_fee_fmt}] [epoch: {}]",
120 self.sync_status, behind, self.epoch
121 )
122 };
123
124 let chain_health = format!("Chain health: {:.2}%\n\n", self.health);
125
126 let wallet_info = {
127 let wallet_address = self
128 .default_wallet_address
129 .as_ref()
130 .map(|it| it.to_string())
131 .unwrap_or("address not set".to_string());
132
133 let wallet_balance = self
134 .default_wallet_address_balance
135 .as_ref()
136 .map(|balance| format!("{:.4}", balance.pretty()))
137 .unwrap_or("could not find balance".to_string());
138
139 format!("Default wallet address: {wallet_address} [{wallet_balance}]")
140 };
141
142 [network, uptime, chain, chain_health, wallet_info].join("\n")
143 }
144}
145
146impl InfoCommand {
147 pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
148 let (node_status, head, network, start_time, default_wallet_address) = tokio::try_join!(
149 NodeStatus::call(&client, (true,)),
150 ChainHead::call(&client, ()),
151 StateNetworkName::call(&client, ()),
152 StartTime::call(&client, ()),
153 WalletDefaultAddress::call(&client, ()),
154 )?;
155
156 let cur_duration: Duration = SystemTime::now().duration_since(UNIX_EPOCH)?;
157 let blocks_per_tipset_last_finality =
158 node_status.chain_status.blocks_per_tipset_last_finality;
159
160 let default_wallet_address_balance = if let Some(def_addr) = default_wallet_address {
161 let balance = WalletBalance::call(&client, (def_addr,)).await?;
162 Some(balance)
163 } else {
164 None
165 };
166
167 let node_status_info = NodeStatusInfo::new(
168 cur_duration,
169 blocks_per_tipset_last_finality,
170 &head,
171 start_time,
172 network,
173 default_wallet_address,
174 default_wallet_address_balance,
175 );
176
177 println!("{}", node_status_info.format(Utc::now()));
178
179 Ok(())
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::blocks::RawBlockHeader;
187 use crate::blocks::{CachingBlockHeader, Tipset};
188 use crate::shim::clock::EPOCH_DURATION_SECONDS;
189 use crate::shim::{address::Address, econ::TokenAmount};
190 use chrono::DateTime;
191 use quickcheck_macros::quickcheck;
192 use std::{str::FromStr, time::Duration};
193
194 fn mock_tipset_at(seconds_since_unix_epoch: u64) -> Tipset {
195 CachingBlockHeader::new(RawBlockHeader {
196 miner_address: Address::from_str("f2kmbjvz7vagl2z6pfrbjoggrkjofxspp7cqtw2zy").unwrap(),
197 timestamp: seconds_since_unix_epoch,
198 ..Default::default()
199 })
200 .into()
201 }
202
203 fn mock_node_status() -> NodeStatusInfo {
204 NodeStatusInfo {
205 lag: 0,
206 health: 90.,
207 epoch: ChainEpoch::MAX,
208 base_fee: TokenAmount::from_whole(1),
209 sync_status: SyncStatus::Ok,
210 start_time: DateTime::<chrono::Utc>::MIN_UTC,
211 network: "calibnet".to_string(),
212 default_wallet_address: None,
213 default_wallet_address_balance: None,
214 }
215 }
216
217 fn node_status(duration: Duration, tipset: &Tipset) -> NodeStatusInfo {
218 NodeStatusInfo::new(
219 duration,
220 20.,
221 tipset,
222 DateTime::<chrono::Utc>::MIN_UTC,
223 "calibnet".to_string(),
224 None,
225 None,
226 )
227 }
228
229 #[quickcheck]
230 fn test_sync_status_ok(duration: Duration) {
231 let tipset = mock_tipset_at(duration.as_secs() + (EPOCH_DURATION_SECONDS as u64 * 3 / 2));
232
233 let status = node_status(duration, &tipset);
234
235 assert_ne!(status.sync_status, SyncStatus::Slow);
236 assert_ne!(status.sync_status, SyncStatus::Behind);
237 }
238
239 #[quickcheck]
240 fn test_sync_status_behind(duration: Duration) {
241 let duration = duration + Duration::from_secs(300);
242 let tipset = mock_tipset_at(duration.as_secs().saturating_sub(200));
243 let status = node_status(duration, &tipset);
244
245 assert!(status.health.is_finite());
246 assert_ne!(status.sync_status, SyncStatus::Ok);
247 assert_ne!(status.sync_status, SyncStatus::Slow);
248 }
249
250 #[quickcheck]
251 fn test_sync_status_slow(duration: Duration) {
252 let duration = duration + Duration::from_secs(300);
253 let tipset = mock_tipset_at(
254 duration
255 .as_secs()
256 .saturating_sub(EPOCH_DURATION_SECONDS as u64 * 4),
257 );
258 let status = node_status(duration, &tipset);
259 assert!(status.health.is_finite());
260 assert_ne!(status.sync_status, SyncStatus::Behind);
261 assert_ne!(status.sync_status, SyncStatus::Ok);
262 }
263
264 #[test]
265 fn block_sync_timestamp() {
266 let duration = Duration::from_secs(60);
267 let tipset = mock_tipset_at(duration.as_secs() - 10);
268 let status = node_status(duration, &tipset);
269
270 assert!(
271 status
272 .format(DateTime::<chrono::Utc>::MIN_UTC)
273 .contains("10s behind")
274 );
275 }
276
277 #[test]
278 fn test_lag_uptime_ahead() {
279 let mut status = mock_node_status();
280 status.lag = -360;
281 assert!(
282 status
283 .format(DateTime::<chrono::Utc>::MIN_UTC)
284 .contains("6m ahead")
285 );
286 }
287
288 #[test]
289 fn chain_status_test() {
290 let duration = Duration::from_secs(100_000);
291 let tipset = mock_tipset_at(duration.as_secs() - 59);
292 let status = node_status(duration, &tipset);
293 let expected_status_fmt =
294 "[sync: Slow! (59s behind)] [basefee: 0 FIL] [epoch: 0]".to_string();
295 assert!(
296 status
297 .format(DateTime::<chrono::Utc>::MIN_UTC)
298 .contains(&expected_status_fmt)
299 );
300
301 let tipset = mock_tipset_at(duration.as_secs() - 30000);
302 let status = node_status(duration, &tipset);
303
304 let expected_status_fmt =
305 "[sync: Behind! (8h 20m behind)] [basefee: 0 FIL] [epoch: 0]".to_string();
306 assert!(
307 status
308 .format(DateTime::<chrono::Utc>::MIN_UTC)
309 .contains(&expected_status_fmt)
310 );
311 }
312}