diff --git a/CHANGELOG.md b/CHANGELOG.md index 63f5bd5364..784473b24d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The minor version will be incremented upon a breaking change and the patch versi - spl: Add `token_program` constraint to `Token`, `Mint`, and `AssociatedToken` accounts in order to override required `token_program` fields and use different token interface implementations in the same instruction ([#2460](https://github.com/coral-xyz/anchor/pull/2460)) - cli: Add support for Solidity programs. `anchor init` and `anchor new` take an option `--solidity` which creates solidity code rather than rust. `anchor build` and `anchor test` work accordingly ([#2421](https://github.com/coral-xyz/anchor/pull/2421)) - bench: Add benchmarking for compute units usage ([#2466](https://github.com/coral-xyz/anchor/pull/2466)) +- cli: `idl set-buffer`, `idl set-authority` and `idl close` take an option `--print-only`. which prints transaction in a base64 Borsh compatible format but not sent to the cluster. It's helpful when managing authority under a multisig, e.g., a user can create a proposal for a `Custom Instruction` in SPL Governance ([#2486](https://github.com/coral-xyz/anchor/pull/2486)). ### Fixes diff --git a/Cargo.lock b/Cargo.lock index a56e49d6c6..7da4ed4378 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,8 @@ dependencies = [ "anchor-lang", "anchor-syn", "anyhow", + "base64 0.13.1", + "bincode", "cargo_toml", "chrono", "clap 4.2.4", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 63537baf59..1d8665d756 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,6 +19,8 @@ default = [] [dependencies] clap = { version = "4.2.4", features = ["derive"] } anyhow = "1.0.32" +base64 = "0.13.1" +bincode = "1.3.3" syn = { version = "1.0.60", features = ["full", "extra-traits"] } anchor-lang = { path = "../lang", version = "0.27.0" } anchor-client = { path = "../client", version = "0.27.0" } diff --git a/cli/src/lib.rs b/cli/src/lib.rs index b1df254e4a..0d2dd1f386 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -336,6 +336,10 @@ pub enum IdlCommand { }, Close { program_id: Pubkey, + /// When used, the content of the instruction will only be printed in base64 form and not executed. + /// Useful for multisig execution when the local wallet keypair is not available. + #[clap(long)] + print_only: bool, }, /// Writes an IDL into a buffer account. This can be used with SetBuffer /// to perform an upgrade. @@ -350,6 +354,10 @@ pub enum IdlCommand { /// Address of the buffer account to set as the idl on the program. #[clap(short, long)] buffer: Pubkey, + /// When used, the content of the instruction will only be printed in base64 form and not executed. + /// Useful for multisig execution when the local wallet keypair is not available. + #[clap(long)] + print_only: bool, }, /// Upgrades the IDL to the new file. An alias for first writing and then /// then setting the idl buffer account. @@ -369,6 +377,10 @@ pub enum IdlCommand { /// New authority of the IDL account. #[clap(short, long)] new_authority: Pubkey, + /// When used, the content of the instruction will only be printed in base64 form and not executed. + /// Useful for multisig execution when the local wallet keypair is not available. + #[clap(long)] + print_only: bool, }, /// Command to remove the ability to modify the IDL account. This should /// likely be used in conjection with eliminating an "upgrade authority" on @@ -1823,14 +1835,19 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { program_id, filepath, } => idl_init(cfg_override, program_id, filepath), - IdlCommand::Close { program_id } => idl_close(cfg_override, program_id), + IdlCommand::Close { + program_id, + print_only, + } => idl_close(cfg_override, program_id, print_only), IdlCommand::WriteBuffer { program_id, filepath, } => idl_write_buffer(cfg_override, program_id, filepath).map(|_| ()), - IdlCommand::SetBuffer { program_id, buffer } => { - idl_set_buffer(cfg_override, program_id, buffer) - } + IdlCommand::SetBuffer { + program_id, + buffer, + print_only, + } => idl_set_buffer(cfg_override, program_id, buffer, print_only), IdlCommand::Upgrade { program_id, filepath, @@ -1839,7 +1856,8 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { program_id, address, new_authority, - } => idl_set_authority(cfg_override, program_id, address, new_authority), + print_only, + } => idl_set_authority(cfg_override, program_id, address, new_authority, print_only), IdlCommand::EraseAuthority { program_id } => idl_erase_authority(cfg_override, program_id), IdlCommand::Authority { program_id } => idl_authority(cfg_override, program_id), IdlCommand::Parse { @@ -1852,6 +1870,12 @@ fn idl(cfg_override: &ConfigOverride, subcmd: IdlCommand) -> Result<()> { } } +fn get_idl_account(client: &RpcClient, idl_address: &Pubkey) -> Result { + let account = client.get_account(idl_address)?; + let mut data: &[u8] = &account.data; + AccountDeserialize::try_deserialize(&mut data).map_err(|e| anyhow!("{:?}", e)) +} + fn idl_init(cfg_override: &ConfigOverride, program_id: Pubkey, idl_filepath: String) -> Result<()> { with_workspace(cfg_override, |cfg| { let keypair = cfg.provider.wallet.to_string(); @@ -1866,12 +1890,14 @@ fn idl_init(cfg_override: &ConfigOverride, program_id: Pubkey, idl_filepath: Str }) } -fn idl_close(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> { +fn idl_close(cfg_override: &ConfigOverride, program_id: Pubkey, print_only: bool) -> Result<()> { with_workspace(cfg_override, |cfg| { let idl_address = IdlAccount::address(&program_id); - idl_close_account(cfg, &program_id, idl_address)?; + idl_close_account(cfg, &program_id, idl_address, print_only)?; - println!("Idl account closed: {idl_address:?}"); + if !print_only { + println!("Idl account closed: {idl_address:?}"); + } Ok(()) }) @@ -1897,19 +1923,30 @@ fn idl_write_buffer( }) } -fn idl_set_buffer(cfg_override: &ConfigOverride, program_id: Pubkey, buffer: Pubkey) -> Result<()> { +fn idl_set_buffer( + cfg_override: &ConfigOverride, + program_id: Pubkey, + buffer: Pubkey, + print_only: bool, +) -> Result<()> { with_workspace(cfg_override, |cfg| { let keypair = solana_sdk::signature::read_keypair_file(&cfg.provider.wallet.to_string()) .map_err(|_| anyhow!("Unable to read keypair file"))?; let url = cluster_url(cfg, &cfg.test_validator); let client = RpcClient::new(url); + let idl_address = IdlAccount::address(&program_id); + let idl_authority = if print_only { + get_idl_account(&client, &idl_address)?.authority + } else { + keypair.pubkey() + }; // Instruction to set the buffer onto the IdlAccount. - let set_buffer_ix = { + let ix = { let accounts = vec![ AccountMeta::new(buffer, false), - AccountMeta::new(IdlAccount::address(&program_id), false), - AccountMeta::new(keypair.pubkey(), true), + AccountMeta::new(idl_address, false), + AccountMeta::new(idl_authority, true), ]; let mut data = anchor_lang::idl::IDL_IX_TAG.to_le_bytes().to_vec(); data.append(&mut IdlInstruction::SetBuffer.try_to_vec()?); @@ -1920,20 +1957,24 @@ fn idl_set_buffer(cfg_override: &ConfigOverride, program_id: Pubkey, buffer: Pub } }; - // Build the transaction. - let latest_hash = client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - &[set_buffer_ix], - Some(&keypair.pubkey()), - &[&keypair], - latest_hash, - ); + if print_only { + print_idl_instruction("SetBuffer", &ix, &idl_address)?; + } else { + // Build the transaction. + let latest_hash = client.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&keypair.pubkey()), + &[&keypair], + latest_hash, + ); - // Send the transaction. - client.send_and_confirm_transaction_with_spinner_and_commitment( - &tx, - CommitmentConfig::confirmed(), - )?; + // Send the transaction. + client.send_and_confirm_transaction_with_spinner_and_commitment( + &tx, + CommitmentConfig::confirmed(), + )?; + } Ok(()) }) @@ -1945,7 +1986,7 @@ fn idl_upgrade( idl_filepath: String, ) -> Result<()> { let buffer = idl_write_buffer(cfg_override, program_id, idl_filepath)?; - idl_set_buffer(cfg_override, program_id, buffer) + idl_set_buffer(cfg_override, program_id, buffer, false) } fn idl_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> { @@ -1964,9 +2005,7 @@ fn idl_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<() } }; - let account = client.get_account(&idl_address)?; - let mut data: &[u8] = &account.data; - let idl_account: IdlAccount = AccountDeserialize::try_deserialize(&mut data)?; + let idl_account = get_idl_account(&client, &idl_address)?; println!("{:?}", idl_account.authority); @@ -1979,6 +2018,7 @@ fn idl_set_authority( program_id: Pubkey, address: Option, new_authority: Pubkey, + print_only: bool, ) -> Result<()> { with_workspace(cfg_override, |cfg| { // Misc. @@ -1991,6 +2031,12 @@ fn idl_set_authority( let url = cluster_url(cfg, &cfg.test_validator); let client = RpcClient::new(url); + let idl_authority = if print_only { + get_idl_account(&client, &idl_address)?.authority + } else { + keypair.pubkey() + }; + // Instruction data. let data = serialize_idl_ix(anchor_lang::idl::IdlInstruction::SetAuthority { new_authority })?; @@ -1998,7 +2044,7 @@ fn idl_set_authority( // Instruction accounts. let accounts = vec![ AccountMeta::new(idl_address, false), - AccountMeta::new_readonly(keypair.pubkey(), true), + AccountMeta::new_readonly(idl_authority, true), ]; // Instruction. @@ -2007,20 +2053,25 @@ fn idl_set_authority( accounts, data, }; - // Send transaction. - let latest_hash = client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&keypair.pubkey()), - &[&keypair], - latest_hash, - ); - client.send_and_confirm_transaction_with_spinner_and_commitment( - &tx, - CommitmentConfig::confirmed(), - )?; - println!("Authority update complete."); + if print_only { + print_idl_instruction("SetAuthority", &ix, &idl_address)?; + } else { + // Send transaction. + let latest_hash = client.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&keypair.pubkey()), + &[&keypair], + latest_hash, + ); + client.send_and_confirm_transaction_with_spinner_and_commitment( + &tx, + CommitmentConfig::confirmed(), + )?; + + println!("Authority update complete."); + } Ok(()) }) @@ -2037,22 +2088,32 @@ fn idl_erase_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Res return Ok(()); } - idl_set_authority(cfg_override, program_id, None, ERASED_AUTHORITY)?; + idl_set_authority(cfg_override, program_id, None, ERASED_AUTHORITY, false)?; Ok(()) } -fn idl_close_account(cfg: &Config, program_id: &Pubkey, idl_address: Pubkey) -> Result<()> { +fn idl_close_account( + cfg: &Config, + program_id: &Pubkey, + idl_address: Pubkey, + print_only: bool, +) -> Result<()> { let keypair = solana_sdk::signature::read_keypair_file(&cfg.provider.wallet.to_string()) .map_err(|_| anyhow!("Unable to read keypair file"))?; let url = cluster_url(cfg, &cfg.test_validator); let client = RpcClient::new(url); + let idl_authority = if print_only { + get_idl_account(&client, &idl_address)?.authority + } else { + keypair.pubkey() + }; // Instruction accounts. let accounts = vec![ AccountMeta::new(idl_address, false), - AccountMeta::new_readonly(keypair.pubkey(), true), - AccountMeta::new(keypair.pubkey(), true), + AccountMeta::new_readonly(idl_authority, true), + AccountMeta::new(keypair.pubkey(), false), ]; // Instruction. let ix = Instruction { @@ -2060,18 +2121,23 @@ fn idl_close_account(cfg: &Config, program_id: &Pubkey, idl_address: Pubkey) -> accounts, data: { serialize_idl_ix(anchor_lang::idl::IdlInstruction::Close {})? }, }; - // Send transaction. - let latest_hash = client.get_latest_blockhash()?; - let tx = Transaction::new_signed_with_payer( - &[ix], - Some(&keypair.pubkey()), - &[&keypair], - latest_hash, - ); - client.send_and_confirm_transaction_with_spinner_and_commitment( - &tx, - CommitmentConfig::confirmed(), - )?; + + if print_only { + print_idl_instruction("Close", &ix, &idl_address)?; + } else { + // Send transaction. + let latest_hash = client.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&keypair.pubkey()), + &[&keypair], + latest_hash, + ); + client.send_and_confirm_transaction_with_spinner_and_commitment( + &tx, + CommitmentConfig::confirmed(), + )?; + } Ok(()) } @@ -2179,6 +2245,35 @@ fn write_idl(idl: &Idl, out: OutFile) -> Result<()> { Ok(()) } +/// Print `base64+borsh` encoded IDL instruction. +fn print_idl_instruction(ix_name: &str, ix: &Instruction, idl_address: &Pubkey) -> Result<()> { + println!("Print only mode. No execution!"); + println!("Instruction: {ix_name}"); + println!("IDL address: {idl_address}"); + println!("Program: {}", ix.program_id); + + // Serialize with `bincode` because `Instruction` does not implement `BorshSerialize` + let mut serialized_ix = bincode::serialize(ix)?; + + // Remove extra bytes in order to make the serialized instruction `borsh` compatible + // `bincode` uses 8 bytes(LE) for length meanwhile `borsh` uses 4 bytes(LE) + let mut remove_extra_vec_bytes = |index: usize| { + serialized_ix.drain((index + 4)..(index + 8)); + }; + + let accounts_index = std::mem::size_of_val(&ix.program_id); + remove_extra_vec_bytes(accounts_index); + let data_index = accounts_index + 4 + std::mem::size_of_val(&*ix.accounts); + remove_extra_vec_bytes(data_index); + + println!( + "Base64 encoded instruction: {}", + base64::encode(serialized_ix) + ); + + Ok(()) +} + fn account( cfg_override: &ConfigOverride, account_type: String,