1use {
2 crate::clap_app::{Error, COMPUTE_UNIT_LIMIT_ARG, COMPUTE_UNIT_PRICE_ARG, MULTISIG_SIGNER_ARG},
3 clap::ArgMatches,
4 solana_clap_v3_utils::{
5 input_parsers::pubkey_of_signer,
6 input_validators::normalize_to_url_if_moniker,
7 keypair::SignerFromPathConfig,
8 nonce::{NONCE_ARG, NONCE_AUTHORITY_ARG},
9 offline::{BLOCKHASH_ARG, DUMP_TRANSACTION_MESSAGE, SIGNER_ARG, SIGN_ONLY_ARG},
10 },
11 solana_cli_output::OutputFormat,
12 solana_client::nonblocking::rpc_client::RpcClient,
13 solana_commitment_config::CommitmentConfig,
14 solana_remote_wallet::remote_wallet::RemoteWalletManager,
15 solana_sdk::{
16 account::Account as RawAccount, hash::Hash, pubkey::Pubkey, signature::Signer,
17 signer::null_signer::NullSigner,
18 },
19 spl_associated_token_account_interface::address::get_associated_token_address_with_program_id,
20 spl_token_2022_interface::{
21 extension::StateWithExtensionsOwned,
22 state::{Account, Mint},
23 },
24 spl_token_client::{
25 client::{
26 ProgramClient, ProgramOfflineClient, ProgramRpcClient, ProgramRpcClientSendTransaction,
27 },
28 token::ComputeUnitLimit,
29 },
30 std::{process::exit, rc::Rc, str::FromStr, sync::Arc, time::Duration},
31};
32
33fn get_cli_config(matches: &ArgMatches) -> solana_cli_config::Config {
34 if let Some(config_file) = matches.value_of("config_file") {
35 solana_cli_config::Config::load(config_file).unwrap_or_else(|_| {
36 eprintln!("error: Could not find config file `{}`", config_file);
37 exit(1);
38 })
39 } else if let Some(config_file) = &*solana_cli_config::CONFIG_FILE {
40 solana_cli_config::Config::load(config_file).unwrap_or_default()
41 } else {
42 solana_cli_config::Config::default()
43 }
44}
45
46type SignersOf = Vec<(Arc<dyn Signer>, Pubkey)>;
47fn signers_of(
48 matches: &ArgMatches,
49 name: &str,
50 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
51) -> Result<Option<SignersOf>, Box<dyn std::error::Error>> {
52 if let Some(values) = matches.try_get_many::<String>(name).ok().flatten() {
53 let mut results = Vec::new();
54 for (i, value) in values.enumerate() {
55 let name = format!("{}-{}", name, i.saturating_add(1));
56 let signer = signer_from_path(matches, value, &name, wallet_manager)?;
57 let signer_pubkey = signer.pubkey();
58 results.push((Arc::from(signer), signer_pubkey));
59 }
60 Ok(Some(results))
61 } else {
62 Ok(None)
63 }
64}
65
66pub(crate) struct MintInfo {
67 pub program_id: Pubkey,
68 pub address: Pubkey,
69 pub decimals: u8,
70}
71
72const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(30);
73const DEFAULT_CONFIRM_TX_TIMEOUT: Duration = Duration::from_secs(5);
74
75pub struct Config<'a> {
76 pub default_signer: Option<Arc<dyn Signer>>,
77 pub rpc_client: Arc<RpcClient>,
78 pub program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>>,
79 pub websocket_url: String,
80 pub output_format: OutputFormat,
81 pub fee_payer: Option<Arc<dyn Signer>>,
82 pub nonce_account: Option<Pubkey>,
83 pub nonce_authority: Option<Arc<dyn Signer>>,
84 pub nonce_blockhash: Option<Hash>,
85 pub sign_only: bool,
86 pub dump_transaction_message: bool,
87 pub multisigner_pubkeys: Vec<&'a Pubkey>,
88 pub program_id: Pubkey,
89 pub restrict_to_program_id: bool,
90 pub compute_unit_price: Option<u64>,
91 pub compute_unit_limit: ComputeUnitLimit,
92}
93
94impl<'a> Config<'a> {
95 pub async fn new(
96 matches: &ArgMatches,
97 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
98 bulk_signers: &mut Vec<Arc<dyn Signer>>,
99 multisigner_ids: &'a mut Vec<Pubkey>,
100 ) -> Config<'a> {
101 let cli_config = get_cli_config(matches);
102 let json_rpc_url = normalize_to_url_if_moniker(
103 matches
104 .value_of("json_rpc_url")
105 .unwrap_or(&cli_config.json_rpc_url),
106 );
107 let websocket_url = solana_cli_config::Config::compute_websocket_url(&json_rpc_url);
108 let commitment_config = CommitmentConfig::from_str(&cli_config.commitment)
109 .unwrap_or_else(|_| CommitmentConfig::confirmed());
110 let rpc_client = Arc::new(RpcClient::new_with_timeouts_and_commitment(
111 json_rpc_url,
112 DEFAULT_RPC_TIMEOUT,
113 commitment_config,
114 DEFAULT_CONFIRM_TX_TIMEOUT,
115 ));
116 let sign_only = matches.try_contains_id(SIGN_ONLY_ARG.name).unwrap_or(false);
117 let program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>> = if sign_only {
118 let blockhash = matches
119 .get_one::<Hash>(BLOCKHASH_ARG.name)
120 .copied()
121 .unwrap_or_default();
122 Arc::new(ProgramOfflineClient::new(
123 blockhash,
124 ProgramRpcClientSendTransaction,
125 ))
126 } else {
127 Arc::new(ProgramRpcClient::new(
128 rpc_client.clone(),
129 ProgramRpcClientSendTransaction,
130 ))
131 };
132 Self::new_with_clients_and_ws_url(
133 matches,
134 wallet_manager,
135 bulk_signers,
136 multisigner_ids,
137 rpc_client,
138 program_client,
139 websocket_url,
140 )
141 .await
142 }
143
144 fn extract_multisig_signers(
145 matches: &ArgMatches,
146 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
147 bulk_signers: &mut Vec<Arc<dyn Signer>>,
148 multisigner_ids: &'a mut Vec<Pubkey>,
149 ) -> Vec<&'a Pubkey> {
150 let multisig_signers = signers_of(matches, MULTISIG_SIGNER_ARG.name, wallet_manager)
151 .unwrap_or_else(|e| {
152 eprintln!("error: {}", e);
153 exit(1);
154 });
155 if let Some(mut multisig_signers) = multisig_signers {
156 multisig_signers.sort_by(|(_, lp), (_, rp)| lp.cmp(rp));
157 let (signers, pubkeys): (Vec<_>, Vec<_>) = multisig_signers.into_iter().unzip();
158 bulk_signers.extend(signers);
159 multisigner_ids.extend(pubkeys);
160 }
161 multisigner_ids.iter().collect::<Vec<_>>()
162 }
163
164 pub async fn new_with_clients_and_ws_url(
165 matches: &ArgMatches,
166 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
167 bulk_signers: &mut Vec<Arc<dyn Signer>>,
168 multisigner_ids: &'a mut Vec<Pubkey>,
169 rpc_client: Arc<RpcClient>,
170 program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>>,
171 websocket_url: String,
172 ) -> Config<'a> {
173 let cli_config = get_cli_config(matches);
174 let multisigner_pubkeys =
175 Self::extract_multisig_signers(matches, wallet_manager, bulk_signers, multisigner_ids);
176
177 let config = SignerFromPathConfig {
178 allow_null_signer: !multisigner_pubkeys.is_empty(),
179 };
180
181 let default_keypair = cli_config.keypair_path.clone();
182
183 let default_signer: Option<Arc<dyn Signer>> = {
184 if let Some(owner_path) = matches.try_get_one::<String>("owner").ok().flatten() {
185 signer_from_path_with_config(matches, owner_path, "owner", wallet_manager, &config)
186 .ok()
187 } else {
188 signer_from_path_with_config(
189 matches,
190 &default_keypair,
191 "default",
192 wallet_manager,
193 &config,
194 )
195 .map_err(|e| {
196 if std::fs::metadata(&default_keypair).is_ok() {
197 eprintln!("error: {}", e);
198 exit(1);
199 } else {
200 e
201 }
202 })
203 .ok()
204 }
205 }
206 .map(Arc::from);
207
208 let fee_payer: Option<Arc<dyn Signer>> = matches
209 .value_of("fee_payer")
210 .map(|path| {
211 Arc::from(
212 signer_from_path(matches, path, "fee_payer", wallet_manager).unwrap_or_else(
213 |e| {
214 eprintln!("error: {}", e);
215 exit(1);
216 },
217 ),
218 )
219 })
220 .or_else(|| default_signer.clone());
221
222 let verbose = matches.is_present("verbose");
223 let output_format = matches
224 .value_of("output_format")
225 .map(|value| match value {
226 "json" => OutputFormat::Json,
227 "json-compact" => OutputFormat::JsonCompact,
228 _ => unreachable!(),
229 })
230 .unwrap_or(if verbose {
231 OutputFormat::DisplayVerbose
232 } else {
233 OutputFormat::Display
234 });
235
236 let nonce_account = match pubkey_of_signer(matches, NONCE_ARG.name, wallet_manager) {
237 Ok(account) => account,
238 Err(e) => {
239 if e.is::<clap::parser::MatchesError>() {
240 None
241 } else {
242 eprintln!("error: {}", e);
243 exit(1);
244 }
245 }
246 };
247 let nonce_authority = if nonce_account.is_some() {
248 let (nonce_authority, _) = signer_from_path(
249 matches,
250 matches
251 .value_of(NONCE_AUTHORITY_ARG.name)
252 .unwrap_or(&cli_config.keypair_path),
253 NONCE_AUTHORITY_ARG.name,
254 wallet_manager,
255 )
256 .map(Arc::from)
257 .map(|s: Arc<dyn Signer>| {
258 let p = s.pubkey();
259 (s, p)
260 })
261 .unwrap_or_else(|e| {
262 eprintln!("error: {}", e);
263 exit(1);
264 });
265
266 Some(nonce_authority)
267 } else {
268 None
269 };
270
271 let sign_only = matches.try_contains_id(SIGN_ONLY_ARG.name).unwrap_or(false);
272 let dump_transaction_message = matches
273 .try_contains_id(DUMP_TRANSACTION_MESSAGE.name)
274 .unwrap_or(false);
275
276 let pubkey_from_matches = |name| {
277 matches
278 .try_get_one::<String>(name)
279 .ok()
280 .flatten()
281 .and_then(|pubkey| Pubkey::from_str(pubkey).ok())
282 };
283
284 let default_program_id = spl_token_interface::id();
285 let (program_id, restrict_to_program_id) = if matches.is_present("program_2022") {
286 (spl_token_2022_interface::id(), true)
287 } else if let Some(program_id) = pubkey_from_matches("program_id") {
288 (program_id, true)
289 } else if !sign_only {
290 if let Some(address) = pubkey_from_matches("token")
291 .or_else(|| pubkey_from_matches("account"))
292 .or_else(|| pubkey_from_matches("address"))
293 {
294 (
295 rpc_client
296 .get_account(&address)
297 .await
298 .map(|account| account.owner)
299 .unwrap_or(default_program_id),
300 false,
301 )
302 } else {
303 (default_program_id, false)
304 }
305 } else {
306 (default_program_id, false)
307 };
308
309 if matches.try_contains_id(BLOCKHASH_ARG.name).unwrap_or(false)
310 && matches
311 .try_contains_id(COMPUTE_UNIT_PRICE_ARG.name)
312 .unwrap_or(false)
313 && !matches
314 .try_contains_id(COMPUTE_UNIT_LIMIT_ARG.name)
315 .unwrap_or(false)
316 {
317 clap::Error::with_description(
318 format!(
319 "Need to set `{}` if `{}` and `--{}` are set",
320 COMPUTE_UNIT_LIMIT_ARG.long, COMPUTE_UNIT_PRICE_ARG.long, BLOCKHASH_ARG.long,
321 ),
322 clap::ErrorKind::MissingRequiredArgument,
323 )
324 .exit();
325 }
326
327 let nonce_blockhash = matches
328 .try_get_one::<Hash>(BLOCKHASH_ARG.name)
329 .ok()
330 .flatten()
331 .copied();
332
333 let compute_unit_price = matches.get_one::<u64>(COMPUTE_UNIT_PRICE_ARG.name).copied();
334
335 let compute_unit_limit = matches
336 .get_one::<u32>(COMPUTE_UNIT_LIMIT_ARG.name)
337 .copied()
338 .map(ComputeUnitLimit::Static)
339 .unwrap_or_else(|| {
340 if nonce_blockhash.is_some() {
341 ComputeUnitLimit::Default
342 } else {
343 ComputeUnitLimit::Simulated
344 }
345 });
346
347 Self {
348 default_signer,
349 rpc_client,
350 program_client,
351 websocket_url,
352 output_format,
353 fee_payer,
354 nonce_account,
355 nonce_authority,
356 nonce_blockhash,
357 sign_only,
358 dump_transaction_message,
359 multisigner_pubkeys,
360 program_id,
361 restrict_to_program_id,
362 compute_unit_price,
363 compute_unit_limit,
364 }
365 }
366
367 pub(crate) fn default_signer(&self) -> Result<Arc<dyn Signer>, Error> {
369 if let Some(default_signer) = &self.default_signer {
370 Ok(default_signer.clone())
371 } else {
372 Err("default signer is required, please specify a valid default signer by identifying a \
373 valid configuration file using the --config argument, or by creating a valid config \
374 at the default location of ~/.config/solana/cli/config.yml using the solana config \
375 command".to_string().into())
376 }
377 }
378
379 pub fn fee_payer(&self) -> Result<Arc<dyn Signer>, Error> {
381 if let Some(fee_payer) = &self.fee_payer {
382 Ok(fee_payer.clone())
383 } else {
384 Err("fee payer is required, please specify a valid fee payer using the --fee-payer argument, \
385 or by identifying a valid configuration file using the --config argument, or by creating \
386 a valid config at the default location of ~/.config/solana/cli/config.yml using the solana \
387 config command".to_string().into())
388 }
389 }
390
391 pub(crate) async fn associated_token_address_or_override(
394 &self,
395 arg_matches: &ArgMatches,
396 override_name: &str,
397 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
398 ) -> Result<Pubkey, Error> {
399 let token = pubkey_of_signer(arg_matches, "token", wallet_manager)
400 .map_err(|e| -> Error { e.to_string().into() })?;
401 self.associated_token_address_for_token_or_override(
402 arg_matches,
403 override_name,
404 wallet_manager,
405 token,
406 )
407 .await
408 }
409
410 pub(crate) async fn associated_token_address_for_token_or_override(
413 &self,
414 arg_matches: &ArgMatches,
415 override_name: &str,
416 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
417 token: Option<Pubkey>,
418 ) -> Result<Pubkey, Error> {
419 if let Some(address) = pubkey_of_signer(arg_matches, override_name, wallet_manager)
420 .map_err(|e| -> Error { e.to_string().into() })?
421 {
422 return Ok(address);
423 }
424
425 let token = token.unwrap();
426 let program_id = self.get_mint_info(&token, None).await?.program_id;
427 let owner = self.pubkey_or_default(arg_matches, "owner", wallet_manager)?;
428 self.associated_token_address_for_token_and_program(&token, &owner, &program_id)
429 }
430
431 pub(crate) fn associated_token_address_for_token_and_program(
432 &self,
433 token: &Pubkey,
434 owner: &Pubkey,
435 program_id: &Pubkey,
436 ) -> Result<Pubkey, Error> {
437 Ok(get_associated_token_address_with_program_id(
438 owner, token, program_id,
439 ))
440 }
441
442 pub(crate) fn pubkey_or_default(
445 &self,
446 arg_matches: &ArgMatches,
447 address_name: &str,
448 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
449 ) -> Result<Pubkey, Error> {
450 if let Some(address) = pubkey_of_signer(arg_matches, address_name, wallet_manager)
451 .map_err(|e| -> Error { e.to_string().into() })?
452 {
453 return Ok(address);
454 }
455
456 Ok(self.default_signer()?.pubkey())
457 }
458
459 pub(crate) fn signer_or_default(
462 &self,
463 arg_matches: &ArgMatches,
464 authority_name: &str,
465 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
466 ) -> (Arc<dyn Signer>, Pubkey) {
467 let config = SignerFromPathConfig {
470 allow_null_signer: !self.multisigner_pubkeys.is_empty(),
471 };
472 let mut load_authority = move || -> Result<Arc<dyn Signer>, Error> {
473 if authority_name != "owner" {
474 if let Some(keypair_path) = arg_matches.value_of(authority_name) {
475 return signer_from_path_with_config(
476 arg_matches,
477 keypair_path,
478 authority_name,
479 wallet_manager,
480 &config,
481 )
482 .map(Arc::from)
483 .map_err(|e| e.to_string().into());
484 }
485 }
486
487 self.default_signer()
488 };
489
490 let authority = load_authority().unwrap_or_else(|e| {
491 eprintln!("error: {}", e);
492 exit(1);
493 });
494
495 let authority_address = authority.pubkey();
496 (authority, authority_address)
497 }
498
499 pub(crate) async fn get_account_checked(
500 &self,
501 account_pubkey: &Pubkey,
502 ) -> Result<RawAccount, Error> {
503 if let Ok(Some(account)) = self.program_client.get_account(*account_pubkey).await {
504 if self.program_id == account.owner {
505 Ok(account)
506 } else {
507 Err(format!(
508 "Account {} is owned by {}, not configured program id {}",
509 account_pubkey, account.owner, self.program_id
510 )
511 .into())
512 }
513 } else {
514 Err(format!("Account {} not found", account_pubkey).into())
515 }
516 }
517
518 pub(crate) async fn get_mint_info(
519 &self,
520 mint: &Pubkey,
521 mint_decimals: Option<u8>,
522 ) -> Result<MintInfo, Error> {
523 if self.sign_only {
524 Ok(MintInfo {
525 program_id: self.program_id,
526 address: *mint,
527 decimals: mint_decimals.unwrap_or_default(),
528 })
529 } else {
530 let account = self.get_account_checked(mint).await?;
531 let mint_account = StateWithExtensionsOwned::<Mint>::unpack(account.data)
532 .map_err(|_| format!("Could not find mint account {}", mint))?;
533 if let Some(decimals) = mint_decimals {
534 if decimals != mint_account.base.decimals {
535 return Err(format!(
536 "Mint {:?} has decimals {}, not configured decimals {}",
537 mint, mint_account.base.decimals, decimals
538 )
539 .into());
540 }
541 }
542 Ok(MintInfo {
543 program_id: account.owner,
544 address: *mint,
545 decimals: mint_account.base.decimals,
546 })
547 }
548 }
549
550 pub(crate) async fn check_account(
551 &self,
552 token_account: &Pubkey,
553 mint_address: Option<Pubkey>,
554 ) -> Result<Pubkey, Error> {
555 if !self.sign_only {
556 let account = self.get_account_checked(token_account).await?;
557 let source_account = StateWithExtensionsOwned::<Account>::unpack(account.data)
558 .map_err(|_| format!("Could not find token account {}", token_account))?;
559 let source_mint = source_account.base.mint;
560 if let Some(mint) = mint_address {
561 if source_mint != mint {
562 return Err(format!(
563 "Source {:?} does not contain {:?} tokens",
564 token_account, mint
565 )
566 .into());
567 }
568 }
569 Ok(source_mint)
570 } else {
571 Ok(mint_address.unwrap_or_default())
572 }
573 }
574}
575
576fn signer_from_path(
584 matches: &ArgMatches,
585 path: &str,
586 keypair_name: &str,
587 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
588) -> Result<Box<dyn Signer>, Box<dyn std::error::Error>> {
589 let config = SignerFromPathConfig::default();
590 signer_from_path_with_config(matches, path, keypair_name, wallet_manager, &config)
591}
592
593fn signer_from_path_with_config(
594 matches: &ArgMatches,
595 path: &str,
596 keypair_name: &str,
597 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
598 config: &SignerFromPathConfig,
599) -> Result<Box<dyn Signer>, Box<dyn std::error::Error>> {
600 if let Ok(pubkey) = Pubkey::from_str(path) {
601 if matches.try_contains_id(SIGNER_ARG.name).is_err()
602 && (config.allow_null_signer || matches.try_contains_id(SIGN_ONLY_ARG.name)?)
603 {
604 return Ok(Box::new(NullSigner::new(&pubkey)));
605 }
606 }
607
608 solana_clap_v3_utils::keypair::signer_from_path_with_config(
609 matches,
610 path,
611 keypair_name,
612 wallet_manager,
613 config,
614 )
615}