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#[derive(Debug, Parser)]
29#[command(version, about, long_about)]
30struct Cli {
31 #[command(subcommand)]
32 pub command: Command,
33
34 #[arg(short, long, global = true, env = "DATA_ANCHOR_PROGRAM_ID")]
36 pub program_id: Option<Pubkey>,
37
38 #[arg(short, long, global = true, env = "DATA_ANCHOR_NAMESPACE")]
40 pub namespace: Option<String>,
41
42 #[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 #[arg(short = 's', long, global = true, env = "DATA_ANCHOR_PAYER")]
54 pub payer: Option<String>,
55
56 #[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 #[arg(short, long, global = true, env = "DATA_ANCHOR_INDEXER_URL")]
69 pub indexer_url: Option<IndexerUrl>,
70
71 #[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 #[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 #[command(subcommand, visible_alias = "br")]
123 Blober(BloberSubCommand),
124 #[command(subcommand, visible_alias = "b")]
126 Blob(BlobSubCommand),
127 #[command(subcommand, visible_alias = "i")]
129 Indexer(IndexerSubCommand),
130 #[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 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 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 cancellation_token.cancel();
217
218 Ok(())
219 }
220}