Skip to content

Commit

Permalink
cli: Add print base64 instruction option for some of the IDL commands (
Browse files Browse the repository at this point in the history
…coral-xyz#2486)

Co-authored-by: acheron <acheroncrypto@gmail.com>
  • Loading branch information
ochaloup and acheroncrypto authored May 18, 2023
1 parent b7bada1 commit 41a4d82
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
213 changes: 154 additions & 59 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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(())
})
Expand All @@ -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()?);
Expand All @@ -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(())
})
Expand All @@ -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<()> {
Expand All @@ -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);

Expand All @@ -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.
Expand All @@ -1991,14 +2031,20 @@ 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 })?;

// Instruction accounts.
let accounts = vec![
AccountMeta::new(idl_address, false),
AccountMeta::new_readonly(keypair.pubkey(), true),
AccountMeta::new_readonly(idl_authority, true),
];

// Instruction.
Expand All @@ -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(())
})
Expand All @@ -2037,41 +2088,56 @@ 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 {
program_id: *program_id,
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(())
}
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 41a4d82

Please sign in to comment.