cli: add solana program extend subcommand (#34043)
This commit is contained in:
parent
b7f839ea18
commit
34f5c68416
|
@ -2316,6 +2316,26 @@ impl fmt::Display for CliUpgradeableProgramClosed {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliUpgradeableProgramExtended {
|
||||
pub program_id: String,
|
||||
pub additional_bytes: u32,
|
||||
}
|
||||
impl QuietDisplay for CliUpgradeableProgramExtended {}
|
||||
impl VerboseDisplay for CliUpgradeableProgramExtended {}
|
||||
impl fmt::Display for CliUpgradeableProgramExtended {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f)?;
|
||||
writeln!(
|
||||
f,
|
||||
"Extended Program Id {} by {} bytes",
|
||||
&self.program_id, self.additional_bytes,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliUpgradeableBuffer {
|
||||
|
|
|
@ -17,7 +17,7 @@ use {
|
|||
solana_cli_output::{
|
||||
CliProgram, CliProgramAccountType, CliProgramAuthority, CliProgramBuffer, CliProgramId,
|
||||
CliUpgradeableBuffer, CliUpgradeableBuffers, CliUpgradeableProgram,
|
||||
CliUpgradeableProgramClosed, CliUpgradeablePrograms,
|
||||
CliUpgradeableProgramClosed, CliUpgradeableProgramExtended, CliUpgradeablePrograms,
|
||||
},
|
||||
solana_client::{
|
||||
connection_cache::ConnectionCache,
|
||||
|
@ -124,6 +124,10 @@ pub enum ProgramCliCommand {
|
|||
use_lamports_unit: bool,
|
||||
bypass_warning: bool,
|
||||
},
|
||||
ExtendProgram {
|
||||
program_pubkey: Pubkey,
|
||||
additional_bytes: u32,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait ProgramSubCommands {
|
||||
|
@ -417,6 +421,28 @@ impl ProgramSubCommands for App<'_, '_> {
|
|||
.help("Bypass the permanent program closure warning"),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("extend")
|
||||
.about("Extend the length of an upgradeable program to deploy larger programs")
|
||||
.arg(
|
||||
Arg::with_name("program_id")
|
||||
.index(1)
|
||||
.value_name("PROGRAM_ID")
|
||||
.takes_value(true)
|
||||
.required(true)
|
||||
.validator(is_valid_pubkey)
|
||||
.help("Address of the program to extend"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("additional_bytes")
|
||||
.index(2)
|
||||
.value_name("ADDITIONAL_BYTES")
|
||||
.takes_value(true)
|
||||
.required(true)
|
||||
.validator(is_parsable::<u32>)
|
||||
.help("Number of bytes that will be allocated for the program's data account")
|
||||
)
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("deploy")
|
||||
|
@ -675,6 +701,26 @@ pub fn parse_program_subcommand(
|
|||
signers: signer_info.signers,
|
||||
}
|
||||
}
|
||||
("extend", Some(matches)) => {
|
||||
let program_pubkey = pubkey_of(matches, "program_id").unwrap();
|
||||
let additional_bytes = value_of(matches, "additional_bytes").unwrap();
|
||||
|
||||
let signer_info = default_signer.generate_unique_signers(
|
||||
vec![Some(
|
||||
default_signer.signer_from_path(matches, wallet_manager)?,
|
||||
)],
|
||||
matches,
|
||||
wallet_manager,
|
||||
)?;
|
||||
|
||||
CliCommandInfo {
|
||||
command: CliCommand::Program(ProgramCliCommand::ExtendProgram {
|
||||
program_pubkey,
|
||||
additional_bytes,
|
||||
}),
|
||||
signers: signer_info.signers,
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Ok(response)
|
||||
|
@ -799,6 +845,10 @@ pub fn process_program_subcommand(
|
|||
*use_lamports_unit,
|
||||
*bypass_warning,
|
||||
),
|
||||
ProgramCliCommand::ExtendProgram {
|
||||
program_pubkey,
|
||||
additional_bytes,
|
||||
} => process_extend_program(&rpc_client, config, *program_pubkey, *additional_bytes),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1716,6 +1766,100 @@ fn process_close(
|
|||
}
|
||||
}
|
||||
|
||||
fn process_extend_program(
|
||||
rpc_client: &RpcClient,
|
||||
config: &CliConfig,
|
||||
program_pubkey: Pubkey,
|
||||
additional_bytes: u32,
|
||||
) -> ProcessResult {
|
||||
let payer_pubkey = config.signers[0].pubkey();
|
||||
|
||||
if additional_bytes == 0 {
|
||||
return Err("Additional bytes must be greater than zero".into());
|
||||
}
|
||||
|
||||
let program_account = match rpc_client
|
||||
.get_account_with_commitment(&program_pubkey, config.commitment)?
|
||||
.value
|
||||
{
|
||||
Some(program_account) => Ok(program_account),
|
||||
None => Err(format!("Unable to find program {program_pubkey}")),
|
||||
}?;
|
||||
|
||||
if !bpf_loader_upgradeable::check_id(&program_account.owner) {
|
||||
return Err(format!("Account {program_pubkey} is not an upgradeable program").into());
|
||||
}
|
||||
|
||||
let programdata_pubkey = match program_account.state() {
|
||||
Ok(UpgradeableLoaderState::Program {
|
||||
programdata_address: programdata_pubkey,
|
||||
}) => Ok(programdata_pubkey),
|
||||
_ => Err(format!(
|
||||
"Account {program_pubkey} is not an upgradeable program"
|
||||
)),
|
||||
}?;
|
||||
|
||||
let programdata_account = match rpc_client
|
||||
.get_account_with_commitment(&programdata_pubkey, config.commitment)?
|
||||
.value
|
||||
{
|
||||
Some(programdata_account) => Ok(programdata_account),
|
||||
None => Err(format!("Program {program_pubkey} is closed")),
|
||||
}?;
|
||||
|
||||
let upgrade_authority_address = match programdata_account.state() {
|
||||
Ok(UpgradeableLoaderState::ProgramData {
|
||||
slot: _,
|
||||
upgrade_authority_address,
|
||||
}) => Ok(upgrade_authority_address),
|
||||
_ => Err(format!("Program {program_pubkey} is closed")),
|
||||
}?;
|
||||
|
||||
match upgrade_authority_address {
|
||||
None => Err(format!("Program {program_pubkey} is not upgradeable")),
|
||||
_ => Ok(()),
|
||||
}?;
|
||||
|
||||
let blockhash = rpc_client.get_latest_blockhash()?;
|
||||
|
||||
let mut tx = Transaction::new_unsigned(Message::new(
|
||||
&[bpf_loader_upgradeable::extend_program(
|
||||
&program_pubkey,
|
||||
Some(&payer_pubkey),
|
||||
additional_bytes,
|
||||
)],
|
||||
Some(&payer_pubkey),
|
||||
));
|
||||
|
||||
tx.try_sign(&[config.signers[0]], blockhash)?;
|
||||
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
|
||||
&tx,
|
||||
config.commitment,
|
||||
RpcSendTransactionConfig {
|
||||
preflight_commitment: Some(config.commitment.commitment),
|
||||
..RpcSendTransactionConfig::default()
|
||||
},
|
||||
);
|
||||
if let Err(err) = result {
|
||||
if let ClientErrorKind::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::InvalidInstructionData,
|
||||
)) = err.kind()
|
||||
{
|
||||
return Err("Extending a program is not supported by the cluster".into());
|
||||
} else {
|
||||
return Err(format!("Extend program failed: {err}").into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config
|
||||
.output_format
|
||||
.formatted_string(&CliUpgradeableProgramExtended {
|
||||
program_id: program_pubkey.to_string(),
|
||||
additional_bytes,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn calculate_max_chunk_size<F>(create_msg: &F) -> usize
|
||||
where
|
||||
F: Fn(u32, Vec<u8>) -> Message,
|
||||
|
@ -3115,6 +3259,38 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_parse_extend_program() {
|
||||
let test_commands = get_clap_app("test", "desc", "version");
|
||||
|
||||
let default_keypair = Keypair::new();
|
||||
let keypair_file = make_tmp_path("keypair_file");
|
||||
write_keypair_file(&default_keypair, &keypair_file).unwrap();
|
||||
let default_signer = DefaultSigner::new("", &keypair_file);
|
||||
|
||||
// defaults
|
||||
let program_pubkey = Pubkey::new_unique();
|
||||
let additional_bytes = 100;
|
||||
|
||||
let test_command = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"program",
|
||||
"extend",
|
||||
&program_pubkey.to_string(),
|
||||
&additional_bytes.to_string(),
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_command, &default_signer, &mut None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::Program(ProgramCliCommand::ExtendProgram {
|
||||
program_pubkey,
|
||||
additional_bytes
|
||||
}),
|
||||
signers: vec![read_keypair_file(&keypair_file).unwrap().into()],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_keypair_file() {
|
||||
solana_logger::setup();
|
||||
|
|
|
@ -726,6 +726,90 @@ fn test_cli_program_close_program() {
|
|||
assert_eq!(programdata_lamports, recipient_account.lamports);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_program_extend_program() {
|
||||
solana_logger::setup();
|
||||
|
||||
let mut noop_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
noop_path.push("tests");
|
||||
noop_path.push("fixtures");
|
||||
noop_path.push("noop");
|
||||
noop_path.set_extension("so");
|
||||
|
||||
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 rpc_client =
|
||||
RpcClient::new_with_commitment(test_validator.rpc_url(), CommitmentConfig::processed());
|
||||
|
||||
let mut file = File::open(noop_path.to_str().unwrap()).unwrap();
|
||||
let mut program_data = Vec::new();
|
||||
file.read_to_end(&mut program_data).unwrap();
|
||||
let max_len = program_data.len();
|
||||
let minimum_balance_for_programdata = rpc_client
|
||||
.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_programdata(
|
||||
max_len,
|
||||
))
|
||||
.unwrap();
|
||||
let minimum_balance_for_program = rpc_client
|
||||
.get_minimum_balance_for_rent_exemption(UpgradeableLoaderState::size_of_program())
|
||||
.unwrap();
|
||||
let upgrade_authority = Keypair::new();
|
||||
|
||||
let mut config = CliConfig::recent_for_tests();
|
||||
let keypair = Keypair::new();
|
||||
config.json_rpc_url = test_validator.rpc_url();
|
||||
config.signers = vec![&keypair];
|
||||
config.command = CliCommand::Airdrop {
|
||||
pubkey: None,
|
||||
lamports: 100 * minimum_balance_for_programdata + minimum_balance_for_program,
|
||||
};
|
||||
process_command(&config).unwrap();
|
||||
|
||||
// Deploy the upgradeable program
|
||||
let program_keypair = Keypair::new();
|
||||
config.signers = vec![&keypair, &upgrade_authority, &program_keypair];
|
||||
config.command = CliCommand::Program(ProgramCliCommand::Deploy {
|
||||
program_location: Some(noop_path.to_str().unwrap().to_string()),
|
||||
program_signer_index: Some(2),
|
||||
program_pubkey: Some(program_keypair.pubkey()),
|
||||
buffer_signer_index: None,
|
||||
buffer_pubkey: None,
|
||||
allow_excessive_balance: false,
|
||||
upgrade_authority_signer_index: 1,
|
||||
is_final: false,
|
||||
max_len: Some(max_len),
|
||||
skip_fee_check: false,
|
||||
});
|
||||
config.output_format = OutputFormat::JsonCompact;
|
||||
process_command(&config).unwrap();
|
||||
|
||||
let (programdata_pubkey, _) = Pubkey::find_program_address(
|
||||
&[program_keypair.pubkey().as_ref()],
|
||||
&bpf_loader_upgradeable::id(),
|
||||
);
|
||||
|
||||
// Wait one slot to avoid "Program was deployed in this block already" error
|
||||
wait_n_slots(&rpc_client, 1);
|
||||
|
||||
// Extend program
|
||||
let additional_bytes = 100;
|
||||
config.signers = vec![&keypair];
|
||||
config.command = CliCommand::Program(ProgramCliCommand::ExtendProgram {
|
||||
program_pubkey: program_keypair.pubkey(),
|
||||
additional_bytes,
|
||||
});
|
||||
process_command(&config).unwrap();
|
||||
|
||||
let programdata_account = rpc_client.get_account(&programdata_pubkey).unwrap();
|
||||
let expected_len =
|
||||
UpgradeableLoaderState::size_of_programdata(max_len + additional_bytes as usize);
|
||||
assert_eq!(expected_len, programdata_account.data.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_program_write_buffer() {
|
||||
solana_logger::setup();
|
||||
|
|
Loading…
Reference in New Issue