Skip to main content

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