nitro_da_cli/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::sync::Arc;
4
5use benchmark::BenchmarkSubCommand;
6use blob::BlobSubCommand;
7use blober::BloberSubCommand;
8use clap::{CommandFactory, Parser, Subcommand, error::ErrorKind};
9use formatting::OutputFormat;
10use indexer::IndexerSubCommand;
11use nitro_da_blober::find_blober_address;
12use nitro_da_client::{BloberClient, BloberClientResult};
13use solana_cli_config::Config;
14use solana_sdk::{
15    pubkey::Pubkey,
16    signature::Keypair,
17    signer::{EncodableKey, Signer},
18};
19use tracing::trace;
20
21mod benchmark;
22mod blob;
23mod blober;
24mod formatting;
25mod indexer;
26
27/// The CLI options for the Blober CLI client.
28#[derive(Debug, Parser)]
29#[command(version, about, long_about)]
30struct Cli {
31    #[command(subcommand)]
32    pub command: Command,
33
34    /// The program ID of the Blober program.
35    #[arg(short, long, global = true, env = "BLOBER_PROGRAM_ID")]
36    pub program_id: Option<Pubkey>,
37
38    /// The namespace to use to generate the blober PDA.
39    #[arg(short, long, global = true, env = "BLOBER_NAMESPACE")]
40    pub namespace: Option<String>,
41
42    /// The payer account to use for transactions.
43    #[arg(short = 's', long, global = true, env = "BLOBER_PAYER")]
44    pub payer: Option<String>,
45
46    /// The output format to use.
47    #[arg(
48        short,
49        long,
50        global = true,
51        env = "BLOBER_OUTPUT",
52        value_enum,
53        default_value_t = OutputFormat::Text
54    )]
55    pub output: OutputFormat,
56
57    /// The URL of the indexer to use.
58    #[arg(short, long, global = true, env = "BLOBER_INDEXER_URL")]
59    pub indexer_url: Option<String>,
60
61    /// The path to the Solana [`Config`] file.
62    #[arg(
63        short,
64        long,
65        global = true,
66        env = "BLOBER_SOLANA_CONFIG_FILE",
67        default_value_t = solana_cli_config::CONFIG_FILE.as_ref().unwrap().clone()
68    )]
69    pub config_file: String,
70}
71
72impl Cli {
73    fn exit_with_missing_arg(msg: &str) -> ! {
74        Self::command()
75            .error(ErrorKind::MissingRequiredArgument, msg)
76            .exit()
77    }
78}
79
80#[derive(Debug, Subcommand)]
81enum Command {
82    /// Subcommands for managing the blober account.
83    #[command(subcommand, visible_alias = "br")]
84    Blober(BloberSubCommand),
85    /// Subcommands for managing blobs.
86    #[command(subcommand, visible_alias = "b")]
87    Blob(BlobSubCommand),
88    /// Subcommands for querying the indexer.
89    #[command(subcommand, visible_alias = "i")]
90    Indexer(IndexerSubCommand),
91    /// Subcommands for benchmarking the blober.
92    #[command(subcommand, visible_alias = "m")]
93    Benchmark(BenchmarkSubCommand),
94}
95
96pub struct Options {
97    command: Command,
98    program_id: Pubkey,
99    payer: Arc<Keypair>,
100    indexer_url: Option<String>,
101    namespace: String,
102    config: Config,
103    output: OutputFormat,
104}
105
106impl Options {
107    /// Parse the CLI options and load data from the Solana [`Config`] file and the payer
108    /// [`Keypair`].
109    pub fn parse() -> Self {
110        trace!("Parsing options");
111        let args = Cli::parse();
112        let config = Config::load(&args.config_file).unwrap();
113        let payer_path = args.payer.as_ref().unwrap_or(&config.keypair_path);
114        let payer = Arc::new(Keypair::read_from_file(payer_path).unwrap());
115        trace!("Parsed options: {args:?} {config:?} {payer:?}");
116
117        let Some(namespace) = args.namespace else {
118            Cli::exit_with_missing_arg(
119                "Namespace is not set. Please provide a namespace using the --namespace flag or set the BLOBER_NAMESPACE environment variable.",
120            );
121        };
122
123        let Some(program_id) = args.program_id else {
124            Cli::exit_with_missing_arg(
125                "Program ID is not set. Please provide a program ID using the --program-id flag or set the BLOBER_PROGRAM_ID environment variable.",
126            );
127        };
128
129        Self {
130            namespace,
131            indexer_url: args.indexer_url,
132            command: args.command,
133            program_id,
134            output: args.output,
135            payer,
136            config,
137        }
138    }
139
140    /// Run the parsed CLI command.
141    pub async fn run(self) -> BloberClientResult {
142        let blober = find_blober_address(self.program_id, self.payer.pubkey(), &self.namespace);
143
144        let builder = BloberClient::builder()
145            .payer(self.payer.clone())
146            .program_id(self.program_id);
147
148        let client = if let Some(indexer_url) = self.indexer_url {
149            builder
150                .indexer_from_url(&indexer_url)
151                .await?
152                .build_with_config(self.config)
153                .await?
154        } else {
155            builder.build_with_config(self.config).await?
156        };
157
158        let client = Arc::new(client);
159
160        let output = match self.command {
161            Command::Blober(subcommand) => {
162                subcommand.run(client.clone(), blober, self.namespace).await
163            }
164            Command::Blob(subcommand) => subcommand.run(client.clone(), blober).await,
165            Command::Indexer(subcommand) => subcommand.run(client.clone(), blober).await,
166            Command::Benchmark(subcommand) => subcommand.run(client.clone(), blober).await,
167        }?;
168
169        println!("{}", output.serialize_output(self.output));
170
171        Ok(())
172    }
173}