389 lines
14 KiB
Rust
389 lines
14 KiB
Rust
use crate::{signers_of, Error, MULTISIG_SIGNER_ARG};
|
|
use clap::ArgMatches;
|
|
use solana_clap_utils::{
|
|
input_parsers::{pubkey_of, pubkey_of_signer},
|
|
input_validators::normalize_to_url_if_moniker,
|
|
keypair::{signer_from_path, signer_from_path_with_config, SignerFromPathConfig},
|
|
nonce::{NONCE_ARG, NONCE_AUTHORITY_ARG},
|
|
offline::{DUMP_TRANSACTION_MESSAGE, SIGN_ONLY_ARG},
|
|
};
|
|
use solana_cli_output::OutputFormat;
|
|
use solana_client::nonblocking::rpc_client::RpcClient;
|
|
use solana_remote_wallet::remote_wallet::RemoteWalletManager;
|
|
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Signer};
|
|
use spl_associated_token_account::*;
|
|
use spl_token_2022::{
|
|
extension::StateWithExtensionsOwned,
|
|
state::{Account, Mint},
|
|
};
|
|
use spl_token_client::client::{ProgramClient, ProgramRpcClient, ProgramRpcClientSendTransaction};
|
|
use std::{process::exit, sync::Arc};
|
|
|
|
pub(crate) struct MintInfo {
|
|
pub program_id: Pubkey,
|
|
pub address: Pubkey,
|
|
pub decimals: u8,
|
|
}
|
|
|
|
pub(crate) struct Config<'a> {
|
|
pub(crate) default_signer: Arc<dyn Signer>,
|
|
pub(crate) rpc_client: Arc<RpcClient>,
|
|
pub(crate) program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>>,
|
|
pub(crate) websocket_url: String,
|
|
pub(crate) output_format: OutputFormat,
|
|
pub(crate) fee_payer: Pubkey,
|
|
pub(crate) nonce_account: Option<Pubkey>,
|
|
pub(crate) nonce_authority: Option<Pubkey>,
|
|
pub(crate) sign_only: bool,
|
|
pub(crate) dump_transaction_message: bool,
|
|
pub(crate) multisigner_pubkeys: Vec<&'a Pubkey>,
|
|
pub(crate) program_id: Pubkey,
|
|
}
|
|
|
|
impl<'a> Config<'a> {
|
|
pub(crate) fn new(
|
|
matches: &ArgMatches,
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
bulk_signers: &mut Vec<Arc<dyn Signer>>,
|
|
multisigner_ids: &'a mut Vec<Pubkey>,
|
|
) -> Self {
|
|
let cli_config = if let Some(config_file) = matches.value_of("config_file") {
|
|
solana_cli_config::Config::load(config_file).unwrap_or_else(|_| {
|
|
eprintln!("error: Could not find config file `{}`", config_file);
|
|
exit(1);
|
|
})
|
|
} else {
|
|
solana_cli_config::Config::default()
|
|
};
|
|
let json_rpc_url = normalize_to_url_if_moniker(
|
|
matches
|
|
.value_of("json_rpc_url")
|
|
.unwrap_or(&cli_config.json_rpc_url),
|
|
);
|
|
let websocket_url = solana_cli_config::Config::compute_websocket_url(&json_rpc_url);
|
|
let rpc_client = Arc::new(RpcClient::new_with_commitment(
|
|
json_rpc_url,
|
|
CommitmentConfig::confirmed(),
|
|
));
|
|
let program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>> = Arc::new(
|
|
ProgramRpcClient::new(rpc_client.clone(), ProgramRpcClientSendTransaction),
|
|
);
|
|
Self::new_with_clients_and_ws_url(
|
|
matches,
|
|
wallet_manager,
|
|
bulk_signers,
|
|
multisigner_ids,
|
|
rpc_client,
|
|
program_client,
|
|
websocket_url,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn new_with_clients_and_ws_url(
|
|
matches: &ArgMatches,
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
bulk_signers: &mut Vec<Arc<dyn Signer>>,
|
|
multisigner_ids: &'a mut Vec<Pubkey>,
|
|
rpc_client: Arc<RpcClient>,
|
|
program_client: Arc<dyn ProgramClient<ProgramRpcClientSendTransaction>>,
|
|
websocket_url: String,
|
|
) -> Self {
|
|
let cli_config = if let Some(config_file) = matches.value_of("config_file") {
|
|
solana_cli_config::Config::load(config_file).unwrap_or_else(|_| {
|
|
eprintln!("error: Could not find config file `{}`", config_file);
|
|
exit(1);
|
|
})
|
|
} else {
|
|
solana_cli_config::Config::default()
|
|
};
|
|
let multisig_signers = signers_of(matches, MULTISIG_SIGNER_ARG.name, wallet_manager)
|
|
.unwrap_or_else(|e| {
|
|
eprintln!("error: {}", e);
|
|
exit(1);
|
|
});
|
|
if let Some(mut multisig_signers) = multisig_signers {
|
|
multisig_signers.sort_by(|(_, lp), (_, rp)| lp.cmp(rp));
|
|
let (signers, pubkeys): (Vec<_>, Vec<_>) = multisig_signers.into_iter().unzip();
|
|
bulk_signers.extend(signers);
|
|
multisigner_ids.extend(pubkeys);
|
|
}
|
|
let multisigner_pubkeys = multisigner_ids.iter().collect::<Vec<_>>();
|
|
|
|
let config = SignerFromPathConfig {
|
|
allow_null_signer: !multisigner_pubkeys.is_empty(),
|
|
};
|
|
|
|
let default_keypair = cli_config.keypair_path.clone();
|
|
|
|
let default_signer: Arc<dyn Signer> = {
|
|
if let Some(owner_path) = matches.value_of("owner") {
|
|
signer_from_path_with_config(matches, owner_path, "owner", wallet_manager, &config)
|
|
} else {
|
|
signer_from_path_with_config(
|
|
matches,
|
|
&default_keypair,
|
|
"default",
|
|
wallet_manager,
|
|
&config,
|
|
)
|
|
}
|
|
}
|
|
.map(Arc::from)
|
|
.unwrap_or_else(|e| {
|
|
eprintln!("error: {}", e);
|
|
exit(1);
|
|
});
|
|
|
|
let (signer, fee_payer) = matches
|
|
.value_of("fee_payer")
|
|
.map_or(Ok(default_signer.clone()), |path| {
|
|
signer_from_path(matches, path, "fee_payer", wallet_manager).map(Arc::from)
|
|
})
|
|
.map(|s: Arc<dyn Signer>| {
|
|
let p = s.pubkey();
|
|
(s, p)
|
|
})
|
|
.unwrap_or_else(|e| {
|
|
eprintln!("error: {}", e);
|
|
exit(1);
|
|
});
|
|
bulk_signers.push(signer);
|
|
|
|
let verbose = matches.is_present("verbose");
|
|
let output_format = matches
|
|
.value_of("output_format")
|
|
.map(|value| match value {
|
|
"json" => OutputFormat::Json,
|
|
"json-compact" => OutputFormat::JsonCompact,
|
|
_ => unreachable!(),
|
|
})
|
|
.unwrap_or(if verbose {
|
|
OutputFormat::DisplayVerbose
|
|
} else {
|
|
OutputFormat::Display
|
|
});
|
|
|
|
let nonce_account = pubkey_of_signer(matches, NONCE_ARG.name, wallet_manager)
|
|
.unwrap_or_else(|e| {
|
|
eprintln!("error: {}", e);
|
|
exit(1);
|
|
});
|
|
let nonce_authority = if nonce_account.is_some() {
|
|
let (signer, nonce_authority) = signer_from_path(
|
|
matches,
|
|
matches
|
|
.value_of(NONCE_AUTHORITY_ARG.name)
|
|
.unwrap_or(&cli_config.keypair_path),
|
|
NONCE_AUTHORITY_ARG.name,
|
|
wallet_manager,
|
|
)
|
|
.map(Arc::from)
|
|
.map(|s: Arc<dyn Signer>| {
|
|
let p = s.pubkey();
|
|
(s, p)
|
|
})
|
|
.unwrap_or_else(|e| {
|
|
eprintln!("error: {}", e);
|
|
exit(1);
|
|
});
|
|
bulk_signers.push(signer);
|
|
|
|
Some(nonce_authority)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
|
|
let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name);
|
|
let program_id = pubkey_of(matches, "program_id").unwrap();
|
|
|
|
Self {
|
|
default_signer,
|
|
rpc_client,
|
|
program_client,
|
|
websocket_url,
|
|
output_format,
|
|
fee_payer,
|
|
nonce_account,
|
|
nonce_authority,
|
|
sign_only,
|
|
dump_transaction_message,
|
|
multisigner_pubkeys,
|
|
program_id,
|
|
}
|
|
}
|
|
|
|
// Check if an explicit token account address was provided, otherwise
|
|
// return the associated token address for the default address.
|
|
pub(crate) async fn associated_token_address_or_override(
|
|
&self,
|
|
arg_matches: &ArgMatches<'_>,
|
|
override_name: &str,
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
) -> Pubkey {
|
|
let token = pubkey_of_signer(arg_matches, "token", wallet_manager).unwrap();
|
|
self.associated_token_address_for_token_or_override(
|
|
arg_matches,
|
|
override_name,
|
|
wallet_manager,
|
|
token,
|
|
)
|
|
.await
|
|
}
|
|
|
|
// Check if an explicit token account address was provided, otherwise
|
|
// return the associated token address for the default address.
|
|
pub(crate) async fn associated_token_address_for_token_or_override(
|
|
&self,
|
|
arg_matches: &ArgMatches<'_>,
|
|
override_name: &str,
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
token: Option<Pubkey>,
|
|
) -> Pubkey {
|
|
if let Some(address) = pubkey_of_signer(arg_matches, override_name, wallet_manager).unwrap()
|
|
{
|
|
return address;
|
|
}
|
|
|
|
let token = token.unwrap();
|
|
let program_id = self.get_mint_info(&token, None).await.unwrap().program_id;
|
|
self.associated_token_address_for_token_and_program(&token, &program_id)
|
|
}
|
|
|
|
pub(crate) fn associated_token_address_for_token_and_program(
|
|
&self,
|
|
token: &Pubkey,
|
|
program_id: &Pubkey,
|
|
) -> Pubkey {
|
|
let owner = self.default_signer.pubkey();
|
|
get_associated_token_address_with_program_id(&owner, token, program_id)
|
|
}
|
|
|
|
// Checks if an explicit address was provided, otherwise return the default address.
|
|
pub(crate) fn pubkey_or_default(
|
|
&self,
|
|
arg_matches: &ArgMatches,
|
|
address_name: &str,
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
) -> Pubkey {
|
|
if address_name != "owner" {
|
|
if let Some(address) =
|
|
pubkey_of_signer(arg_matches, address_name, wallet_manager).unwrap()
|
|
{
|
|
return address;
|
|
}
|
|
}
|
|
|
|
self.default_signer.pubkey()
|
|
}
|
|
|
|
// Checks if an explicit signer was provided, otherwise return the default signer.
|
|
pub(crate) fn signer_or_default(
|
|
&self,
|
|
arg_matches: &ArgMatches,
|
|
authority_name: &str,
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
) -> (Arc<dyn Signer>, Pubkey) {
|
|
// If there are `--multisig-signers` on the command line, allow `NullSigner`s to
|
|
// be returned for multisig account addresses
|
|
let config = SignerFromPathConfig {
|
|
allow_null_signer: !self.multisigner_pubkeys.is_empty(),
|
|
};
|
|
let mut load_authority = move || -> Result<Arc<dyn Signer>, _> {
|
|
if authority_name != "owner" {
|
|
if let Some(keypair_path) = arg_matches.value_of(authority_name) {
|
|
return signer_from_path_with_config(
|
|
arg_matches,
|
|
keypair_path,
|
|
authority_name,
|
|
wallet_manager,
|
|
&config,
|
|
)
|
|
.map(Arc::from);
|
|
}
|
|
}
|
|
|
|
Ok(self.default_signer.clone())
|
|
};
|
|
|
|
let authority = load_authority().unwrap_or_else(|e| {
|
|
eprintln!("error: {}", e);
|
|
exit(1);
|
|
});
|
|
|
|
let authority_address = authority.pubkey();
|
|
(authority, authority_address)
|
|
}
|
|
|
|
pub(crate) async fn get_mint_info(
|
|
&self,
|
|
mint: &Pubkey,
|
|
mint_decimals: Option<u8>,
|
|
) -> Result<MintInfo, Error> {
|
|
if self.sign_only {
|
|
Ok(MintInfo {
|
|
program_id: self.program_id,
|
|
address: *mint,
|
|
decimals: mint_decimals.unwrap_or_default(),
|
|
})
|
|
} else {
|
|
let account = self.rpc_client.get_account(mint).await?;
|
|
self.check_owner(mint, &account.owner)?;
|
|
let mint_account = StateWithExtensionsOwned::<Mint>::unpack(account.data)
|
|
.map_err(|_| format!("Could not find mint account {}", mint))?;
|
|
if let Some(decimals) = mint_decimals {
|
|
if decimals != mint_account.base.decimals {
|
|
return Err(format!(
|
|
"Mint {:?} has decimals {}, not configured decimals {}",
|
|
mint, mint_account.base.decimals, decimals
|
|
)
|
|
.into());
|
|
}
|
|
}
|
|
Ok(MintInfo {
|
|
program_id: account.owner,
|
|
address: *mint,
|
|
decimals: mint_account.base.decimals,
|
|
})
|
|
}
|
|
}
|
|
|
|
pub(crate) fn check_owner(&self, account: &Pubkey, owner: &Pubkey) -> Result<(), Error> {
|
|
if self.program_id != *owner {
|
|
Err(format!(
|
|
"Account {:?} is owned by {}, not configured program id {}",
|
|
account, owner, self.program_id
|
|
)
|
|
.into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn check_account(
|
|
&self,
|
|
token_account: &Pubkey,
|
|
mint_address: Option<Pubkey>,
|
|
) -> Result<Pubkey, Error> {
|
|
if !self.sign_only {
|
|
let account = self.rpc_client.get_account(token_account).await?;
|
|
let source_account = StateWithExtensionsOwned::<Account>::unpack(account.data)
|
|
.map_err(|_| format!("Could not find token account {}", token_account))?;
|
|
let source_mint = source_account.base.mint;
|
|
if let Some(mint) = mint_address {
|
|
if source_mint != mint {
|
|
return Err(format!(
|
|
"Source {:?} does not contain {:?} tokens",
|
|
token_account, mint
|
|
)
|
|
.into());
|
|
}
|
|
}
|
|
self.check_owner(token_account, &account.owner)?;
|
|
Ok(source_mint)
|
|
} else {
|
|
Ok(mint_address.unwrap_or_default())
|
|
}
|
|
}
|
|
}
|