Browse Source

cli: Add print base64 instruction option for some of the IDL commands (#2486)

Co-authored-by: acheron <acheroncrypto@gmail.com>
chalda 2 years ago
parent
commit
41a4d820d3
4 changed files with 159 additions and 59 deletions
  1. 1 0
      CHANGELOG.md
  2. 2 0
      Cargo.lock
  3. 2 0
      cli/Cargo.toml
  4. 154 59
      cli/src/lib.rs

+ 1 - 0
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
 

+ 2 - 0
Cargo.lock

@@ -169,6 +169,8 @@ dependencies = [
  "anchor-lang",
  "anchor-syn",
  "anyhow",
+ "base64 0.13.1",
+ "bincode",
  "cargo_toml",
  "chrono",
  "clap 4.2.4",

+ 2 - 0
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" }

+ 154 - 59
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<IdlAccount> {
+    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<Pubkey>,
     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,