diff --git a/Cargo.lock b/Cargo.lock index 8616c2632f..9348db394a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index f45c5713e4..645b7b66fb 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -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, + pub deactivation_slot: u64, + pub last_extended_slot: u64, + pub addresses: Vec, +} +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, diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1ed5ddaef6..6fdfa258c4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -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" } diff --git a/cli/src/address_lookup_table.rs b/cli/src/address_lookup_table.rs new file mode 100644 index 0000000000..7f0fa9d313 --- /dev/null +++ b/cli/src/address_lookup_table.rs @@ -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, + }, + 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>, +) -> Result { + 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 = 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, + 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, +) -> 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(), + })) +} diff --git a/cli/src/clap_app.rs b/cli/src/clap_app.rs index 3d48ed3716..1760b51617 100644 --- a/cli/src/clap_app.rs +++ b/cli/src/clap_app.rs @@ -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() diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 2a2397efd3..d202a2a69f 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -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) + } } } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 85d90869ff..c271990b58 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -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; diff --git a/cli/tests/address_lookup_table.rs b/cli/tests/address_lookup_table.rs new file mode 100644 index 0000000000..5d370d48c4 --- /dev/null +++ b/cli/tests/address_lookup_table.rs @@ -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 = (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::>(), + 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); + } +}