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