data_anchor/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{path::PathBuf, str::FromStr, sync::Arc};
4
5use anchor_lang::prelude::Pubkey;
6use benchmark::BenchmarkSubCommand;
7use blob::BlobSubCommand;
8use blober::BloberSubCommand;
9use clap::{CommandFactory, Parser, Subcommand, error::ErrorKind};
10use data_anchor_client::{BloberIdentifier, DataAnchorClient, DataAnchorClientResult};
11use data_anchor_utils::{compression, encoding};
12use formatting::OutputFormat;
13use indexer::IndexerSubCommand;
14use solana_cli_config::Config;
15use solana_keypair::Keypair;
16use solana_signer::{EncodableKey, Signer};
17use tracing::trace;
18
19mod benchmark;
20mod blob;
21mod blober;
22mod formatting;
23mod indexer;
24
25const NAMESPACE_MISSING_MSG: &str = "Namespace is not set. Please provide a namespace using the --namespace flag or set the DATA_ANCHOR_NAMESPACE environment variable.";
26const INDEXER_URL_MISSING_MSG: &str = "Indexer URL is not set. Please provide a URL using the --indexer-url flag or set the DATA_ANCHOR_INDEXER_URL environment variable.";
27
28/// The CLI options for the Blober CLI client.
29#[derive(Debug, Parser)]
30#[command(version, about, long_about)]
31struct Cli {
32    #[command(subcommand)]
33    pub command: Command,
34
35    /// The program ID of the Blober program.
36    #[arg(short, long, global = true, env = "DATA_ANCHOR_PROGRAM_ID")]
37    pub program_id: Option<Pubkey>,
38
39    /// The namespace to use to generate the blober PDA.
40    #[arg(short, long, global = true, env = "DATA_ANCHOR_NAMESPACE")]
41    pub namespace: Option<String>,
42
43    /// The blober PDA to use instead of generating one from the namespace.
44    #[arg(
45        short,
46        long,
47        global = true,
48        env = "DATA_ANCHOR_BLOBER_PDA",
49        value_name = "BLOBER_PDA"
50    )]
51    pub blober_pda: Option<Pubkey>,
52
53    /// The payer account to use for transactions.
54    #[arg(short = 's', long, global = true, env = "DATA_ANCHOR_PAYER")]
55    pub payer: Option<String>,
56
57    /// The output format to use.
58    #[arg(
59        short,
60        long,
61        global = true,
62        env = "DATA_ANCHOR_OUTPUT",
63        value_enum,
64        default_value_t = OutputFormat::Text
65    )]
66    pub output: OutputFormat,
67
68    /// The URL of the indexer to use.
69    #[arg(short, long, global = true, env = "DATA_ANCHOR_INDEXER_URL")]
70    pub indexer_url: Option<String>,
71
72    /// The API token for the indexer, if required.
73    #[arg(
74        long,
75        global = true,
76        env = "DATA_ANCHOR_INDEXER_API_TOKEN",
77        hide_env_values = true
78    )]
79    pub indexer_api_token: Option<String>,
80
81    /// The path to the Solana [`Config`] file.
82    #[arg(
83        short,
84        long,
85        global = true,
86        env = "DATA_ANCHOR_SOLANA_CONFIG_FILE",
87        default_value_t = solana_cli_config::CONFIG_FILE.as_ref().unwrap().clone()
88    )]
89    pub config_file: String,
90}
91
92impl Cli {
93    fn exit_with_missing_arg(msg: &str) -> ! {
94        Self::command()
95            .error(ErrorKind::MissingRequiredArgument, msg)
96            .exit()
97    }
98
99    fn payer_keypair(&self, config: &Config) -> String {
100        if let Some(payer) = &self.payer {
101            return payer.to_owned();
102        }
103
104        let Ok(path_to_config) = PathBuf::from_str(&self.config_file);
105
106        let Some(directory) = path_to_config.parent() else {
107            Self::exit_with_missing_arg("Failed to get the parent directory of the config file")
108        };
109
110        let path = directory.join(&config.keypair_path);
111
112        let Some(path_str) = path.to_str() else {
113            Self::exit_with_missing_arg("Failed to convert the keypair path to a string")
114        };
115
116        path_str.to_owned()
117    }
118}
119
120#[derive(Debug, Subcommand)]
121enum Command {
122    /// Subcommands for managing the blober account.
123    #[command(subcommand, visible_alias = "br")]
124    Blober(BloberSubCommand),
125    /// Subcommands for managing blobs.
126    #[command(subcommand, visible_alias = "b")]
127    Blob(BlobSubCommand),
128    /// Subcommands for querying the indexer.
129    #[command(subcommand, visible_alias = "i")]
130    Indexer(IndexerSubCommand),
131    /// Subcommands for benchmarking the blober.
132    #[command(subcommand, visible_alias = "m")]
133    Benchmark(BenchmarkSubCommand),
134}
135
136pub struct Options {
137    command: Command,
138    program_id: Pubkey,
139    payer: Arc<Keypair>,
140    blober_pda: BloberIdentifier,
141    indexer_url: Option<String>,
142    indexer_api_token: Option<String>,
143    config: Config,
144    output: OutputFormat,
145}
146
147impl Options {
148    /// Parse the CLI options and load data from the Solana [`Config`] file and the payer
149    /// [`Keypair`].
150    pub fn parse() -> Self {
151        trace!("Parsing options");
152        let args = Cli::parse();
153        let config = Config::load(&args.config_file).unwrap();
154        let payer_path = args.payer_keypair(&config);
155        let payer = Arc::new(Keypair::read_from_file(payer_path).unwrap());
156        trace!("Parsed options: {args:?} {config:?} {payer:?}");
157
158        let program_id = args.program_id.unwrap_or(data_anchor_blober::id());
159
160        let blober_pda = if let Some(blober_pda) = args.blober_pda {
161            blober_pda.into()
162        } else {
163            let Some(nmsp) = args.namespace else {
164                Cli::exit_with_missing_arg(NAMESPACE_MISSING_MSG);
165            };
166
167            (payer.pubkey(), nmsp).into()
168        };
169
170        Self {
171            indexer_url: args.indexer_url,
172            indexer_api_token: args.indexer_api_token,
173            command: args.command,
174            program_id,
175            output: args.output,
176            blober_pda,
177            payer,
178            config,
179        }
180    }
181
182    /// Run the parsed CLI command.
183    pub async fn run(self) -> DataAnchorClientResult {
184        let output = match self.command {
185            Command::Indexer(subcommand) => {
186                let Some(indexer_url) = self.indexer_url else {
187                    Cli::exit_with_missing_arg(INDEXER_URL_MISSING_MSG);
188                };
189                let client = DataAnchorClient::<encoding::Default, compression::Default>::builder()
190                    .payer(self.payer.clone())
191                    .program_id(self.program_id)
192                    .indexer_from_url(&indexer_url, self.indexer_api_token.clone())
193                    .await?
194                    .build_with_config(self.config)
195                    .await?;
196                let client = Arc::new(client);
197
198                subcommand
199                    .run(
200                        client.clone(),
201                        self.blober_pda
202                            .to_blober_address(self.program_id, self.payer.pubkey()),
203                    )
204                    .await
205            }
206            subcommand => {
207                let Some(namespace) = &self.blober_pda.namespace() else {
208                    Cli::exit_with_missing_arg(NAMESPACE_MISSING_MSG);
209                };
210                let client = DataAnchorClient::<encoding::Default, compression::Default>::builder()
211                    .payer(self.payer.clone())
212                    .program_id(self.program_id)
213                    .build_with_config(self.config)
214                    .await?;
215                let client = Arc::new(client);
216
217                match subcommand {
218                    Command::Blob(subcommand) => subcommand.run(client.clone(), namespace).await,
219                    Command::Blober(subcommand) => {
220                        subcommand
221                            .run(
222                                client.clone(),
223                                self.blober_pda,
224                                self.program_id,
225                                self.payer.pubkey(),
226                            )
227                            .await
228                    }
229                    Command::Benchmark(subcommand) => {
230                        subcommand.run(client.clone(), namespace).await
231                    }
232                    _ => unreachable!("Indexer subcommands should have been handled above"),
233                }
234            }
235        }?;
236
237        println!("{}", output.serialize_output(self.output));
238
239        Ok(())
240    }
241}