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, ()),
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 crate::blocks::RawBlockHeader;
186 use crate::blocks::{CachingBlockHeader, Tipset};
187 use crate::shim::clock::EPOCH_DURATION_SECONDS;
188 use crate::shim::{address::Address, econ::TokenAmount};
189 use chrono::DateTime;
190 use quickcheck_macros::quickcheck;
191 use std::{str::FromStr, sync::Arc, time::Duration};
192
193 use super::{NodeStatusInfo, SyncStatus};
194
195 fn mock_tipset_at(seconds_since_unix_epoch: u64) -> Arc<Tipset> {
196 let mock_header = CachingBlockHeader::new(RawBlockHeader {
197 miner_address: Address::from_str("f2kmbjvz7vagl2z6pfrbjoggrkjofxspp7cqtw2zy").unwrap(),
198 timestamp: seconds_since_unix_epoch,
199 ..Default::default()
200 });
201 let tipset = Tipset::from(&mock_header);
202
203 Arc::new(tipset)
204 }
205
206 fn mock_node_status() -> NodeStatusInfo {
207 NodeStatusInfo {
208 lag: 0,
209 health: 90.,
210 epoch: i64::MAX,
211 base_fee: TokenAmount::from_whole(1),
212 sync_status: SyncStatus::Ok,
213 start_time: DateTime::<chrono::Utc>::MIN_UTC,
214 network: "calibnet".to_string(),
215 default_wallet_address: None,
216 default_wallet_address_balance: None,
217 }
218 }
219
220 fn node_status(duration: Duration, tipset: &Tipset) -> NodeStatusInfo {
221 NodeStatusInfo::new(
222 duration,
223 20.,
224 tipset,
225 DateTime::<chrono::Utc>::MIN_UTC,
226 "calibnet".to_string(),
227 None,
228 None,
229 )
230 }
231
232 #[quickcheck]
233 fn test_sync_status_ok(duration: Duration) {
234 let tipset = mock_tipset_at(duration.as_secs() + (EPOCH_DURATION_SECONDS as u64 * 3 / 2));
235
236 let status = node_status(duration, tipset.as_ref());
237
238 assert_ne!(status.sync_status, SyncStatus::Slow);
239 assert_ne!(status.sync_status, SyncStatus::Behind);
240 }
241
242 #[quickcheck]
243 fn test_sync_status_behind(duration: Duration) {
244 let duration = duration + Duration::from_secs(300);
245 let tipset = mock_tipset_at(duration.as_secs().saturating_sub(200));
246 let status = node_status(duration, tipset.as_ref());
247
248 assert!(status.health.is_finite());
249 assert_ne!(status.sync_status, SyncStatus::Ok);
250 assert_ne!(status.sync_status, SyncStatus::Slow);
251 }
252
253 #[quickcheck]
254 fn test_sync_status_slow(duration: Duration) {
255 let duration = duration + Duration::from_secs(300);
256 let tipset = mock_tipset_at(
257 duration
258 .as_secs()
259 .saturating_sub(EPOCH_DURATION_SECONDS as u64 * 4),
260 );
261 let status = node_status(duration, tipset.as_ref());
262 assert!(status.health.is_finite());
263 assert_ne!(status.sync_status, SyncStatus::Behind);
264 assert_ne!(status.sync_status, SyncStatus::Ok);
265 }
266
267 #[test]
268 fn block_sync_timestamp() {
269 let duration = Duration::from_secs(60);
270 let tipset = mock_tipset_at(duration.as_secs() - 10);
271 let status = node_status(duration, tipset.as_ref());
272
273 assert!(
274 status
275 .format(DateTime::<chrono::Utc>::MIN_UTC)
276 .contains("10s behind")
277 );
278 }
279
280 #[test]
281 fn test_lag_uptime_ahead() {
282 let mut status = mock_node_status();
283 status.lag = -360;
284 assert!(
285 status
286 .format(DateTime::<chrono::Utc>::MIN_UTC)
287 .contains("6m ahead")
288 );
289 }
290
291 #[test]
292 fn chain_status_test() {
293 let duration = Duration::from_secs(100_000);
294 let tipset = mock_tipset_at(duration.as_secs() - 59);
295 let status = node_status(duration, tipset.as_ref());
296 let expected_status_fmt =
297 "[sync: Slow! (59s behind)] [basefee: 0 FIL] [epoch: 0]".to_string();
298 assert!(
299 status
300 .format(DateTime::<chrono::Utc>::MIN_UTC)
301 .contains(&expected_status_fmt)
302 );
303
304 let tipset = mock_tipset_at(duration.as_secs() - 30000);
305 let status = node_status(duration, tipset.as_ref());
306
307 let expected_status_fmt =
308 "[sync: Behind! (8h 20m behind)] [basefee: 0 FIL] [epoch: 0]".to_string();
309 assert!(
310 status
311 .format(DateTime::<chrono::Utc>::MIN_UTC)
312 .contains(&expected_status_fmt)
313 );
314 }
315}