use {
crate::{
error::GovernanceError,
state::{
enums::{GovernanceAccountType, TransactionExecutionStatus},
legacy::ProposalInstructionV1,
},
PROGRAM_AUTHORITY_SEED,
},
borsh::{maybestd::io::Write, BorshDeserialize, BorshSchema, BorshSerialize},
core::panic,
solana_program::{
account_info::AccountInfo,
clock::UnixTimestamp,
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
program_pack::IsInitialized,
pubkey::Pubkey,
},
spl_governance_tools::account::{get_account_data, get_account_type, AccountMaxSize},
};
#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct InstructionData {
pub program_id: Pubkey,
pub accounts: Vec<AccountMetaData>,
pub data: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct AccountMetaData {
pub pubkey: Pubkey,
pub is_signer: bool,
pub is_writable: bool,
}
impl From<Instruction> for InstructionData {
fn from(instruction: Instruction) -> Self {
InstructionData {
program_id: instruction.program_id,
accounts: instruction
.accounts
.iter()
.map(|a| AccountMetaData {
pubkey: a.pubkey,
is_signer: a.is_signer,
is_writable: a.is_writable,
})
.collect(),
data: instruction.data,
}
}
}
impl From<&InstructionData> for Instruction {
fn from(instruction: &InstructionData) -> Self {
Instruction {
program_id: instruction.program_id,
accounts: instruction
.accounts
.iter()
.map(|a| AccountMeta {
pubkey: a.pubkey,
is_signer: a.is_signer,
is_writable: a.is_writable,
})
.collect(),
data: instruction.data.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct ProposalTransactionV2 {
pub account_type: GovernanceAccountType,
pub proposal: Pubkey,
pub option_index: u8,
pub transaction_index: u16,
pub hold_up_time: u32,
pub instructions: Vec<InstructionData>,
pub executed_at: Option<UnixTimestamp>,
pub execution_status: TransactionExecutionStatus,
pub reserved_v2: [u8; 8],
}
impl AccountMaxSize for ProposalTransactionV2 {
fn get_max_size(&self) -> Option<usize> {
let instructions_size = self
.instructions
.iter()
.map(|i| i.accounts.len() * 34 + i.data.len() + 40)
.sum::<usize>();
Some(instructions_size + 62)
}
}
impl IsInitialized for ProposalTransactionV2 {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::ProposalTransactionV2
}
}
impl ProposalTransactionV2 {
pub fn serialize<W: Write>(self, writer: W) -> Result<(), ProgramError> {
if self.account_type == GovernanceAccountType::ProposalTransactionV2 {
borsh::to_writer(writer, &self)?
} else if self.account_type == GovernanceAccountType::ProposalInstructionV1 {
if self.instructions.len() != 1 {
panic!("Multiple instructions are not supported by ProposalInstructionV1")
};
if self.reserved_v2 != [0; 8] {
panic!("Extended data not supported by ProposalInstructionV1")
}
let proposal_transaction_data_v1 = ProposalInstructionV1 {
account_type: self.account_type,
proposal: self.proposal,
instruction_index: self.transaction_index,
hold_up_time: self.hold_up_time,
instruction: self.instructions[0].clone(),
executed_at: self.executed_at,
execution_status: self.execution_status,
};
borsh::to_writer(writer, &proposal_transaction_data_v1)?
}
Ok(())
}
}
pub fn get_proposal_transaction_address_seeds<'a>(
proposal: &'a Pubkey,
option_index: &'a [u8; 1], instruction_index_le_bytes: &'a [u8; 2], ) -> [&'a [u8]; 4] {
[
PROGRAM_AUTHORITY_SEED,
proposal.as_ref(),
option_index,
instruction_index_le_bytes,
]
}
pub fn get_proposal_transaction_address<'a>(
program_id: &Pubkey,
proposal: &'a Pubkey,
option_index_le_bytes: &'a [u8; 1], instruction_index_le_bytes: &'a [u8; 2], ) -> Pubkey {
Pubkey::find_program_address(
&get_proposal_transaction_address_seeds(
proposal,
option_index_le_bytes,
instruction_index_le_bytes,
),
program_id,
)
.0
}
pub fn get_proposal_transaction_data(
program_id: &Pubkey,
proposal_transaction_info: &AccountInfo,
) -> Result<ProposalTransactionV2, ProgramError> {
let account_type: GovernanceAccountType =
get_account_type(program_id, proposal_transaction_info)?;
if account_type == GovernanceAccountType::ProposalInstructionV1 {
let proposal_transaction_data_v1 =
get_account_data::<ProposalInstructionV1>(program_id, proposal_transaction_info)?;
return Ok(ProposalTransactionV2 {
account_type,
proposal: proposal_transaction_data_v1.proposal,
option_index: 0, transaction_index: proposal_transaction_data_v1.instruction_index,
hold_up_time: proposal_transaction_data_v1.hold_up_time,
instructions: vec![proposal_transaction_data_v1.instruction],
executed_at: proposal_transaction_data_v1.executed_at,
execution_status: proposal_transaction_data_v1.execution_status,
reserved_v2: [0; 8],
});
}
get_account_data::<ProposalTransactionV2>(program_id, proposal_transaction_info)
}
pub fn get_proposal_transaction_data_for_proposal(
program_id: &Pubkey,
proposal_transaction_info: &AccountInfo,
proposal: &Pubkey,
) -> Result<ProposalTransactionV2, ProgramError> {
let proposal_transaction_data =
get_proposal_transaction_data(program_id, proposal_transaction_info)?;
if proposal_transaction_data.proposal != *proposal {
return Err(GovernanceError::InvalidProposalForProposalTransaction.into());
}
Ok(proposal_transaction_data)
}
#[cfg(test)]
mod test {
use {
super::*,
base64::{engine::general_purpose, Engine as _},
solana_program::{bpf_loader_upgradeable, clock::Epoch},
std::str::FromStr,
};
fn create_test_account_meta_data() -> AccountMetaData {
AccountMetaData {
pubkey: Pubkey::new_unique(),
is_signer: true,
is_writable: false,
}
}
fn create_test_instruction_data() -> Vec<InstructionData> {
vec![InstructionData {
program_id: Pubkey::new_unique(),
accounts: vec![
create_test_account_meta_data(),
create_test_account_meta_data(),
create_test_account_meta_data(),
],
data: vec![1, 2, 3],
}]
}
fn create_test_proposal_transaction() -> ProposalTransactionV2 {
ProposalTransactionV2 {
account_type: GovernanceAccountType::ProposalTransactionV2,
proposal: Pubkey::new_unique(),
option_index: 0,
transaction_index: 1,
hold_up_time: 10,
instructions: create_test_instruction_data(),
executed_at: Some(100),
execution_status: TransactionExecutionStatus::Success,
reserved_v2: [0; 8],
}
}
#[test]
fn test_account_meta_data_size() {
let account_meta_data = create_test_account_meta_data();
let size = account_meta_data.try_to_vec().unwrap().len();
assert_eq!(34, size);
}
#[test]
fn test_proposal_transaction_max_size() {
let proposal_transaction = create_test_proposal_transaction();
let size = proposal_transaction.try_to_vec().unwrap().len();
assert_eq!(proposal_transaction.get_max_size(), Some(size));
}
#[test]
fn test_empty_proposal_transaction_max_size() {
let mut proposal_transaction = create_test_proposal_transaction();
proposal_transaction.instructions[0].data = vec![];
proposal_transaction.instructions[0].accounts = vec![];
let size = proposal_transaction.try_to_vec().unwrap().len();
assert_eq!(proposal_transaction.get_max_size(), Some(size));
}
#[test]
fn test_upgrade_instruction_serialization() {
let program_address =
Pubkey::from_str("Hita5Lun87S4MADAF4vGoWEgFm5DyuVqxoWzzqYxS3AD").unwrap();
let buffer_address =
Pubkey::from_str("5XqXkgJGAUwrUHBkxbKpYMGqsRoQLfyqRbYUEkjNY6hL").unwrap();
let governance = Pubkey::from_str("FqSReK9R8QxvFZgdrAwGT3gsYp1ZGfiFjS8xrzyyadn3").unwrap();
let upgrade_instruction = bpf_loader_upgradeable::upgrade(
&program_address,
&buffer_address,
&governance,
&governance,
);
let instruction_data: InstructionData = upgrade_instruction.clone().into();
let mut instruction_bytes = vec![];
instruction_data.serialize(&mut instruction_bytes).unwrap();
let encoded = general_purpose::STANDARD_NO_PAD.encode(&instruction_bytes);
let instruction =
Instruction::from(&InstructionData::deserialize(&mut &instruction_bytes[..]).unwrap());
assert_eq!(upgrade_instruction, instruction);
assert_eq!(encoded,"Aqj2kU6IobDiEBU+92OuKwDCuT0WwSTSwFN6EASAAAAHAAAAchkHXTU9jF+rKpILT6dzsVyNI9NsQy9cab+GGvdwNn0AAfh2HVruy2YibpgcQUmJf5att5YdPXSv1k2pRAKAfpSWAAFDVQuXWos2urmegSPblI813GlTm7CJ/8rv+9yzNE3yfwAB3Gw+apCyfrRNqJ6f1160Htkx+uYZT6FIILQ3WzNA4KwAAQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAAAAGp9UXGMd0yShWY5hpHV62i164o5tLbVxzVVshAAAAAAAA3Gw+apCyfrRNqJ6f1160Htkx+uYZT6FIILQ3WzNA4KwBAAQAAAADAAAA");
}
#[test]
fn test_proposal_transaction_v1_to_v2_serialization_roundtrip() {
let proposal_transaction_v1_source = ProposalInstructionV1 {
account_type: GovernanceAccountType::ProposalInstructionV1,
proposal: Pubkey::new_unique(),
instruction_index: 1,
hold_up_time: 120,
instruction: create_test_instruction_data()[0].clone(),
executed_at: Some(155),
execution_status: TransactionExecutionStatus::Success,
};
let mut account_data = vec![];
proposal_transaction_v1_source
.serialize(&mut account_data)
.unwrap();
let program_id = Pubkey::new_unique();
let info_key = Pubkey::new_unique();
let mut lamports = 10u64;
let account_info = AccountInfo::new(
&info_key,
false,
false,
&mut lamports,
&mut account_data[..],
&program_id,
false,
Epoch::default(),
);
let proposal_transaction_v2 =
get_proposal_transaction_data(&program_id, &account_info).unwrap();
proposal_transaction_v2
.serialize(&mut account_info.data.borrow_mut()[..])
.unwrap();
let proposal_transaction_v1_target =
get_account_data::<ProposalInstructionV1>(&program_id, &account_info).unwrap();
assert_eq!(
proposal_transaction_v1_source,
proposal_transaction_v1_target
)
}
}