spl_token_cli/
config.rs

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    // Returns Ok(default signer), or Err if there is no default signer configured
368    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    // Returns Ok(fee payer), or Err if there is no fee payer configured
380    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    // Check if an explicit token account address was provided, otherwise
392    // return the associated token address for the default address.
393    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    // Check if an explicit token account address was provided, otherwise
411    // return the associated token address for the default address.
412    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    // Checks if an explicit address was provided, otherwise return the default
443    // address if there is one
444    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    // Checks if an explicit signer was provided, otherwise return the default
460    // signer.
461    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        // If there are `--multisig-signers` on the command line, allow `NullSigner`s to
468        // be returned for multisig account addresses
469        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
576// In clap v2, `value_of` returns `None` if the argument id is not previously
577// specified in `Arg`. In contrast, in clap v3, `value_of` panics in this case.
578// Therefore, compared to the same function in solana-clap-utils,
579// `signer_from_path` in solana-clap-v3-utils errors early when `path` is a
580// valid pubkey, but `SIGNER_ARG.name` is not specified in the args.
581// This function behaves exactly as `signer_from_path` from solana-clap-utils by
582// catching this special case.
583fn 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}