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