data_anchor/
lib.rs

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