cli: Add subcommands for address lookup tables (#27123)

* cli: Add subcommand for creating address lookup tables

* cli: Add additional subcommands for address lookup tables

* short commands
This commit is contained in:
Justin Starry 2022-08-18 22:12:53 +01:00 committed by GitHub
parent 0b54b22f58
commit d8380e4d4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1135 additions and 4 deletions

1
Cargo.lock generated
View File

@ -4874,6 +4874,7 @@ dependencies = [
"serde_derive",
"serde_json",
"solana-account-decoder",
"solana-address-lookup-table-program",
"solana-bpf-loader-program",
"solana-clap-utils",
"solana-cli-config",

View File

@ -2111,6 +2111,75 @@ impl fmt::Display for CliUpgradeableBuffers {
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CliAddressLookupTable {
pub lookup_table_address: String,
pub authority: Option<String>,
pub deactivation_slot: u64,
pub last_extended_slot: u64,
pub addresses: Vec<String>,
}
impl QuietDisplay for CliAddressLookupTable {}
impl VerboseDisplay for CliAddressLookupTable {}
impl fmt::Display for CliAddressLookupTable {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f)?;
writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
if let Some(authority) = &self.authority {
writeln_name_value(f, "Authority:", authority)?;
} else {
writeln_name_value(f, "Authority:", "None (frozen)")?;
}
if self.deactivation_slot == u64::MAX {
writeln_name_value(f, "Deactivation Slot:", "None (still active)")?;
} else {
writeln_name_value(f, "Deactivation Slot:", &self.deactivation_slot.to_string())?;
}
if self.last_extended_slot == 0 {
writeln_name_value(f, "Last Extended Slot:", "None (empty)")?;
} else {
writeln_name_value(
f,
"Last Extended Slot:",
&self.last_extended_slot.to_string(),
)?;
}
if self.addresses.is_empty() {
writeln_name_value(f, "Address Table Entries:", "None (empty)")?;
} else {
writeln!(f, "{}", style("Address Table Entries:".to_string()).bold())?;
writeln!(f)?;
writeln!(
f,
"{}",
style(format!(" {:<5} {}", "Index", "Address")).bold()
)?;
for (index, address) in self.addresses.iter().enumerate() {
writeln!(f, " {:<5} {}", index, address)?;
}
}
Ok(())
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliAddressLookupTableCreated {
pub lookup_table_address: String,
pub signature: String,
}
impl QuietDisplay for CliAddressLookupTableCreated {}
impl VerboseDisplay for CliAddressLookupTableCreated {}
impl fmt::Display for CliAddressLookupTableCreated {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f)?;
writeln_name_value(f, "Signature:", &self.signature)?;
writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
Ok(())
}
}
#[derive(Debug, Default)]
pub struct ReturnSignersConfig {
pub dump_transaction_message: bool,

View File

@ -28,6 +28,7 @@ serde = "1.0.143"
serde_derive = "1.0.103"
serde_json = "1.0.83"
solana-account-decoder = { path = "../account-decoder", version = "=1.12.0" }
solana-address-lookup-table-program = { path = "../programs/address-lookup-table", version = "=1.12.0" }
solana-bpf-loader-program = { path = "../programs/bpf_loader", version = "=1.12.0" }
solana-clap-utils = { path = "../clap-utils", version = "=1.12.0" }
solana-cli-config = { path = "../cli-config", version = "=1.12.0" }

View File

@ -0,0 +1,832 @@
use {
crate::cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult},
clap::{App, AppSettings, Arg, ArgMatches, SubCommand},
solana_address_lookup_table_program::{
instruction::{
close_lookup_table, create_lookup_table, deactivate_lookup_table, extend_lookup_table,
freeze_lookup_table,
},
state::AddressLookupTable,
},
solana_clap_utils::{self, input_parsers::*, input_validators::*, keypair::*},
solana_cli_output::{CliAddressLookupTable, CliAddressLookupTableCreated, CliSignature},
solana_client::{rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig},
solana_remote_wallet::remote_wallet::RemoteWalletManager,
solana_sdk::{
account::from_account, clock::Clock, commitment_config::CommitmentConfig, message::Message,
pubkey::Pubkey, sysvar, transaction::Transaction,
},
std::sync::Arc,
};
#[derive(Debug, PartialEq, Eq)]
pub enum AddressLookupTableCliCommand {
CreateLookupTable {
authority_signer_index: SignerIndex,
payer_signer_index: SignerIndex,
},
FreezeLookupTable {
lookup_table_pubkey: Pubkey,
authority_signer_index: SignerIndex,
bypass_warning: bool,
},
ExtendLookupTable {
lookup_table_pubkey: Pubkey,
authority_signer_index: SignerIndex,
payer_signer_index: SignerIndex,
new_addresses: Vec<Pubkey>,
},
DeactivateLookupTable {
lookup_table_pubkey: Pubkey,
authority_signer_index: SignerIndex,
bypass_warning: bool,
},
CloseLookupTable {
lookup_table_pubkey: Pubkey,
authority_signer_index: SignerIndex,
recipient_pubkey: Pubkey,
},
ShowLookupTable {
lookup_table_pubkey: Pubkey,
},
}
pub trait AddressLookupTableSubCommands {
fn address_lookup_table_subcommands(self) -> Self;
}
impl AddressLookupTableSubCommands for App<'_, '_> {
fn address_lookup_table_subcommands(self) -> Self {
self.subcommand(
SubCommand::with_name("address-lookup-table")
.about("Address lookup table management")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("create")
.about("Create a lookup table")
.arg(
Arg::with_name("authority")
.long("authority")
.value_name("AUTHORITY_SIGNER")
.takes_value(true)
.validator(is_valid_signer)
.help("Lookup table authority [default: the default configured keypair]")
)
.arg(
Arg::with_name("payer")
.long("payer")
.value_name("PAYER_SIGNER")
.takes_value(true)
.validator(is_valid_signer)
.help("Account that will pay rent fees for the created lookup table [default: the default configured keypair]")
)
)
.subcommand(
SubCommand::with_name("freeze")
.about("Permanently freezes a lookup table")
.arg(
Arg::with_name("lookup_table_address")
.index(1)
.value_name("LOOKUP_TABLE_ADDRESS")
.takes_value(true)
.required(true)
.validator(is_pubkey)
.help("Address of the lookup table")
)
.arg(
Arg::with_name("authority")
.long("authority")
.value_name("AUTHORITY_SIGNER")
.takes_value(true)
.validator(is_valid_signer)
.help("Lookup table authority [default: the default configured keypair]")
)
.arg(
Arg::with_name("bypass_warning")
.long("bypass-warning")
.takes_value(false)
.help("Bypass the permanent lookup table freeze warning"),
),
)
.subcommand(
SubCommand::with_name("extend")
.about("Append more addresses to a lookup table")
.arg(
Arg::with_name("lookup_table_address")
.index(1)
.value_name("LOOKUP_TABLE_ADDRESS")
.takes_value(true)
.required(true)
.validator(is_pubkey)
.help("Address of the lookup table")
)
.arg(
Arg::with_name("authority")
.long("authority")
.value_name("AUTHORITY_SIGNER")
.takes_value(true)
.validator(is_valid_signer)
.help("Lookup table authority [default: the default configured keypair]")
)
.arg(
Arg::with_name("payer")
.long("payer")
.value_name("PAYER_SIGNER")
.takes_value(true)
.validator(is_valid_signer)
.help("Account that will pay rent fees for the extended lookup table [default: the default configured keypair]")
)
.arg(
Arg::with_name("addresses")
.long("addresses")
.value_name("ADDRESS_1,ADDRESS_2")
.takes_value(true)
.use_delimiter(true)
.required(true)
.validator(is_pubkey)
.help("Comma separated list of addresses to append")
)
)
.subcommand(
SubCommand::with_name("deactivate")
.about("Permanently deactivates a lookup table")
.arg(
Arg::with_name("lookup_table_address")
.index(1)
.value_name("LOOKUP_TABLE_ADDRESS")
.takes_value(true)
.required(true)
.help("Address of the lookup table")
)
.arg(
Arg::with_name("authority")
.long("authority")
.value_name("AUTHORITY_SIGNER")
.takes_value(true)
.validator(is_valid_signer)
.help("Lookup table authority [default: the default configured keypair]")
)
.arg(
Arg::with_name("bypass_warning")
.long("bypass-warning")
.takes_value(false)
.help("Bypass the permanent lookup table deactivation warning"),
),
)
.subcommand(
SubCommand::with_name("close")
.about("Permanently closes a lookup table")
.arg(
Arg::with_name("lookup_table_address")
.index(1)
.value_name("LOOKUP_TABLE_ADDRESS")
.takes_value(true)
.required(true)
.help("Address of the lookup table")
)
.arg(
Arg::with_name("recipient")
.long("recipient")
.value_name("RECIPIENT_ADDRESS")
.takes_value(true)
.validator(is_pubkey)
.help("Address of the recipient account to deposit the closed account's lamports [default: the default configured keypair]")
)
.arg(
Arg::with_name("authority")
.long("authority")
.value_name("AUTHORITY_SIGNER")
.takes_value(true)
.validator(is_valid_signer)
.help("Lookup table authority [default: the default configured keypair]")
)
)
.subcommand(
SubCommand::with_name("get")
.about("Display information about a lookup table")
.arg(
Arg::with_name("lookup_table_address")
.index(1)
.value_name("LOOKUP_TABLE_ADDRESS")
.takes_value(true)
.help("Address of the lookup table to show")
)
)
)
}
}
pub fn parse_address_lookup_table_subcommand(
matches: &ArgMatches<'_>,
default_signer: &DefaultSigner,
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
) -> Result<CliCommandInfo, CliError> {
let (subcommand, sub_matches) = matches.subcommand();
let response = match (subcommand, sub_matches) {
("create", Some(matches)) => {
let mut bulk_signers = vec![Some(
default_signer.signer_from_path(matches, wallet_manager)?,
)];
let authority_pubkey = if let Ok((authority_signer, Some(authority_pubkey))) =
signer_of(matches, "authority", wallet_manager)
{
bulk_signers.push(authority_signer);
Some(authority_pubkey)
} else {
Some(
default_signer
.signer_from_path(matches, wallet_manager)?
.pubkey(),
)
};
let payer_pubkey = if let Ok((payer_signer, Some(payer_pubkey))) =
signer_of(matches, "payer", wallet_manager)
{
bulk_signers.push(payer_signer);
Some(payer_pubkey)
} else {
Some(
default_signer
.signer_from_path(matches, wallet_manager)?
.pubkey(),
)
};
let signer_info =
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
CliCommandInfo {
command: CliCommand::AddressLookupTable(
AddressLookupTableCliCommand::CreateLookupTable {
authority_signer_index: signer_info.index_of(authority_pubkey).unwrap(),
payer_signer_index: signer_info.index_of(payer_pubkey).unwrap(),
},
),
signers: signer_info.signers,
}
}
("freeze", Some(matches)) => {
let lookup_table_pubkey = pubkey_of(matches, "lookup_table_address").unwrap();
let mut bulk_signers = vec![Some(
default_signer.signer_from_path(matches, wallet_manager)?,
)];
let authority_pubkey = if let Ok((authority_signer, Some(authority_pubkey))) =
signer_of(matches, "authority", wallet_manager)
{
bulk_signers.push(authority_signer);
Some(authority_pubkey)
} else {
Some(
default_signer
.signer_from_path(matches, wallet_manager)?
.pubkey(),
)
};
let signer_info =
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
CliCommandInfo {
command: CliCommand::AddressLookupTable(
AddressLookupTableCliCommand::FreezeLookupTable {
lookup_table_pubkey,
authority_signer_index: signer_info.index_of(authority_pubkey).unwrap(),
bypass_warning: matches.is_present("bypass_warning"),
},
),
signers: signer_info.signers,
}
}
("extend", Some(matches)) => {
let lookup_table_pubkey = pubkey_of(matches, "lookup_table_address").unwrap();
let mut bulk_signers = vec![Some(
default_signer.signer_from_path(matches, wallet_manager)?,
)];
let authority_pubkey = if let Ok((authority_signer, Some(authority_pubkey))) =
signer_of(matches, "authority", wallet_manager)
{
bulk_signers.push(authority_signer);
Some(authority_pubkey)
} else {
Some(
default_signer
.signer_from_path(matches, wallet_manager)?
.pubkey(),
)
};
let payer_pubkey = if let Ok((payer_signer, Some(payer_pubkey))) =
signer_of(matches, "payer", wallet_manager)
{
bulk_signers.push(payer_signer);
Some(payer_pubkey)
} else {
Some(
default_signer
.signer_from_path(matches, wallet_manager)?
.pubkey(),
)
};
let new_addresses: Vec<Pubkey> = values_of(matches, "addresses").unwrap();
let signer_info =
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
CliCommandInfo {
command: CliCommand::AddressLookupTable(
AddressLookupTableCliCommand::ExtendLookupTable {
lookup_table_pubkey,
authority_signer_index: signer_info.index_of(authority_pubkey).unwrap(),
payer_signer_index: signer_info.index_of(payer_pubkey).unwrap(),
new_addresses,
},
),
signers: signer_info.signers,
}
}
("deactivate", Some(matches)) => {
let lookup_table_pubkey = pubkey_of(matches, "lookup_table_address").unwrap();
let mut bulk_signers = vec![Some(
default_signer.signer_from_path(matches, wallet_manager)?,
)];
let authority_pubkey = if let Ok((authority_signer, Some(authority_pubkey))) =
signer_of(matches, "authority", wallet_manager)
{
bulk_signers.push(authority_signer);
Some(authority_pubkey)
} else {
Some(
default_signer
.signer_from_path(matches, wallet_manager)?
.pubkey(),
)
};
let signer_info =
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
CliCommandInfo {
command: CliCommand::AddressLookupTable(
AddressLookupTableCliCommand::DeactivateLookupTable {
lookup_table_pubkey,
authority_signer_index: signer_info.index_of(authority_pubkey).unwrap(),
bypass_warning: matches.is_present("bypass_warning"),
},
),
signers: signer_info.signers,
}
}
("close", Some(matches)) => {
let lookup_table_pubkey = pubkey_of(matches, "lookup_table_address").unwrap();
let mut bulk_signers = vec![Some(
default_signer.signer_from_path(matches, wallet_manager)?,
)];
let authority_pubkey = if let Ok((authority_signer, Some(authority_pubkey))) =
signer_of(matches, "authority", wallet_manager)
{
bulk_signers.push(authority_signer);
Some(authority_pubkey)
} else {
Some(
default_signer
.signer_from_path(matches, wallet_manager)?
.pubkey(),
)
};
let recipient_pubkey = if let Some(recipient_pubkey) = pubkey_of(matches, "recipient") {
recipient_pubkey
} else {
default_signer
.signer_from_path(matches, wallet_manager)?
.pubkey()
};
let signer_info =
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
CliCommandInfo {
command: CliCommand::AddressLookupTable(
AddressLookupTableCliCommand::CloseLookupTable {
lookup_table_pubkey,
authority_signer_index: signer_info.index_of(authority_pubkey).unwrap(),
recipient_pubkey,
},
),
signers: signer_info.signers,
}
}
("get", Some(matches)) => {
let lookup_table_pubkey = pubkey_of(matches, "lookup_table_address").unwrap();
CliCommandInfo {
command: CliCommand::AddressLookupTable(
AddressLookupTableCliCommand::ShowLookupTable {
lookup_table_pubkey,
},
),
signers: vec![],
}
}
_ => unreachable!(),
};
Ok(response)
}
pub fn process_address_lookup_table_subcommand(
rpc_client: Arc<RpcClient>,
config: &CliConfig,
subcommand: &AddressLookupTableCliCommand,
) -> ProcessResult {
match subcommand {
AddressLookupTableCliCommand::CreateLookupTable {
authority_signer_index,
payer_signer_index,
} => process_create_lookup_table(
&rpc_client,
config,
*authority_signer_index,
*payer_signer_index,
),
AddressLookupTableCliCommand::FreezeLookupTable {
lookup_table_pubkey,
authority_signer_index,
bypass_warning,
} => process_freeze_lookup_table(
&rpc_client,
config,
*lookup_table_pubkey,
*authority_signer_index,
*bypass_warning,
),
AddressLookupTableCliCommand::ExtendLookupTable {
lookup_table_pubkey,
authority_signer_index,
payer_signer_index,
new_addresses,
} => process_extend_lookup_table(
&rpc_client,
config,
*lookup_table_pubkey,
*authority_signer_index,
*payer_signer_index,
new_addresses.to_vec(),
),
AddressLookupTableCliCommand::DeactivateLookupTable {
lookup_table_pubkey,
authority_signer_index,
bypass_warning,
} => process_deactivate_lookup_table(
&rpc_client,
config,
*lookup_table_pubkey,
*authority_signer_index,
*bypass_warning,
),
AddressLookupTableCliCommand::CloseLookupTable {
lookup_table_pubkey,
authority_signer_index,
recipient_pubkey,
} => process_close_lookup_table(
&rpc_client,
config,
*lookup_table_pubkey,
*authority_signer_index,
*recipient_pubkey,
),
AddressLookupTableCliCommand::ShowLookupTable {
lookup_table_pubkey,
} => process_show_lookup_table(&rpc_client, config, *lookup_table_pubkey),
}
}
fn process_create_lookup_table(
rpc_client: &RpcClient,
config: &CliConfig,
authority_signer_index: usize,
payer_signer_index: usize,
) -> ProcessResult {
let authority_signer = config.signers[authority_signer_index];
let payer_signer = config.signers[payer_signer_index];
let get_clock_result = rpc_client
.get_account_with_commitment(&sysvar::clock::id(), CommitmentConfig::finalized())?;
let clock_account = get_clock_result.value.expect("Clock account doesn't exist");
let clock: Clock = from_account(&clock_account).ok_or_else(|| {
CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string())
})?;
let authority_address = authority_signer.pubkey();
let payer_address = payer_signer.pubkey();
let (create_lookup_table_ix, lookup_table_address) =
create_lookup_table(authority_address, payer_address, clock.slot);
let blockhash = rpc_client.get_latest_blockhash()?;
let mut tx = Transaction::new_unsigned(Message::new(
&[create_lookup_table_ix],
Some(&config.signers[0].pubkey()),
));
tx.try_sign(
&[config.signers[0], authority_signer, payer_signer],
blockhash,
)?;
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
config.commitment,
RpcSendTransactionConfig {
skip_preflight: false,
preflight_commitment: Some(config.commitment.commitment),
..RpcSendTransactionConfig::default()
},
);
match result {
Err(err) => Err(format!("Create failed: {}", err).into()),
Ok(signature) => Ok(config
.output_format
.formatted_string(&CliAddressLookupTableCreated {
lookup_table_address: lookup_table_address.to_string(),
signature: signature.to_string(),
})),
}
}
pub const FREEZE_LOOKUP_TABLE_WARNING: &str = "WARNING! \
Once a lookup table is frozen, it can never be modified or unfrozen again. \
To proceed with freezing, rerun the `freeze` command with the `--bypass-warning` flag";
fn process_freeze_lookup_table(
rpc_client: &RpcClient,
config: &CliConfig,
lookup_table_pubkey: Pubkey,
authority_signer_index: usize,
bypass_warning: bool,
) -> ProcessResult {
let authority_signer = config.signers[authority_signer_index];
let get_lookup_table_result =
rpc_client.get_account_with_commitment(&lookup_table_pubkey, config.commitment)?;
let lookup_table_account = get_lookup_table_result.value.ok_or_else(|| {
format!("Lookup table account {lookup_table_pubkey} not found, was it already closed?")
})?;
if !solana_address_lookup_table_program::check_id(&lookup_table_account.owner) {
return Err(format!(
"Lookup table account {lookup_table_pubkey} is not owned by the Address Lookup Table program",
)
.into());
}
if !bypass_warning {
return Err(String::from(FREEZE_LOOKUP_TABLE_WARNING).into());
}
let authority_address = authority_signer.pubkey();
let freeze_lookup_table_ix = freeze_lookup_table(lookup_table_pubkey, authority_address);
let blockhash = rpc_client.get_latest_blockhash()?;
let mut tx = Transaction::new_unsigned(Message::new(
&[freeze_lookup_table_ix],
Some(&config.signers[0].pubkey()),
));
tx.try_sign(&[config.signers[0], authority_signer], blockhash)?;
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
config.commitment,
RpcSendTransactionConfig {
skip_preflight: false,
preflight_commitment: Some(config.commitment.commitment),
..RpcSendTransactionConfig::default()
},
);
match result {
Err(err) => Err(format!("Freeze failed: {}", err).into()),
Ok(signature) => Ok(config.output_format.formatted_string(&CliSignature {
signature: signature.to_string(),
})),
}
}
fn process_extend_lookup_table(
rpc_client: &RpcClient,
config: &CliConfig,
lookup_table_pubkey: Pubkey,
authority_signer_index: usize,
payer_signer_index: usize,
new_addresses: Vec<Pubkey>,
) -> ProcessResult {
let authority_signer = config.signers[authority_signer_index];
let payer_signer = config.signers[payer_signer_index];
if new_addresses.is_empty() {
return Err("Lookup tables must be extended by at least one address".into());
}
let get_lookup_table_result =
rpc_client.get_account_with_commitment(&lookup_table_pubkey, config.commitment)?;
let lookup_table_account = get_lookup_table_result.value.ok_or_else(|| {
format!("Lookup table account {lookup_table_pubkey} not found, was it already closed?")
})?;
if !solana_address_lookup_table_program::check_id(&lookup_table_account.owner) {
return Err(format!(
"Lookup table account {lookup_table_pubkey} is not owned by the Address Lookup Table program",
)
.into());
}
let authority_address = authority_signer.pubkey();
let payer_address = payer_signer.pubkey();
let extend_lookup_table_ix = extend_lookup_table(
lookup_table_pubkey,
authority_address,
Some(payer_address),
new_addresses,
);
let blockhash = rpc_client.get_latest_blockhash()?;
let mut tx = Transaction::new_unsigned(Message::new(
&[extend_lookup_table_ix],
Some(&config.signers[0].pubkey()),
));
tx.try_sign(&[config.signers[0], authority_signer], blockhash)?;
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
config.commitment,
RpcSendTransactionConfig {
skip_preflight: false,
preflight_commitment: Some(config.commitment.commitment),
..RpcSendTransactionConfig::default()
},
);
match result {
Err(err) => Err(format!("Extend failed: {}", err).into()),
Ok(signature) => Ok(config.output_format.formatted_string(&CliSignature {
signature: signature.to_string(),
})),
}
}
pub const DEACTIVATE_LOOKUP_TABLE_WARNING: &str = "WARNING! \
Once a lookup table is deactivated, it is no longer usable by transactions.
Deactivated lookup tables may only be closed and cannot be recreated at the same address. \
To proceed with deactivation, rerun the `deactivate` command with the `--bypass-warning` flag";
fn process_deactivate_lookup_table(
rpc_client: &RpcClient,
config: &CliConfig,
lookup_table_pubkey: Pubkey,
authority_signer_index: usize,
bypass_warning: bool,
) -> ProcessResult {
let authority_signer = config.signers[authority_signer_index];
let get_lookup_table_result =
rpc_client.get_account_with_commitment(&lookup_table_pubkey, config.commitment)?;
let lookup_table_account = get_lookup_table_result.value.ok_or_else(|| {
format!("Lookup table account {lookup_table_pubkey} not found, was it already closed?")
})?;
if !solana_address_lookup_table_program::check_id(&lookup_table_account.owner) {
return Err(format!(
"Lookup table account {lookup_table_pubkey} is not owned by the Address Lookup Table program",
)
.into());
}
if !bypass_warning {
return Err(String::from(DEACTIVATE_LOOKUP_TABLE_WARNING).into());
}
let authority_address = authority_signer.pubkey();
let deactivate_lookup_table_ix =
deactivate_lookup_table(lookup_table_pubkey, authority_address);
let blockhash = rpc_client.get_latest_blockhash()?;
let mut tx = Transaction::new_unsigned(Message::new(
&[deactivate_lookup_table_ix],
Some(&config.signers[0].pubkey()),
));
tx.try_sign(&[config.signers[0], authority_signer], blockhash)?;
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
config.commitment,
RpcSendTransactionConfig {
skip_preflight: false,
preflight_commitment: Some(config.commitment.commitment),
..RpcSendTransactionConfig::default()
},
);
match result {
Err(err) => Err(format!("Deactivate failed: {}", err).into()),
Ok(signature) => Ok(config.output_format.formatted_string(&CliSignature {
signature: signature.to_string(),
})),
}
}
fn process_close_lookup_table(
rpc_client: &RpcClient,
config: &CliConfig,
lookup_table_pubkey: Pubkey,
authority_signer_index: usize,
recipient_pubkey: Pubkey,
) -> ProcessResult {
let authority_signer = config.signers[authority_signer_index];
let get_lookup_table_result =
rpc_client.get_account_with_commitment(&lookup_table_pubkey, config.commitment)?;
let lookup_table_account = get_lookup_table_result.value.ok_or_else(|| {
format!("Lookup table account {lookup_table_pubkey} not found, was it already closed?")
})?;
if !solana_address_lookup_table_program::check_id(&lookup_table_account.owner) {
return Err(format!(
"Lookup table account {lookup_table_pubkey} is not owned by the Address Lookup Table program",
)
.into());
}
let lookup_table_account = AddressLookupTable::deserialize(&lookup_table_account.data)?;
if lookup_table_account.meta.deactivation_slot == u64::MAX {
return Err(format!(
"Lookup table account {lookup_table_pubkey} is not deactivated. Only deactivated lookup tables may be closed",
)
.into());
}
let authority_address = authority_signer.pubkey();
let close_lookup_table_ix =
close_lookup_table(lookup_table_pubkey, authority_address, recipient_pubkey);
let blockhash = rpc_client.get_latest_blockhash()?;
let mut tx = Transaction::new_unsigned(Message::new(
&[close_lookup_table_ix],
Some(&config.signers[0].pubkey()),
));
tx.try_sign(&[config.signers[0], authority_signer], blockhash)?;
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
&tx,
config.commitment,
RpcSendTransactionConfig {
skip_preflight: false,
preflight_commitment: Some(config.commitment.commitment),
..RpcSendTransactionConfig::default()
},
);
match result {
Err(err) => Err(format!("Close failed: {}", err).into()),
Ok(signature) => Ok(config.output_format.formatted_string(&CliSignature {
signature: signature.to_string(),
})),
}
}
fn process_show_lookup_table(
rpc_client: &RpcClient,
config: &CliConfig,
lookup_table_pubkey: Pubkey,
) -> ProcessResult {
let get_lookup_table_result =
rpc_client.get_account_with_commitment(&lookup_table_pubkey, config.commitment)?;
let lookup_table_account = get_lookup_table_result.value.ok_or_else(|| {
format!("Lookup table account {lookup_table_pubkey} not found, was it already closed?")
})?;
if !solana_address_lookup_table_program::check_id(&lookup_table_account.owner) {
return Err(format!(
"Lookup table account {lookup_table_pubkey} is not owned by the Address Lookup Table program",
)
.into());
}
let lookup_table_account = AddressLookupTable::deserialize(&lookup_table_account.data)?;
Ok(config
.output_format
.formatted_string(&CliAddressLookupTable {
lookup_table_address: lookup_table_pubkey.to_string(),
authority: lookup_table_account
.meta
.authority
.as_ref()
.map(ToString::to_string),
deactivation_slot: lookup_table_account.meta.deactivation_slot,
last_extended_slot: lookup_table_account.meta.last_extended_slot,
addresses: lookup_table_account
.addresses
.iter()
.map(ToString::to_string)
.collect(),
}))
}

View File

@ -1,7 +1,7 @@
use {
crate::{
cli::*, cluster_query::*, feature::*, inflation::*, nonce::*, program::*, stake::*,
validator_info::*, vote::*, wallet::*,
address_lookup_table::AddressLookupTableSubCommands, cli::*, cluster_query::*, feature::*,
inflation::*, nonce::*, program::*, stake::*, validator_info::*, vote::*, wallet::*,
},
clap::{App, AppSettings, Arg, ArgGroup, SubCommand},
solana_clap_utils::{self, input_validators::*, keypair::*},
@ -130,6 +130,7 @@ pub fn get_clap_app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> A
.inflation_subcommands()
.nonce_subcommands()
.program_subcommands()
.address_lookup_table_subcommands()
.stake_subcommands()
.validator_info_subcommands()
.vote_subcommands()

View File

@ -1,7 +1,7 @@
use {
crate::{
clap_app::*, cluster_query::*, feature::*, inflation::*, nonce::*, program::*,
spend_utils::*, stake::*, validator_info::*, vote::*, wallet::*,
address_lookup_table::*, clap_app::*, cluster_query::*, feature::*, inflation::*, nonce::*,
program::*, spend_utils::*, stake::*, validator_info::*, vote::*, wallet::*,
},
clap::{crate_description, crate_name, value_t_or_exit, ArgMatches, Shell},
log::*,
@ -440,6 +440,8 @@ pub enum CliCommand {
StakeMinimumDelegation {
use_lamports_unit: bool,
},
// Address lookup table commands
AddressLookupTable(AddressLookupTableCliCommand),
}
#[derive(Debug, PartialEq)]
@ -687,6 +689,9 @@ pub fn parse_command(
("program", Some(matches)) => {
parse_program_subcommand(matches, default_signer, wallet_manager)
}
("address-lookup-table", Some(matches)) => {
parse_address_lookup_table_subcommand(matches, default_signer, wallet_manager)
}
("wait-for-max-stake", Some(matches)) => {
let max_stake_percent = value_t_or_exit!(matches, "max_percent", f32);
Ok(CliCommandInfo {
@ -1627,6 +1632,11 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
derived_address_program_id.as_ref(),
compute_unit_price.as_ref(),
),
// Address Lookup Table Commands
CliCommand::AddressLookupTable(subcommand) => {
process_address_lookup_table_subcommand(rpc_client, config, subcommand)
}
}
}

View File

@ -23,6 +23,7 @@ extern crate const_format;
extern crate serde_derive;
pub mod address_lookup_table;
pub mod checks;
pub mod clap_app;
pub mod cli;

View File

@ -0,0 +1,216 @@
use {
solana_cli::{
address_lookup_table::{
AddressLookupTableCliCommand, DEACTIVATE_LOOKUP_TABLE_WARNING,
FREEZE_LOOKUP_TABLE_WARNING,
},
cli::{process_command, CliCommand, CliConfig},
},
solana_cli_output::{CliAddressLookupTable, CliAddressLookupTableCreated, OutputFormat},
solana_faucet::faucet::run_local_faucet,
solana_sdk::{
native_token::LAMPORTS_PER_SOL,
pubkey::Pubkey,
signature::{Keypair, Signer},
},
solana_streamer::socket::SocketAddrSpace,
solana_test_validator::TestValidator,
std::str::FromStr,
};
#[test]
fn test_cli_create_extend_and_freeze_address_lookup_table() {
let mint_keypair = Keypair::new();
let mint_pubkey = mint_keypair.pubkey();
let faucet_addr = run_local_faucet(mint_keypair, None);
let test_validator =
TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified);
let mut config = CliConfig::recent_for_tests();
let keypair = Keypair::new();
config.json_rpc_url = test_validator.rpc_url();
config.signers = vec![&keypair];
config.output_format = OutputFormat::JsonCompact;
// Airdrop SOL for transaction fees
config.command = CliCommand::Airdrop {
pubkey: None,
lamports: 10 * LAMPORTS_PER_SOL,
};
process_command(&config).unwrap();
// Create lookup table
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::CreateLookupTable {
authority_signer_index: 0,
payer_signer_index: 0,
});
let response: CliAddressLookupTableCreated =
serde_json::from_str(&process_command(&config).unwrap()).unwrap();
let lookup_table_pubkey = Pubkey::from_str(&response.lookup_table_address).unwrap();
// Validate created lookup table
{
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::ShowLookupTable {
lookup_table_pubkey,
});
let response: CliAddressLookupTable =
serde_json::from_str(&process_command(&config).unwrap()).unwrap();
assert_eq!(
response,
CliAddressLookupTable {
lookup_table_address: lookup_table_pubkey.to_string(),
authority: Some(keypair.pubkey().to_string()),
deactivation_slot: u64::MAX,
last_extended_slot: 0,
addresses: vec![],
}
);
}
// Extend lookup table
let new_addresses: Vec<Pubkey> = (0..5).map(|_| Pubkey::new_unique()).collect();
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::ExtendLookupTable {
lookup_table_pubkey,
authority_signer_index: 0,
payer_signer_index: 0,
new_addresses: new_addresses.clone(),
});
process_command(&config).unwrap();
// Validate extended lookup table
{
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::ShowLookupTable {
lookup_table_pubkey,
});
let CliAddressLookupTable {
addresses,
last_extended_slot,
..
} = serde_json::from_str(&process_command(&config).unwrap()).unwrap();
assert_eq!(
addresses
.into_iter()
.map(|address| Pubkey::from_str(&address).unwrap())
.collect::<Vec<Pubkey>>(),
new_addresses
);
assert!(last_extended_slot > 0);
}
// Freeze lookup table w/o bypass
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::FreezeLookupTable {
lookup_table_pubkey,
authority_signer_index: 0,
bypass_warning: false,
});
let process_err = process_command(&config).unwrap_err();
assert_eq!(process_err.to_string(), FREEZE_LOOKUP_TABLE_WARNING);
// Freeze lookup table w/ bypass
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::FreezeLookupTable {
lookup_table_pubkey,
authority_signer_index: 0,
bypass_warning: true,
});
process_command(&config).unwrap();
// Validate frozen lookup table
{
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::ShowLookupTable {
lookup_table_pubkey,
});
let CliAddressLookupTable { authority, .. } =
serde_json::from_str(&process_command(&config).unwrap()).unwrap();
assert!(authority.is_none());
}
}
#[test]
fn test_cli_create_and_deactivate_address_lookup_table() {
let mint_keypair = Keypair::new();
let mint_pubkey = mint_keypair.pubkey();
let faucet_addr = run_local_faucet(mint_keypair, None);
let test_validator =
TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified);
let mut config = CliConfig::recent_for_tests();
let keypair = Keypair::new();
config.json_rpc_url = test_validator.rpc_url();
config.signers = vec![&keypair];
config.output_format = OutputFormat::JsonCompact;
// Airdrop SOL for transaction fees
config.command = CliCommand::Airdrop {
pubkey: None,
lamports: 10 * LAMPORTS_PER_SOL,
};
process_command(&config).unwrap();
// Create lookup table
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::CreateLookupTable {
authority_signer_index: 0,
payer_signer_index: 0,
});
let response: CliAddressLookupTableCreated =
serde_json::from_str(&process_command(&config).unwrap()).unwrap();
let lookup_table_pubkey = Pubkey::from_str(&response.lookup_table_address).unwrap();
// Validate created lookup table
{
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::ShowLookupTable {
lookup_table_pubkey,
});
let response: CliAddressLookupTable =
serde_json::from_str(&process_command(&config).unwrap()).unwrap();
assert_eq!(
response,
CliAddressLookupTable {
lookup_table_address: lookup_table_pubkey.to_string(),
authority: Some(keypair.pubkey().to_string()),
deactivation_slot: u64::MAX,
last_extended_slot: 0,
addresses: vec![],
}
);
}
// Deactivate lookup table w/o bypass
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::DeactivateLookupTable {
lookup_table_pubkey,
authority_signer_index: 0,
bypass_warning: false,
});
let process_err = process_command(&config).unwrap_err();
assert_eq!(process_err.to_string(), DEACTIVATE_LOOKUP_TABLE_WARNING);
// Deactivate lookup table w/ bypass
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::DeactivateLookupTable {
lookup_table_pubkey,
authority_signer_index: 0,
bypass_warning: true,
});
process_command(&config).unwrap();
// Validate deactivated lookup table
{
config.command =
CliCommand::AddressLookupTable(AddressLookupTableCliCommand::ShowLookupTable {
lookup_table_pubkey,
});
let CliAddressLookupTable {
deactivation_slot, ..
} = serde_json::from_str(&process_command(&config).unwrap()).unwrap();
assert_ne!(deactivation_slot, u64::MAX);
}
}