1mod list;
5use list::ChainListCommand;
6
7mod prune;
8use prune::ChainPruneCommands;
9
10use super::print_pretty_lotus_json;
11use crate::blocks::{Tipset, TipsetKey};
12use crate::lotus_json::HasLotusJson;
13use crate::message::ChainMessage;
14use crate::rpc::{self, prelude::*};
15use anyhow::{bail, ensure};
16use cid::Cid;
17use clap::Subcommand;
18use nunny::Vec as NonEmpty;
19use std::sync::Arc;
20
21#[derive(Debug, Clone, clap::ValueEnum)]
22pub enum Format {
23 Json,
24 Text,
25}
26
27#[derive(Debug, Subcommand)]
28pub enum ChainCommands {
29 Block {
31 #[arg(short)]
32 cid: Cid,
33 },
34
35 Genesis,
37
38 Head {
40 #[arg(short = 'n', long, default_value = "1")]
43 tipsets: u64,
44 #[arg(long, default_value = "text")]
46 format: Format,
47 },
48
49 Message {
52 #[arg(short)]
53 cid: Cid,
54 },
55
56 ReadObj {
59 #[arg(short)]
60 cid: Cid,
61 },
62
63 SetHead {
66 #[arg(num_args = 1.., required = true)]
68 cids: Vec<Cid>,
69 #[arg(long, conflicts_with = "cids", allow_hyphen_values = true)]
72 epoch: Option<i64>,
73 #[arg(short, long, aliases = ["yes", "no-confirm"], short_alias = 'y')]
75 force: bool,
76 },
77 #[command(subcommand)]
78 Prune(ChainPruneCommands),
79 List(ChainListCommand),
80}
81
82impl ChainCommands {
83 pub async fn run(self, client: rpc::Client) -> anyhow::Result<()> {
84 match self {
85 Self::Block { cid } => {
86 print_pretty_lotus_json(ChainGetBlock::call(&client, (cid,)).await?)
87 }
88 Self::Genesis => print_pretty_lotus_json(ChainGetGenesis::call(&client, ()).await?),
89 Self::Head { tipsets, format } => print_chain_head(&client, tipsets, format).await,
90 Self::Message { cid } => {
91 let bytes = ChainReadObj::call(&client, (cid,)).await?;
92 match fvm_ipld_encoding::from_slice::<ChainMessage>(&bytes)? {
93 ChainMessage::Unsigned(m) => print_pretty_lotus_json(m),
94 ChainMessage::Signed(m) => {
95 let cid = m.cid();
96 println!(
97 "{}",
98 serde_json::to_string_pretty(&m.into_lotus_json().with_cid(cid))?
99 );
100 Ok(())
101 }
102 }
103 }
104 Self::ReadObj { cid } => {
105 let bytes = ChainReadObj::call(&client, (cid,)).await?;
106 println!("{}", hex::encode(bytes));
107 Ok(())
108 }
109 Self::SetHead {
110 cids,
111 epoch: Some(epoch),
112 force: no_confirm,
113 } => {
114 maybe_confirm(no_confirm, SET_HEAD_CONFIRMATION_MESSAGE)?;
115 assert!(cids.is_empty(), "should be disallowed by clap");
116 let tipset = tipset_by_epoch_or_offset(&client, epoch).await?;
117 ChainSetHead::call(&client, (tipset.key().clone(),)).await?;
118 Ok(())
119 }
120 Self::SetHead {
121 cids,
122 epoch: None,
123 force: no_confirm,
124 } => {
125 maybe_confirm(no_confirm, SET_HEAD_CONFIRMATION_MESSAGE)?;
126 ChainSetHead::call(
127 &client,
128 (TipsetKey::from(
129 NonEmpty::new(cids).expect("empty vec disallowed by clap"),
130 ),),
131 )
132 .await?;
133 Ok(())
134 }
135 Self::Prune(cmd) => cmd.run(client).await,
136 Self::List(cmd) => cmd.run(client).await,
137 }
138 }
139}
140
141async fn tipset_by_epoch_or_offset(
144 client: &rpc::Client,
145 epoch_or_offset: i64,
146) -> Result<Arc<Tipset>, jsonrpsee::core::ClientError> {
147 let current_head = ChainHead::call(client, ()).await?;
148
149 let target_epoch = match epoch_or_offset.is_negative() {
150 true => current_head.epoch() + epoch_or_offset, false => epoch_or_offset,
152 };
153 ChainGetTipSetByHeight::call(client, (target_epoch, current_head.key().clone().into())).await
154}
155
156const SET_HEAD_CONFIRMATION_MESSAGE: &str =
157 "Manually setting head is an unsafe operation that could brick the node! Continue?";
158
159fn maybe_confirm(no_confirm: bool, prompt: impl Into<String>) -> anyhow::Result<()> {
160 if no_confirm {
161 return Ok(());
162 }
163 let should_continue = dialoguer::Confirm::new()
164 .default(false)
165 .with_prompt(prompt)
166 .wait_for_newline(true)
167 .interact()?;
168 match should_continue {
169 true => Ok(()),
170 false => bail!("Operation cancelled by user"),
171 }
172}
173
174#[derive(Debug, serde::Serialize)]
175struct TipsetInfo {
176 epoch: u64,
177 cids: Vec<String>,
178}
179
180async fn collect_n_tipsets(client: &rpc::Client, n: u64) -> anyhow::Result<Vec<TipsetInfo>> {
183 ensure!(n > 0, "number of tipsets must be positive");
184 let current_epoch = ChainHead::call(client, ()).await?.epoch() as u64;
185 let mut tipsets = Vec::with_capacity(n as usize);
186 for epoch in (current_epoch.saturating_sub(n - 1)..=current_epoch).rev() {
187 let tipset = tipset_by_epoch_or_offset(client, epoch.try_into()?).await?;
188 tipsets.push(TipsetInfo {
189 epoch,
190 cids: tipset.cids().iter().map(|cid| cid.to_string()).collect(),
191 });
192 }
193 Ok(tipsets)
194}
195
196async fn print_chain_head(client: &rpc::Client, n: u64, format: Format) -> anyhow::Result<()> {
198 let tipsets = collect_n_tipsets(client, n).await?;
199 match format {
200 Format::Json => {
201 println!("{}", serde_json::to_string_pretty(&tipsets)?);
202 }
203 Format::Text => {
204 tipsets.iter().for_each(|epoch_info| {
205 println!("[{}]", epoch_info.epoch);
206 epoch_info.cids.iter().for_each(|cid| {
207 println!("{cid}");
208 });
209 });
210 }
211 }
212 Ok(())
213}