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